فهرست منبع

Merge Sumer release (#673)

Sumer release
Klaudiusz Dembler 3 سال پیش
والد
کامیت
4af55c4ba4
100فایلهای تغییر یافته به همراه3902 افزوده شده و 858 حذف شده
  1. 8 2
      .env
  2. 5 0
      .eslintrc.js
  3. 1 1
      .github/workflows/checks.yml
  4. 1 0
      .node-version
  5. 1 0
      .prettierignore
  6. 7 0
      .storybook/Welcome.stories.mdx
  7. 16 0
      .storybook/WithValue.tsx
  8. 39 0
      .storybook/main.js
  9. 0 0
      .storybook/manager-head.html
  10. 6 0
      .storybook/manager.js
  11. 0 0
      .storybook/preview-head.html
  12. 53 0
      .storybook/preview.jsx
  13. 10 0
      .storybook/theme.js
  14. 1 2
      codegen.config.yml
  15. 6 1
      config-overrides.js
  16. 3 3
      docs/overview.md
  17. 2 2
      docs/styleguide.md
  18. 5 2
      netlify-plugins/contextual-env/index.js
  19. 57 23
      package.json
  20. 320 0
      public/mockServiceWorker.js
  21. 6 2
      scripts/mocking/generateChannels.js
  22. 25 0
      scripts/mocking/generateMemberships.js
  23. 9 7
      src/App.tsx
  24. 37 0
      src/MainLayout.tsx
  25. 135 40
      src/api/client/cache.ts
  26. 47 4
      src/api/client/index.ts
  27. 11 3
      src/api/client/resolvers.ts
  28. 5 5
      src/api/hooks/categories.ts
  29. 26 17
      src/api/hooks/channel.ts
  30. 3 23
      src/api/hooks/coverVideo.ts
  31. 0 13
      src/api/hooks/featuredVideos.ts
  32. 4 2
      src/api/hooks/index.ts
  33. 32 0
      src/api/hooks/membership.ts
  34. 15 0
      src/api/hooks/queryNode.ts
  35. 14 4
      src/api/hooks/video.ts
  36. 60 0
      src/api/hooks/workers.ts
  37. 205 134
      src/api/queries/__generated__/baseTypes.generated.ts
  38. 34 25
      src/api/queries/__generated__/categories.generated.tsx
  39. 148 77
      src/api/queries/__generated__/channels.generated.tsx
  40. 123 0
      src/api/queries/__generated__/memberships.generated.tsx
  41. 53 0
      src/api/queries/__generated__/queryNode.generated.tsx
  42. 3 5
      src/api/queries/__generated__/search.generated.tsx
  43. 30 0
      src/api/queries/__generated__/shared.generated.tsx
  44. 142 152
      src/api/queries/__generated__/videos.generated.tsx
  45. 116 0
      src/api/queries/__generated__/workers.generated.tsx
  46. 4 4
      src/api/queries/categories.graphql
  47. 41 17
      src/api/queries/channels.graphql
  48. 1 0
      src/api/queries/index.ts
  49. 22 0
      src/api/queries/memberships.graphql
  50. 8 0
      src/api/queries/queryNode.graphql
  51. 0 1
      src/api/queries/search.graphql
  52. 11 0
      src/api/queries/shared.graphql
  53. 44 58
      src/api/queries/videos.graphql
  54. 19 0
      src/api/queries/workers.graphql
  55. 190 155
      src/api/schemas/extendedQueryNode.graphql
  56. 39 0
      src/assets/account-creation.svg
  57. 11 0
      src/assets/avatar-silhouette.svg
  58. 0 0
      src/assets/bg-pattern.svg
  59. 22 0
      src/assets/coins.svg
  60. 13 0
      src/assets/empty-videos-illustration.svg
  61. 5 0
      src/assets/joystream-logo.svg
  62. 5 0
      src/assets/polkadot-logo.svg
  63. 3 0
      src/assets/signin-illustration.svg
  64. 65 0
      src/assets/theater-mask.svg
  65. BIN
      src/assets/tile-example.png
  66. 30 0
      src/assets/transaction-illustration.svg
  67. BIN
      src/assets/video-example.png
  68. 16 0
      src/assets/well-blue.svg
  69. 4 3
      src/components/BackgroundPattern.tsx
  70. 7 12
      src/components/ChannelGallery.tsx
  71. 12 11
      src/components/ChannelLink/ChannelLink.tsx
  72. 13 5
      src/components/ChannelPreview.tsx
  73. 41 35
      src/components/CoverVideo/CoverVideo.style.ts
  74. 25 8
      src/components/CoverVideo/CoverVideo.tsx
  75. 81 0
      src/components/Dialogs/ActionDialog/ActionDialog.stories.tsx
  76. 107 0
      src/components/Dialogs/ActionDialog/ActionDialog.style.ts
  77. 67 0
      src/components/Dialogs/ActionDialog/ActionDialog.tsx
  78. 4 0
      src/components/Dialogs/ActionDialog/index.ts
  79. 39 0
      src/components/Dialogs/BaseDialog/BaseDialog.stories.tsx
  80. 32 0
      src/components/Dialogs/BaseDialog/BaseDialog.style.ts
  81. 53 0
      src/components/Dialogs/BaseDialog/BaseDialog.tsx
  82. 4 0
      src/components/Dialogs/BaseDialog/index.ts
  83. 102 0
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx
  84. 95 0
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.style.ts
  85. 146 0
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx
  86. 171 0
      src/components/Dialogs/ImageCropDialog/cropper.ts
  87. 4 0
      src/components/Dialogs/ImageCropDialog/index.ts
  88. 35 0
      src/components/Dialogs/MessageDialog/MessageDialog.stories.tsx
  89. 18 0
      src/components/Dialogs/MessageDialog/MessageDialog.style.ts
  90. 40 0
      src/components/Dialogs/MessageDialog/MessageDialog.tsx
  91. 4 0
      src/components/Dialogs/MessageDialog/index.ts
  92. 64 0
      src/components/Dialogs/Multistepper/Multistepper.stories.tsx
  93. 100 0
      src/components/Dialogs/Multistepper/Multistepper.style.ts
  94. 60 0
      src/components/Dialogs/Multistepper/Multistepper.tsx
  95. 3 0
      src/components/Dialogs/Multistepper/index.ts
  96. 22 0
      src/components/Dialogs/TransactionDialog/TransactionDialog.stories.tsx
  97. 54 0
      src/components/Dialogs/TransactionDialog/TransactionDialog.style.ts
  98. 109 0
      src/components/Dialogs/TransactionDialog/TransactionDialog.tsx
  99. 4 0
      src/components/Dialogs/TransactionDialog/index.ts
  100. 9 0
      src/components/Dialogs/index.ts

+ 8 - 2
.env

@@ -1,9 +1,15 @@
 # This file is commited. Do not store secrets here
 REACT_APP_ENV=staging
+#REACT_APP_ENV=development
 #REACT_APP_ENV=production
-REACT_APP_QUERY_NODE_URL=https://hydra-staging.joystream.app/server/graphql
+#REACT_APP_QUERY_NODE_URL=https://hydra-staging.joystream.app/server/graphql
 #REACT_APP_QUERY_NODE_URL=https://hydra.joystream.org/graphql
 REACT_APP_ORION_URL=https://orion-staging.joystream.app/graphql
 #REACT_APP_ORION_URL=https://orion.joystream.org/graphql
-REACT_APP_STORAGE_NODE_URL=https://staging-3.joystream.app/storage/asset/v0/
+#REACT_APP_STORAGE_NODE_URL=https://staging-3.joystream.app/storage/asset/v0/
 #REACT_APP_STORAGE_NODE_URL=https://rome-rpc-endpoint.joystream.org/asset/v0/
+
+REACT_APP_NODE_URL=wss://sumer-dev-2.joystream.app/rpc
+REACT_APP_FAUCET_URL=https://sumer-dev-2.joystream.app/members/register
+REACT_APP_QUERY_NODE_URL=https://sumer-dev-2.joystream.app/query/server/graphql
+REACT_APP_QUERY_NODE_SUBSCRIPTION_URL=wss://sumer-dev-2.joystream.app/query/server/graphql

+ 5 - 0
.eslintrc.js

@@ -6,6 +6,7 @@ module.exports = {
     jest: true,
   },
   extends: ['plugin:react-hooks/recommended', '@joystream/eslint-config'],
+  plugins: ['@emotion'],
   rules: {
     'react/prop-types': 'off',
     '@typescript-eslint/explicit-module-boundary-types': 'off',
@@ -27,5 +28,9 @@ module.exports = {
     '@typescript-eslint/naming-convention': ['off'],
     // remove once @joystream/eslint-config does not enforce an older version of @typescript-eslint
     '@typescript-eslint/no-unused-vars': ['off'],
+    // make sure we use the proper Emotion imports
+    '@emotion/pkg-renaming': 'error',
+    '@emotion/no-vanilla': 'error',
+    '@emotion/syntax-preference': [2, 'string'],
   },
 }

+ 1 - 1
.github/workflows/checks.yml

@@ -8,7 +8,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        node-version: [12.x]
+        node-version: [14.x]
       fail-fast: true
     steps:
       - uses: actions/checkout@v2

+ 1 - 0
.node-version

@@ -0,0 +1 @@
+14

+ 1 - 0
.prettierignore

@@ -4,3 +4,4 @@ dist/
 storybook-static/
 .coverage
 tsconfig.json
+public/mockServiceWorker.js

+ 7 - 0
.storybook/Welcome.stories.mdx

@@ -0,0 +1,7 @@
+import { Meta } from '@storybook/addon-docs/blocks'
+
+<Meta title="Welcome" />
+
+# Welcome to Joystream Storybook
+
+Possibly one day we'll add some more details here

+ 16 - 0
.storybook/WithValue.tsx

@@ -0,0 +1,16 @@
+import { action } from '@storybook/addon-actions'
+import * as React from 'react'
+
+export interface WithValueProps<T> {
+  initial: T
+  actionName?: string
+  children: (value: T, setValue: (value: T) => void) => React.ReactElement
+}
+
+export function WithValue<T>(props: WithValueProps<T>) {
+  const [value, setValue] = React.useState<T>(props.initial)
+  return props.children(value, (value) => {
+    action(props.actionName || 'setValue')(value)
+    setValue(value)
+  })
+}

+ 39 - 0
.storybook/main.js

@@ -0,0 +1,39 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+const { merge } = require('webpack-merge')
+const path = require('path')
+const fs = require('fs')
+const reactConfig = require('../config-overrides')
+
+// TODO: related to an issue with emotion and storybook, remove once resolved https://github.com/storybookjs/storybook/issues/7540
+function getPackageDir(filepath) {
+  let currDir = path.dirname(require.resolve(filepath))
+  while (true) {
+    if (fs.existsSync(path.join(currDir, 'package.json'))) {
+      return currDir
+    }
+    const { dir, root } = path.parse(currDir)
+    if (dir === root) {
+      throw new Error(`Could not find package.json in the parent directories starting from ${filepath}.`)
+    }
+    currDir = dir
+  }
+}
+
+module.exports = {
+  stories: ['./Welcome.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+  addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/preset-create-react-app'],
+
+  webpackFinal: async (config) => {
+    const craConfig = reactConfig.webpack(config)
+    return merge(craConfig, {
+      resolve: {
+        alias: {
+          '@emotion/core': getPackageDir('@emotion/react'),
+          '@emotion/styled': getPackageDir('@emotion/styled'),
+          'emotion-theming': getPackageDir('@emotion/react'),
+        },
+      },
+    })
+  },
+}

+ 0 - 0
src/shared/.storybook/manager-head.html → .storybook/manager-head.html


+ 6 - 0
.storybook/manager.js

@@ -0,0 +1,6 @@
+import addons from '@storybook/addons'
+import joystreamTheme from './theme'
+
+addons.setConfig({
+  theme: joystreamTheme,
+})

+ 0 - 0
src/shared/.storybook/preview-head.html → .storybook/preview-head.html


+ 53 - 0
.storybook/preview.jsx

@@ -0,0 +1,53 @@
+import React, { useRef } from 'react'
+import { css } from '@emotion/react'
+import { GlobalStyle } from '../src/shared/components'
+import useResizeObserver from 'use-resize-observer'
+
+const wrapperStyle = css`
+  padding: 10px;
+  height: calc(100vh - 20px);
+
+  & > * + * {
+    margin-left: 15px;
+  }
+`
+
+const sizeIndicatorStyle = css`
+  position: absolute;
+  font-size: 12px;
+  right: 4px;
+  top: 4px;
+`
+
+const StylesWrapperDecorator = (styleFn) => {
+  const ref = useRef(null)
+  const { width, height } = useResizeObserver({ ref })
+  return (
+    <div ref={ref} css={wrapperStyle}>
+      <div css={sizeIndicatorStyle}>
+        {width}px x {height}px
+      </div>
+      <GlobalStyle />
+      {styleFn()}
+    </div>
+  )
+}
+
+export const decorators = [StylesWrapperDecorator]
+
+export const parameters = {
+  actions: { argTypesRegex: '^on[A-Z].*' },
+  backgrounds: {
+    default: 'black',
+    values: [
+      {
+        name: 'black',
+        value: '#000000',
+      },
+      {
+        name: 'gray',
+        value: '#272D33',
+      },
+    ],
+  },
+}

+ 10 - 0
.storybook/theme.js

@@ -0,0 +1,10 @@
+import { create } from '@storybook/theming'
+
+export default create({
+  base: 'dark',
+
+  brandTitle: '@joystream/atlas',
+  brandUrl: 'https://joystream.org',
+  brandImage:
+    'https://raw.githubusercontent.com/Joystream/design/master/logo/logo/PNG/Logo-horisontal-basic-white-1x.png',
+})

+ 1 - 2
codegen.config.yml

@@ -7,9 +7,8 @@ documents:
 
 config:
   scalars:
-    Date: Date
+    DateTime: Date
   preResolveTypes: true # avoid using Pick
-  nonOptionalTypename: true
 
 generates:
   src/api/queries/__generated__/baseTypes.generated.ts:

+ 6 - 1
config-overrides.js

@@ -4,7 +4,7 @@ const { override, addBabelPreset, addBabelPlugin, addWebpackAlias, addWebpackMod
 
 module.exports = {
   webpack: override(
-    addBabelPlugin('babel-plugin-emotion'),
+    addBabelPlugin('@emotion/babel-plugin'),
     addBabelPreset('@emotion/babel-preset-css-prop'),
     addWebpackAlias({
       '@': path.resolve(__dirname, 'src/'),
@@ -13,6 +13,11 @@ module.exports = {
       test: /\.(graphql|gql)$/,
       exclude: /node_modules/,
       loader: 'graphql-tag/loader',
+    }),
+    addWebpackModuleRule({
+      test: /\.mjs$/,
+      include: /node_modules/,
+      type: 'javascript/auto',
     })
   ),
   paths: (paths) => {

+ 3 - 3
docs/overview.md

@@ -10,7 +10,7 @@ Atlas is a content consumption and publication app for the Joystream platform. T
 - Typescript
 - GraphQL (+ [Relay-style pagination](https://graphql.org/learn/pagination/))
 - ESLint, Prettier - for enforcing common code style
-- [Mirage JS](https://miragejs.com/) - for [client-side mocking](#client-side-data-mocking)
+- [Mock Service Worker](https://mswjs.io/) - for [client-side mocking](#client-side-data-mocking)
 - [Emotion](https://emotion.sh/) - for all the styling
 - [Apollo Client](https://www.apollographql.com/docs/react/) - for communications with GraphQL-based external services
 
@@ -98,11 +98,11 @@ During development, it may be useful to have an ability to use mocked data for a
 - testing specific kinds of data,
 - infrastructure is down.
 
-To allow that, Atlas uses Mirage JS for client-side mocking. The way this works, when the mocking module gets imported, Mirage creates a local server that intercept all the XHR network requests. Unless the request URL is explicitly marked as passthrough, Mirage will try to resolve that request using defined mocked handlers. We have handlers for most of the query node functionality that allow us to run Atlas independently of any infra with its own set of data.
+To allow that, Atlas uses MSW.js for client-side mocking. The way this works, when the mocking module gets imported, MSW creates a service worker that will intercept all the network requests and will try to resolve that request using defined mocked handlers. We have handlers for most of the query node functionality that allow us to run Atlas independently of any infra with its own set of data.
 
 #### Mocked dataset
 
-All the raw mocked data presented in Atlas can be found in `src/mocking/data/raw`. These JSON files are then parsed and returned by Mirage JS on GraphQL requests.
+All the raw mocked data presented in Atlas can be found in `src/mocking/data/raw`. These JSON files are then parsed and returned by MSW.js on GraphQL requests.
 
 The raw data can be generated by using included scripts:
 

+ 2 - 2
docs/styleguide.md

@@ -49,7 +49,7 @@ const FancySection: React.FC<FancySectionProps> = ({firstProps, secondProp, ...e
 
 ## Styling components
 
-We mainly use `@emotion/styled` to style components, if there is some style pattern or "snippet" that is repeated between multiple components, we like to use the `css` prop from `@emotion/core` to do so.
+We mainly use `@emotion/styled` to style components, if there is some style pattern or "snippet" that is repeated between multiple components, we like to use the `css` prop from `@emotion/react` to do so.
 When possible, we like to use variables from our theme, which is imported from `@/shared/theme`, theme components should be distructured before being used. When possible, *always* use values from the theme as it helps keep the app consistent.
 
 Here is a kitchen sink example:
@@ -57,7 +57,7 @@ Here is a kitchen sink example:
 ```javascript
 // YourComponent.style.tsx
 import styled from "@emotion/styled"
-import {css} from "@emotion/core"
+import {css} from "@emotion/react"
 
 import {colors, sizes} from "@/shared/theme"
 

+ 5 - 2
netlify-plugins/contextual-env/index.js

@@ -5,14 +5,17 @@ const { get } = require('lodash')
 
 const ENV_PRODUCTION = 'PRODUCTION'
 const ENV_STAGING = 'STAGING'
+const ENV_DEVELOPMENT = 'DEVELOPMENT'
 
 module.exports = {
   onPreBuild: async ({ inputs: { production_branch, app_env_prefix }, utils }) => {
-    const { CONTEXT, REVIEW_ID, REPOSITORY_URL } = process.env
+    const { CONTEXT, REVIEW_ID, REPOSITORY_URL, SITE_NAME } = process.env
 
     // === get env based on branch/PR ===
     let env = ENV_PRODUCTION
-    if (CONTEXT === 'branch-deploy') {
+    if (SITE_NAME === 'atlas-app-mocked') {
+      env = ENV_DEVELOPMENT
+    } else if (CONTEXT === 'branch-deploy') {
       env = ENV_STAGING
     } else if (CONTEXT === 'deploy-preview') {
       const productionPull = await isProductionPull({

+ 57 - 23
package.json

@@ -21,12 +21,15 @@
     "build": "react-app-rewired build",
     "eject": "react-app-rewired eject",
     "lint": "eslint --ext .js,.jsx,.ts,.tsx .",
-    "storybook": "start-storybook -p 6006 --quiet -c src/shared/.storybook",
-    "build-storybook": "build-storybook -c src/shared/.storybook",
+    "storybook": "start-storybook -p 6006 -s public",
+    "build-storybook": "build-storybook -s public",
     "mocking:videos": "node scripts/mocking/generateVideos.js",
     "mocking:videosMedia": "node scripts/mocking/generateVideosMedia.js",
+    "mocking:memberships": "node scripts/mocking/generateMemberships.js",
     "mocking:channels": "node scripts/mocking/generateChannels.js",
-    "queries:codegen": "graphql-codegen --config codegen.config.yml",
+    "codegen:graphql": "graphql-codegen --config codegen.config.yml",
+    "codegen:graphql-watch": "yarn codegen:graphql --watch",
+    "codegen:icons": "svgr --config-file svgr-icons.config.js -d src/shared/icons src/shared/icons/svgs",
     "postinstall": "patch-package"
   },
   "lint-staged": {
@@ -36,29 +39,30 @@
   },
   "dependencies": {
     "@apollo/client": "^3.3.0",
-    "@emotion/babel-preset-css-prop": "^10.0.27",
-    "@emotion/core": "^10.0.28",
+    "@emotion/babel-plugin": "~11.0.0",
+    "@emotion/babel-preset-css-prop": "~11.0.0",
+    "@emotion/eslint-plugin": "^11.0.0",
+    "@emotion/react": "~11.0.0",
+    "@emotion/styled": "~11.0.0",
     "@graphql-codegen/cli": "^1.20.1",
     "@graphql-codegen/near-operation-file-preset": "^1.17.13",
     "@graphql-codegen/typescript": "1.20.2",
     "@graphql-codegen/typescript-operations": "1.17.14",
     "@graphql-codegen/typescript-react-apollo": "2.2.1",
+    "@joystream/content-metadata-protobuf": "~1.1.0",
     "@joystream/eslint-config": "^1.0.0",
     "@joystream/prettier-config": "^1.0.0",
-    "@miragejs/graphql": "^0.1.4",
-    "@sentry/react": "^5.27.6",
-    "@storybook/addon-actions": "^5.3.17",
-    "@storybook/addon-docs": "^5.3.17",
-    "@storybook/addon-knobs": "^5.3.17",
-    "@storybook/addon-links": "^5.3.17",
-    "@storybook/addon-storysource": "^5.3.17",
-    "@storybook/addons": "^5.3.19",
-    "@storybook/preset-create-react-app": "^3.1.4",
-    "@storybook/react": "^5.3.17",
-    "@storybook/theming": "^5.3.19",
+    "@joystream/types": "~0.16.1",
+    "@loadable/component": "^5.14.1",
+    "@polkadot/extension-dapp": "~0.37.3-17",
+    "@sentry/integrations": "^6.3.5",
+    "@sentry/react": "^6.3.5",
+    "@tippyjs/react": "^4.2.5",
     "@types/body-scroll-lock": "^2.6.1",
+    "@types/cropperjs": "^1.3.0",
     "@types/faker": "^5.1.0",
     "@types/glider-js": "^1.7.3",
+    "@types/loadable__component": "^5.13.3",
     "@types/lodash": "^4.14.157",
     "@types/node": "^12.0.0",
     "@types/react": "^16.9.0",
@@ -66,12 +70,16 @@
     "@types/react-transition-group": "^4.4.0",
     "@types/video.js": "^7.3.10",
     "apollo": "^2.30.2",
-    "babel-plugin-emotion": "^10.0.33",
+    "awesome-debounce-promise": "^2.1.0",
+    "axios": "^0.21.1",
+    "bn.js": "~5.2.0",
     "body-scroll-lock": "^3.1.5",
+    "cropperjs": "^1.5.10",
     "csstype": "^3.0.0-beta.4",
     "customize-cra": "^1.0.0",
     "date-fns": "^2.15.0",
-    "emotion-normalize": "^10.1.0",
+    "downshift": "^6.1.0",
+    "emotion-normalize": "~11.0.0",
     "eslint-config-prettier": "^6.11.0",
     "eslint-plugin-jsx-a11y": "^6.2.3",
     "eslint-plugin-prettier": "^3.1.3",
@@ -83,27 +91,47 @@
     "graphql-tools": "^7.0.2",
     "history": "^5.0.0",
     "husky": "^4.2.5",
+    "ipfs-only-hash": "^2.1.0",
     "lint-staged": "^10.2.7",
     "lodash": "^4.17.19",
-    "miragejs": "^0.1.40",
+    "msw": "^0.27.0",
     "patch-package": "^6.2.2",
     "postinstall-postinstall": "^2.1.0",
     "prettier": "^2.0.5",
+    "rc-slider": "^9.7.1",
     "react": "^16.13.1",
     "react-app-rewired": "^2.1.6",
     "react-docgen-typescript-loader": "^3.7.1",
     "react-dom": "^16.13.1",
+    "react-dropzone": "^11.3.1",
+    "react-hook-form": "^6.15.3",
     "react-intersection-observer": "^8.31.0",
+    "react-number-format": "^4.4.4",
     "react-player": "^2.2.0",
     "react-router": "^6.0.0-beta.0",
     "react-router-dom": "^6.0.0-beta.0",
     "react-scripts": "4.0.1",
+    "react-spring": "^8.0.27",
     "react-transition-group": "^4.4.1",
-    "storybook-addon-jsx": "^7.1.15",
+    "react-use-measure": "^2.0.4",
+    "retry-axios": "^2.4.0",
+    "subscriptions-transport-ws": "^0.9.18",
     "ts-loader": "^6.2.1",
-    "typescript": "^3.8.3",
-    "use-resize-observer": "^6.1.0",
-    "video.js": "^7.8.3"
+    "typescript": "^4.2.3",
+    "use-resize-observer": "^7.0.0",
+    "video.js": "^7.8.3",
+    "webpack-merge": "^5.7.3"
+  },
+  "devDependencies": {
+    "@storybook/addon-actions": "^6.1.16",
+    "@storybook/addon-essentials": "^6.1.16",
+    "@storybook/addon-links": "^6.1.16",
+    "@storybook/addons": "^6.1.16",
+    "@storybook/node-logger": "^6.1.16",
+    "@storybook/preset-create-react-app": "^3.1.5",
+    "@storybook/react": "^6.1.16",
+    "@storybook/theming": "^6.1.16",
+    "@svgr/cli": "^5.5.0"
   },
   "browserslist": {
     "production": [
@@ -116,5 +144,11 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
+  },
+  "engines": {
+    "node": ">=14"
+  },
+  "msw": {
+    "workerDirectory": "public"
   }
 }

+ 320 - 0
public/mockServiceWorker.js

@@ -0,0 +1,320 @@
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+/* eslint-disable */
+/* tslint:disable */
+
+const INTEGRITY_CHECKSUM = 'f7d0ed371e596d181f62c6f68c4b7baf'
+const bypassHeaderName = 'x-msw-bypass'
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+  return self.skipWaiting()
+})
+
+self.addEventListener('activate', async function (event) {
+  return self.clients.claim()
+})
+
+self.addEventListener('message', async function (event) {
+  const clientId = event.source.id
+
+  if (!clientId || !self.clients) {
+    return
+  }
+
+  const client = await self.clients.get(clientId)
+
+  if (!client) {
+    return
+  }
+
+  const allClients = await self.clients.matchAll()
+
+  switch (event.data) {
+    case 'KEEPALIVE_REQUEST': {
+      sendToClient(client, {
+        type: 'KEEPALIVE_RESPONSE',
+      })
+      break
+    }
+
+    case 'INTEGRITY_CHECK_REQUEST': {
+      sendToClient(client, {
+        type: 'INTEGRITY_CHECK_RESPONSE',
+        payload: INTEGRITY_CHECKSUM,
+      })
+      break
+    }
+
+    case 'MOCK_ACTIVATE': {
+      activeClientIds.add(clientId)
+
+      sendToClient(client, {
+        type: 'MOCKING_ENABLED',
+        payload: true,
+      })
+      break
+    }
+
+    case 'MOCK_DEACTIVATE': {
+      activeClientIds.delete(clientId)
+      break
+    }
+
+    case 'CLIENT_CLOSED': {
+      activeClientIds.delete(clientId)
+
+      const remainingClients = allClients.filter((client) => {
+        return client.id !== clientId
+      })
+
+      // Unregister itself when there are no more clients
+      if (remainingClients.length === 0) {
+        self.registration.unregister()
+      }
+
+      break
+    }
+  }
+})
+
+// Resolve the "master" client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMasterClient(event) {
+  const client = await self.clients.get(event.clientId)
+
+  if (client.frameType === 'top-level') {
+    return client
+  }
+
+  const allClients = await self.clients.matchAll()
+
+  return allClients
+    .filter((client) => {
+      // Get only those clients that are currently visible.
+      return client.visibilityState === 'visible'
+    })
+    .find((client) => {
+      // Find the client ID that's recorded in the
+      // set of clients that have registered the worker.
+      return activeClientIds.has(client.id)
+    })
+}
+
+async function handleRequest(event, requestId) {
+  const client = await resolveMasterClient(event)
+  const response = await getResponse(event, client, requestId)
+
+  // Send back the response clone for the "response:*" life-cycle events.
+  // Ensure MSW is active and ready to handle the message, otherwise
+  // this message will pend indefinitely.
+  if (activeClientIds.has(client.id)) {
+    const clonedResponse = response.clone()
+
+    sendToClient(client, {
+      type: 'RESPONSE',
+      payload: {
+        requestId,
+        type: clonedResponse.type,
+        ok: clonedResponse.ok,
+        status: clonedResponse.status,
+        statusText: clonedResponse.statusText,
+        body: clonedResponse.body === null ? null : await clonedResponse.text(),
+        headers: serializeHeaders(clonedResponse.headers),
+        redirected: clonedResponse.redirected,
+      },
+    })
+  }
+
+  return response
+}
+
+async function getResponse(event, client, requestId) {
+  const { request } = event
+  const requestClone = request.clone()
+  const getOriginalResponse = () => fetch(requestClone)
+
+  // Bypass mocking when the request client is not active.
+  if (!client) {
+    return getOriginalResponse()
+  }
+
+  // Bypass initial page load requests (i.e. static assets).
+  // The absence of the immediate/parent client in the map of the active clients
+  // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+  // and is not ready to handle requests.
+  if (!activeClientIds.has(client.id)) {
+    return await getOriginalResponse()
+  }
+
+  // Bypass requests with the explicit bypass header
+  if (requestClone.headers.get(bypassHeaderName) === 'true') {
+    const cleanRequestHeaders = serializeHeaders(requestClone.headers)
+
+    // Remove the bypass header to comply with the CORS preflight check.
+    delete cleanRequestHeaders[bypassHeaderName]
+
+    const originalRequest = new Request(requestClone, {
+      headers: new Headers(cleanRequestHeaders),
+    })
+
+    return fetch(originalRequest)
+  }
+
+  // Send the request to the client-side MSW.
+  const reqHeaders = serializeHeaders(request.headers)
+  const body = await request.text()
+
+  const clientMessage = await sendToClient(client, {
+    type: 'REQUEST',
+    payload: {
+      id: requestId,
+      url: request.url,
+      method: request.method,
+      headers: reqHeaders,
+      cache: request.cache,
+      mode: request.mode,
+      credentials: request.credentials,
+      destination: request.destination,
+      integrity: request.integrity,
+      redirect: request.redirect,
+      referrer: request.referrer,
+      referrerPolicy: request.referrerPolicy,
+      body,
+      bodyUsed: request.bodyUsed,
+      keepalive: request.keepalive,
+    },
+  })
+
+  switch (clientMessage.type) {
+    case 'MOCK_SUCCESS': {
+      return delayPromise(
+        () => respondWithMock(clientMessage),
+        clientMessage.payload.delay,
+      )
+    }
+
+    case 'MOCK_NOT_FOUND': {
+      return getOriginalResponse()
+    }
+
+    case 'NETWORK_ERROR': {
+      const { name, message } = clientMessage.payload
+      const networkError = new Error(message)
+      networkError.name = name
+
+      // Rejecting a request Promise emulates a network error.
+      throw networkError
+    }
+
+    case 'INTERNAL_ERROR': {
+      const parsedBody = JSON.parse(clientMessage.payload.body)
+
+      console.error(
+        `\
+[MSW] Request handler function for "%s %s" has thrown the following exception:
+
+${parsedBody.errorType}: ${parsedBody.message}
+(see more detailed error stack trace in the mocked response body)
+
+This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error.
+If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
+`,
+        request.method,
+        request.url,
+      )
+
+      return respondWithMock(clientMessage)
+    }
+  }
+
+  return getOriginalResponse()
+}
+
+self.addEventListener('fetch', function (event) {
+  const { request } = event
+
+  // Bypass navigation requests.
+  if (request.mode === 'navigate') {
+    return
+  }
+
+  // Opening the DevTools triggers the "only-if-cached" request
+  // that cannot be handled by the worker. Bypass such requests.
+  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+    return
+  }
+
+  // Bypass all requests when there are no active clients.
+  // Prevents the self-unregistered worked from handling requests
+  // after it's been deleted (still remains active until the next reload).
+  if (activeClientIds.size === 0) {
+    return
+  }
+
+  const requestId = uuidv4()
+
+  return event.respondWith(
+    handleRequest(event, requestId).catch((error) => {
+      console.error(
+        '[MSW] Failed to mock a "%s" request to "%s": %s',
+        request.method,
+        request.url,
+        error,
+      )
+    }),
+  )
+})
+
+function serializeHeaders(headers) {
+  const reqHeaders = {}
+  headers.forEach((value, name) => {
+    reqHeaders[name] = reqHeaders[name]
+      ? [].concat(reqHeaders[name]).concat(value)
+      : value
+  })
+  return reqHeaders
+}
+
+function sendToClient(client, message) {
+  return new Promise((resolve, reject) => {
+    const channel = new MessageChannel()
+
+    channel.port1.onmessage = (event) => {
+      if (event.data && event.data.error) {
+        return reject(event.data.error)
+      }
+
+      resolve(event.data)
+    }
+
+    client.postMessage(JSON.stringify(message), [channel.port2])
+  })
+}
+
+function delayPromise(cb, duration) {
+  return new Promise((resolve) => {
+    setTimeout(() => resolve(cb()), duration)
+  })
+}
+
+function respondWithMock(clientMessage) {
+  return new Response(clientMessage.payload.body, {
+    ...clientMessage.payload,
+    headers: clientMessage.payload.headers,
+  })
+}
+
+function uuidv4() {
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+    const r = (Math.random() * 16) | 0
+    const v = c == 'x' ? r : (r & 0x3) | 0x8
+    return v.toString(16)
+  })
+}

+ 6 - 2
scripts/mocking/generateChannels.js

@@ -5,13 +5,17 @@ const { saveToFile, randomRange } = require('./utils')
 
 const OUTPUT_FILENAME = 'channels.json'
 const CHANNELS_COUNT = 10
+let nextChannelId = 0
 
 const generateChannel = () => {
   const handleWordsCount = randomRange(1, 4)
+  const descriptionWordsCount = randomRange(0, 30)
   return {
-    id: faker.random.uuid(),
-    handle: faker.lorem.words(handleWordsCount),
+    id: (nextChannelId++).toString(),
+    title: faker.lorem.words(handleWordsCount),
+    description: faker.lorem.words(descriptionWordsCount),
     follows: faker.random.number(150000),
+    createdAt: faker.date.past(10),
   }
 }
 

+ 25 - 0
scripts/mocking/generateMemberships.js

@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+const faker = require('faker')
+const { saveToFile, randomRange } = require('./utils')
+
+const OUTPUT_FILENAME = 'memberships.json'
+const MEMBERSHIPS_COUNT = 4
+let nextMemberId = 0
+
+const generateMembership = () => {
+  const handleWordsCount = randomRange(1, 4)
+  const aboutWordsCount = randomRange(0, 30)
+  return {
+    id: (nextMemberId++).toString(),
+    handle: faker.lorem.words(handleWordsCount),
+    about: faker.lorem.words(aboutWordsCount),
+  }
+}
+
+const main = async () => {
+  const memberships = Array.from({ length: MEMBERSHIPS_COUNT }, generateMembership)
+  await saveToFile(memberships, OUTPUT_FILENAME)
+}
+
+main()

+ 9 - 7
src/App.tsx

@@ -2,8 +2,8 @@ import React from 'react'
 import { ApolloProvider } from '@apollo/client'
 
 import { createApolloClient } from '@/api'
-import LayoutWithRouting from '@/views/LayoutWithRouting'
-import { PersonalDataProvider, OverlayManagerProvider } from '@/hooks'
+import { ConnectionStatusProvider, OverlayManagerProvider, SnackbarProvider } from '@/hooks'
+import MainLayout from './MainLayout'
 
 export default function App() {
   // create client on render so the mocking setup is done if needed
@@ -12,11 +12,13 @@ export default function App() {
 
   return (
     <ApolloProvider client={apolloClient}>
-      <PersonalDataProvider>
-        <OverlayManagerProvider>
-          <LayoutWithRouting />
-        </OverlayManagerProvider>
-      </PersonalDataProvider>
+      <SnackbarProvider>
+        <ConnectionStatusProvider>
+          <OverlayManagerProvider>
+            <MainLayout />
+          </OverlayManagerProvider>
+        </ConnectionStatusProvider>
+      </SnackbarProvider>
     </ApolloProvider>
   )
 }

+ 37 - 0
src/MainLayout.tsx

@@ -0,0 +1,37 @@
+import React from 'react'
+import { BrowserRouter, Routes, Route } from 'react-router-dom'
+import loadable from '@loadable/component'
+import { GlobalStyle } from '@/shared/components'
+import { BASE_PATHS } from '@/config/routes'
+import { ViewerLayout } from './views/viewer'
+import { LegalLayout } from './views/legal'
+import { PlaygroundLayout } from './views/playground'
+import { TopbarBase, StudioLoading } from '@/components'
+import { routingTransitions } from '@/styles/routingTransitions'
+
+const LoadableStudioLayout = loadable(() => import('./views/studio/StudioLayout'), {
+  fallback: (
+    <>
+      <TopbarBase variant="studio" />
+      <StudioLoading />
+    </>
+  ),
+})
+
+const MainLayout: React.FC = () => {
+  return (
+    <>
+      <GlobalStyle additionalStyles={[routingTransitions]} />
+      <BrowserRouter>
+        <Routes>
+          <Route path={BASE_PATHS.viewer + '/*'} element={<ViewerLayout />} />
+          <Route path={BASE_PATHS.legal + '/*'} element={<LegalLayout />} />
+          <Route path={BASE_PATHS.studio + '/*'} element={<LoadableStudioLayout />} />
+          <Route path={BASE_PATHS.playground + '/*'} element={<PlaygroundLayout />} />
+        </Routes>
+      </BrowserRouter>
+    </>
+  )
+}
+
+export default MainLayout

+ 135 - 40
src/api/client/cache.ts

@@ -1,51 +1,146 @@
 import { InMemoryCache } from '@apollo/client'
-import { relayStylePagination } from '@apollo/client/utilities'
+import { offsetLimitPagination, Reference, relayStylePagination, StoreObject } from '@apollo/client/utilities'
 import { parseISO } from 'date-fns'
+import {
+  AllChannelFieldsFragment,
+  AssetAvailability,
+  GetVideosQueryVariables,
+  Query,
+  VideoFieldsFragment,
+} from '../queries'
+import { FieldPolicy, FieldReadFunction } from '@apollo/client/cache/inmemory/policies'
+
+const getVideoKeyArgs = (args: Record<string, GetVideosQueryVariables['where']> | null) => {
+  // make sure queries asking for a specific category are separated in cache
+  const channelId = args?.where?.channelId_eq || ''
+  const categoryId = args?.where?.categoryId_eq || ''
+  const isPublic = args?.where?.isPublic_eq ?? ''
+  const channelIdIn = args?.where?.channelId_in ? JSON.stringify(args.where.channelId_in) : ''
+  const createdAtGte = args?.where?.createdAt_gte ? JSON.stringify(args.where.createdAt_gte) : ''
+
+  // only for counting videos in HomeView
+  if (args?.where?.channelId_in && !args?.first) {
+    return `${createdAtGte}:${channelIdIn}`
+  }
+
+  return `${channelId}:${categoryId}:${channelIdIn}:${createdAtGte}:${isPublic}`
+}
+
+const createDateHandler = () => ({
+  merge: (_: unknown, existingData: string | Date): Date => {
+    if (typeof existingData !== 'string') {
+      // TODO: investigate further
+      // rarely, for some reason the object that arrives here is already a date object
+      // in this case parsing attempt will cause an error
+      return existingData
+    }
+    return parseISO(existingData)
+  },
+})
+
+const createCachedUrlsHandler = () => ({
+  merge: (existing: string[] | undefined, incoming: string[]) => {
+    if (!existing || !existing.length) {
+      return incoming
+    }
+
+    if (!existing[0].startsWith('blob:')) {
+      // regular URL in cache, overwrite
+      return incoming
+    }
+
+    if (incoming && incoming.length && incoming[0].startsWith('blob:')) {
+      // incoming URL is cached asset, overwrite
+      return incoming
+    }
+
+    // currently using cached URL, keep it
+    return existing
+  },
+})
+
+const createCachedAvailabilityHandler = () => ({
+  merge: (existing: AssetAvailability | undefined, incoming: AssetAvailability) => {
+    if (incoming === AssetAvailability.Invalid) {
+      // if the incoming availability is invalid that means we deleted the asset so we shouldn't care about what's in cache
+      return incoming
+    }
+
+    if (existing === AssetAvailability.Accepted) {
+      // if the asset is already accepted, update most probably means that:
+      // fresh fetch is trying to overwrite local optimistically updated data
+      // in that case, let's keep it as Accepted to allow usage of cached blob URL
+      return existing
+    }
+    return incoming
+  },
+})
+
+type CachePolicyFields<T extends string> = Partial<Record<T, FieldPolicy | FieldReadFunction>>
+
+const queryCacheFields: CachePolicyFields<keyof Query> = {
+  channelsConnection: relayStylePagination(),
+  videosConnection: relayStylePagination(getVideoKeyArgs),
+  videos: {
+    ...offsetLimitPagination(getVideoKeyArgs),
+    read(existing, opts) {
+      const isPublic = opts.args?.where.isPublic_eq
+
+      const filteredExistingVideos = existing?.filter(
+        (v: StoreObject | Reference) => opts.readField('isPublic', v) === isPublic || isPublic === undefined
+      )
+      // Default to returning the entire cached list,
+      // if offset and limit are not provided.
+      const offset = opts.args?.offset ?? 0
+      const limit = opts.args?.limit ?? filteredExistingVideos?.length
+
+      return filteredExistingVideos?.slice(offset, offset + limit)
+    },
+  },
+  channelByUniqueInput: (existing, { toReference, args }) => {
+    return (
+      existing ||
+      toReference({
+        __typename: 'Channel',
+        id: args?.where.id,
+      })
+    )
+  },
+  videoByUniqueInput: (existing, { toReference, args }) => {
+    return (
+      existing ||
+      toReference({
+        __typename: 'Video',
+        id: args?.where.id,
+      })
+    )
+  },
+}
+
+const videoCacheFields: CachePolicyFields<keyof VideoFieldsFragment> = {
+  createdAt: createDateHandler(),
+  publishedBeforeJoystream: createDateHandler(),
+  thumbnailPhotoUrls: createCachedUrlsHandler(),
+  thumbnailPhotoAvailability: createCachedAvailabilityHandler(),
+}
+
+const channelCacheFields: CachePolicyFields<keyof AllChannelFieldsFragment> = {
+  avatarPhotoUrls: createCachedUrlsHandler(),
+  coverPhotoUrls: createCachedUrlsHandler(),
+  avatarPhotoAvailability: createCachedAvailabilityHandler(),
+  coverPhotoAvailability: createCachedAvailabilityHandler(),
+}
 
 const cache = new InMemoryCache({
   typePolicies: {
     Query: {
-      fields: {
-        channelsConnection: relayStylePagination(),
-        videosConnection: relayStylePagination((args) => {
-          // make sure queries asking for a specific category are separated in cache
-          const channelId = args?.where?.channelId_eq || ''
-          const categoryId = args?.where?.categoryId_eq || ''
-          const channelIdIn = args?.where?.channelId_in ? JSON.stringify(args.where.channelId_in) : ''
-          const createdAtGte = args?.where?.createdAt_gte ? JSON.stringify(args.where.createdAt_gte) : ''
-
-          // only for counting videos in HomeView
-          if (args?.where.channelId_in && !args?.first) {
-            return `${createdAtGte}:${channelIdIn}`
-          }
-
-          return `${channelId}:${categoryId}:${channelIdIn}:${createdAtGte}`
-        }),
-        channel(existing, { toReference, args }) {
-          return (
-            existing ||
-            toReference({
-              __typename: 'Channel',
-              id: args?.where.id,
-            })
-          )
-        },
-      },
+      fields: queryCacheFields,
     },
     Video: {
-      fields: {
-        createdAt: {
-          merge(_, createdAt: string | Date): Date {
-            if (typeof createdAt !== 'string') {
-              // TODO: investigate further
-              // rarely, for some reason the object that arrives here is already a date object
-              // in this case parsing attempt will cause an error
-              return createdAt
-            }
-            return parseISO(createdAt)
-          },
-        },
-      },
+      fields: videoCacheFields,
+    },
+    Channel: {
+      fields: channelCacheFields,
     },
   },
 })

+ 47 - 4
src/api/client/index.ts

@@ -1,7 +1,8 @@
-import { ApolloClient } from '@apollo/client'
+import { ApolloClient, split } from '@apollo/client'
+import { WebSocketLink } from '@apollo/client/link/ws'
 import { wrapSchema } from '@graphql-tools/wrap'
 import { mergeSchemas } from '@graphql-tools/merge'
-import { buildASTSchema } from 'graphql'
+import { buildASTSchema, GraphQLFieldResolver } from 'graphql'
 import { SchemaLink } from '@apollo/client/link/schema'
 
 import extendedQueryNodeSchema from '../schemas/extendedQueryNode.graphql'
@@ -10,6 +11,30 @@ import orionSchema from '../schemas/orion.graphql'
 import cache from './cache'
 import { queryNodeStitchingResolvers } from './resolvers'
 import { createExecutors } from '@/api/client/executors'
+import { delegateToSchema } from '@graphql-tools/delegate'
+import { CreateProxyingResolverFn } from '@graphql-tools/delegate/types'
+import { getMainDefinition } from '@apollo/client/utilities'
+import { QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL } from '@/config/urls'
+
+// we do this so that operationName is passed along with the queries
+// this is needed for our mocking backend to operate
+const createProxyingResolver: CreateProxyingResolverFn = ({
+  subschemaConfig,
+  operation,
+  transformedSchema,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+}): GraphQLFieldResolver<any, any> => {
+  return (_parent, _args, context, info) => {
+    return delegateToSchema({
+      schema: subschemaConfig,
+      operationName: info?.operation?.name?.value,
+      operation,
+      context,
+      info,
+      transformedSchema,
+    })
+  }
+}
 
 const createApolloClient = () => {
   const { queryNodeExecutor, orionExecutor } = createExecutors()
@@ -17,10 +42,13 @@ const createApolloClient = () => {
   const executableQueryNodeSchema = wrapSchema({
     schema: buildASTSchema(extendedQueryNodeSchema),
     executor: queryNodeExecutor,
+    createProxyingResolver,
   })
+
   const executableOrionSchema = wrapSchema({
     schema: buildASTSchema(orionSchema),
     executor: orionExecutor,
+    createProxyingResolver,
   })
 
   const mergedSchema = mergeSchemas({
@@ -28,9 +56,24 @@ const createApolloClient = () => {
     resolvers: queryNodeStitchingResolvers(executableQueryNodeSchema, executableOrionSchema),
   })
 
-  const link = new SchemaLink({ schema: mergedSchema })
+  const queryLink = new SchemaLink({ schema: mergedSchema })
+  const subscriptionLink = new WebSocketLink({
+    uri: QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL,
+    options: {
+      reconnect: true,
+    },
+  })
+
+  const splitLink = split(
+    ({ query }) => {
+      const definition = getMainDefinition(query)
+      return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
+    },
+    subscriptionLink,
+    queryLink
+  )
 
-  return new ApolloClient({ link, cache })
+  return new ApolloClient({ link: splitLink, cache })
 }
 
 export default createApolloClient

+ 11 - 3
src/api/client/resolvers.ts

@@ -20,6 +20,7 @@ const createResolverWithTransforms = (
     delegateToSchema({
       schema,
       operation: 'query',
+      operationName: info?.operation?.name?.value,
       fieldName,
       args,
       context,
@@ -34,12 +35,15 @@ export const queryNodeStitchingResolvers = (
 ): IResolvers => ({
   Query: {
     // video queries
-    video: createResolverWithTransforms(queryNodeSchema, 'video', [RemoveQueryNodeViewsField]),
+    videoByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'videoByUniqueInput', [
+      RemoveQueryNodeViewsField,
+    ]),
     videos: createResolverWithTransforms(queryNodeSchema, 'videos', [RemoveQueryNodeViewsField]),
     videosConnection: createResolverWithTransforms(queryNodeSchema, 'videosConnection', [RemoveQueryNodeViewsField]),
-    featuredVideos: createResolverWithTransforms(queryNodeSchema, 'featuredVideos', [RemoveQueryNodeViewsField]),
     // channel queries
-    channel: createResolverWithTransforms(queryNodeSchema, 'channel', [RemoveQueryNodeFollowsField]),
+    channelByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'channelByUniqueInput', [
+      RemoveQueryNodeFollowsField,
+    ]),
     channels: createResolverWithTransforms(queryNodeSchema, 'channels', [RemoveQueryNodeFollowsField]),
     channelsConnection: createResolverWithTransforms(queryNodeSchema, 'channelsConnection', [
       RemoveQueryNodeFollowsField,
@@ -59,6 +63,8 @@ export const queryNodeStitchingResolvers = (
         return await delegateToSchema({
           schema: orionSchema,
           operation: 'query',
+          // operationName has to be manually kept in sync with the query name used
+          operationName: 'GetVideoViews',
           fieldName: ORION_VIEWS_QUERY_NAME,
           args: {
             videoId: parent.id,
@@ -79,6 +85,8 @@ export const queryNodeStitchingResolvers = (
         return await delegateToSchema({
           schema: orionSchema,
           operation: 'query',
+          // operationName has to be manually kept in sync with the query name used
+          operationName: 'GetChannelFollows',
           fieldName: ORION_FOLLOWS_QUERY_NAME,
           args: {
             channelId: parent.id,

+ 5 - 5
src/api/hooks/categories.ts

@@ -1,11 +1,11 @@
-import { useGetCategoriesQuery, GetCategoriesQueryVariables, GetCategoriesQuery } from '@/api/queries'
+import { useGetVideoCategoriesQuery, GetVideoCategoriesQuery, GetVideoCategoriesQueryVariables } from '@/api/queries'
 import { QueryHookOptions } from '@apollo/client'
 
-type Opts = QueryHookOptions<GetCategoriesQuery>
-const useCategories = (variables?: GetCategoriesQueryVariables, opts?: Opts) => {
-  const { data, ...rest } = useGetCategoriesQuery({ ...opts, variables })
+type Opts = QueryHookOptions<GetVideoCategoriesQuery>
+const useCategories = (variables?: GetVideoCategoriesQueryVariables, opts?: Opts) => {
+  const { data, ...rest } = useGetVideoCategoriesQuery({ ...opts, variables })
   return {
-    categories: data?.categories,
+    categories: data?.videoCategories,
     ...rest,
   }
 }

+ 26 - 17
src/api/hooks/channel.ts

@@ -1,28 +1,29 @@
-import { QueryHookOptions, MutationHookOptions } from '@apollo/client'
+import { MutationHookOptions, QueryHookOptions } from '@apollo/client'
 import {
-  useGetChannelQuery,
-  useGetChannelVideoCountQuery,
-  useGetChannelsQuery,
-  useFollowChannelMutation,
-  useUnfollowChannelMutation,
-  GetChannelQuery,
-  GetChannelVideoCountQuery,
+  AssetAvailability,
   FollowChannelMutation,
-  UnfollowChannelMutation,
+  GetBasicChannelQuery,
+  GetChannelQuery,
   GetChannelsQuery,
   GetChannelsQueryVariables,
-  GetBasicChannelQuery,
+  GetVideoCountQuery,
+  UnfollowChannelMutation,
+  useFollowChannelMutation,
   useGetBasicChannelQuery,
+  useGetChannelQuery,
+  useGetChannelsQuery,
+  useGetVideoCountQuery,
+  useUnfollowChannelMutation,
 } from '@/api/queries'
 
 type BasicChannelOpts = QueryHookOptions<GetBasicChannelQuery>
 export const useBasicChannel = (id: string, opts?: BasicChannelOpts) => {
   const { data, ...rest } = useGetBasicChannelQuery({
     ...opts,
-    variables: { id },
+    variables: { where: { id } },
   })
   return {
-    channel: data?.channel,
+    channel: data?.channelByUniqueInput,
     ...rest,
   }
 }
@@ -31,19 +32,27 @@ type ChannelOpts = QueryHookOptions<GetChannelQuery>
 export const useChannel = (id: string, opts?: ChannelOpts) => {
   const { data, ...rest } = useGetChannelQuery({
     ...opts,
-    variables: { id },
+    variables: { where: { id } },
   })
   return {
-    channel: data?.channel,
+    channel: data?.channelByUniqueInput,
     ...rest,
   }
 }
 
-type VideoCountOpts = QueryHookOptions<GetChannelVideoCountQuery>
+type VideoCountOpts = QueryHookOptions<GetVideoCountQuery>
 export const useChannelVideoCount = (channelId: string, opts?: VideoCountOpts) => {
-  const { data, ...rest } = useGetChannelVideoCountQuery({
+  const { data, ...rest } = useGetVideoCountQuery({
     ...opts,
-    variables: { channelId },
+    variables: {
+      where: {
+        channelId_eq: channelId,
+        thumbnailPhotoAvailability_eq: AssetAvailability.Accepted,
+        mediaAvailability_eq: AssetAvailability.Accepted,
+        isPublic_eq: true,
+        isCensored_eq: false,
+      },
+    },
   })
   return {
     videoCount: data?.videosConnection.totalCount,

+ 3 - 23
src/api/hooks/coverVideo.ts

@@ -1,40 +1,20 @@
 import { CoverVideo, AllChannelFieldsFragment } from '@/api/queries'
 import { MockVideo } from '@/mocking/data/mockVideos'
 
-import {
-  mockCoverVideo,
-  mockCoverVideoChannel,
-  mockCoverVideoInfo,
-  mockCoverVideoMedia,
-} from '@/mocking/data/mockCoverVideo'
+import { mockCoverVideo } from '@/mocking/data/mockCoverVideo'
 
 type UseCoverVideo = () => {
   data: {
     __typename: 'CoverVideo'
     coverDescription: CoverVideo['coverDescription']
-    coverCutMedia: CoverVideo['coverCutMedia']
+    coverCutMediaMetadata: CoverVideo['coverCutMediaMetadata']
     video: MockVideo & { channel: AllChannelFieldsFragment }
   }
 }
 
 const useCoverVideo: UseCoverVideo = () => {
   return {
-    data: {
-      __typename: 'CoverVideo',
-      coverDescription: mockCoverVideoInfo.coverDescription,
-      coverCutMedia: mockCoverVideoInfo.coverCutMedia,
-      video: {
-        ...mockCoverVideo,
-        createdAt: new Date(),
-        category: {
-          __typename: 'Category',
-          id: 'random_category',
-        },
-        duration: mockCoverVideoMedia.duration,
-        media: mockCoverVideoMedia,
-        channel: mockCoverVideoChannel,
-      },
-    },
+    data: mockCoverVideo,
   }
 }
 

+ 0 - 13
src/api/hooks/featuredVideos.ts

@@ -1,13 +0,0 @@
-import { GetFeaturedVideosQuery, GetFeaturedVideosQueryVariables, useGetFeaturedVideosQuery } from '@/api/queries'
-import { QueryHookOptions } from '@apollo/client'
-
-type Opts = QueryHookOptions<GetFeaturedVideosQuery>
-const useFeaturedVideos = (variables?: GetFeaturedVideosQueryVariables, opts?: Opts) => {
-  const { data, ...rest } = useGetFeaturedVideosQuery({ ...opts, variables })
-  return {
-    featuredVideos: data?.featuredVideos,
-    ...rest,
-  }
-}
-
-export default useFeaturedVideos

+ 4 - 2
src/api/hooks/index.ts

@@ -1,10 +1,12 @@
 import useCoverVideo from './coverVideo'
-import useFeaturedVideos from './featuredVideos'
 import useCategories from './categories'
 import useSearch from './search'
 import useVideosConnection from './videosConnection'
 
-export { useCoverVideo, useVideosConnection, useFeaturedVideos, useCategories, useSearch }
+export { useCoverVideo, useVideosConnection, useCategories, useSearch }
 
 export * from './channel'
 export * from './video'
+export * from './membership'
+export * from './queryNode'
+export * from './workers'

+ 32 - 0
src/api/hooks/membership.ts

@@ -0,0 +1,32 @@
+import { QueryHookOptions } from '@apollo/client'
+import {
+  GetMembershipQuery,
+  useGetMembershipQuery,
+  useGetMembershipsQuery,
+  GetMembershipQueryVariables,
+  GetMembershipsQueryVariables,
+} from '@/api/queries'
+
+type MembershipOpts = QueryHookOptions<GetMembershipQuery>
+
+export const useMembership = (variables: GetMembershipQueryVariables, opts?: MembershipOpts) => {
+  const { data, ...rest } = useGetMembershipQuery({
+    ...opts,
+    variables,
+  })
+  return {
+    membership: data?.membershipByUniqueInput,
+    ...rest,
+  }
+}
+
+export const useMemberships = (variables: GetMembershipsQueryVariables, opts?: MembershipOpts) => {
+  const { data, ...rest } = useGetMembershipsQuery({
+    ...opts,
+    variables,
+  })
+  return {
+    memberships: data?.memberships,
+    ...rest,
+  }
+}

+ 15 - 0
src/api/hooks/queryNode.ts

@@ -0,0 +1,15 @@
+import { SubscriptionHookOptions } from '@apollo/client'
+import {
+  GetQueryNodeStateSubscription,
+  GetQueryNodeStateSubscriptionVariables,
+  useGetQueryNodeStateSubscription,
+} from '@/api/queries/__generated__/queryNode.generated'
+
+type QueryNodeStateOpts = SubscriptionHookOptions<GetQueryNodeStateSubscription, GetQueryNodeStateSubscriptionVariables>
+export const useQueryNodeStateSubscription = (opts?: QueryNodeStateOpts) => {
+  const { data, ...rest } = useGetQueryNodeStateSubscription(opts)
+  return {
+    queryNodeState: data?.stateSubscription,
+    ...rest,
+  }
+}

+ 14 - 4
src/api/hooks/video.ts

@@ -6,6 +6,7 @@ import {
   GetVideosQuery,
   GetVideosQueryVariables,
   useGetVideosQuery,
+  useGetVideoCountQuery,
 } from '@/api/queries'
 import { QueryHookOptions, MutationHookOptions } from '@apollo/client'
 
@@ -13,20 +14,29 @@ type VideoOpts = QueryHookOptions<GetVideoQuery>
 export const useVideo = (id: string, opts?: VideoOpts) => {
   const { data, ...queryRest } = useGetVideoQuery({
     ...opts,
-    variables: { id },
+    variables: { where: { id } },
   })
-
   return {
-    video: data?.video,
+    video: data?.videoByUniqueInput,
     ...queryRest,
   }
 }
 
 type VideosOpts = QueryHookOptions<GetVideosQuery>
 export const useVideos = (variables?: GetVideosQueryVariables, opts?: VideosOpts) => {
-  const { data, ...rest } = useGetVideosQuery({ ...opts, variables })
+  const { data, loading: videosLoading, ...rest } = useGetVideosQuery({ ...opts, variables })
+  // Only way to get the video count for pagination as of now
+  const { data: connectionData, loading: countLoading, refetch: refetchCount } = useGetVideoCountQuery({
+    ...opts,
+    variables: {
+      where: variables?.where,
+    },
+  })
   return {
     videos: data?.videos,
+    loading: videosLoading || countLoading,
+    totalCount: connectionData?.videosConnection.totalCount,
+    refetchCount,
     ...rest,
   }
 }

+ 60 - 0
src/api/hooks/workers.ts

@@ -0,0 +1,60 @@
+import { getRandomIntInclusive } from '@/utils/number'
+import { QueryHookOptions } from '@apollo/client'
+import {
+  GetWorkerQuery,
+  GetWorkersQuery,
+  GetWorkersQueryVariables,
+  useGetWorkerQuery,
+  useGetWorkersQuery,
+} from '@/api/queries/__generated__/workers.generated'
+import { WorkerType } from '@/api/queries'
+import { useCallback } from 'react'
+
+type WorkerOpts = QueryHookOptions<GetWorkerQuery>
+export const useWorker = (id: string, opts?: WorkerOpts) => {
+  const { data, ...queryRest } = useGetWorkerQuery({
+    ...opts,
+    variables: { where: { id } },
+  })
+  return {
+    storageProvider: data?.workerByUniqueInput,
+    ...queryRest,
+  }
+}
+
+type WorkersOpts = QueryHookOptions<GetWorkersQuery>
+export const useStorageProviders = (variables: GetWorkersQueryVariables, opts?: WorkersOpts) => {
+  const { data, loading, ...rest } = useGetWorkersQuery({
+    ...opts,
+    variables: {
+      ...variables,
+      where: {
+        metadata_contains: 'http',
+        isActive_eq: true,
+        type_eq: WorkerType.Storage,
+        ...variables.where,
+      },
+    },
+  })
+  return {
+    storageProviders: data?.workers,
+    loading,
+    ...rest,
+  }
+}
+
+export const useRandomStorageProviderUrl = () => {
+  const { storageProviders, loading } = useStorageProviders({ limit: 100 }, { fetchPolicy: 'network-only' })
+
+  const getRandomStorageProviderUrl = useCallback(() => {
+    if (storageProviders?.length && !loading) {
+      const randomStorageIdx = getRandomIntInclusive(0, storageProviders.length - 1)
+      return storageProviders[randomStorageIdx].metadata
+    } else if (!loading) {
+      console.error('No active storage provider available')
+    }
+    return null
+  }, [loading, storageProviders])
+
+  return { getRandomStorageProviderUrl }
+}

+ 205 - 134
src/api/queries/__generated__/baseTypes.generated.ts

@@ -9,149 +9,122 @@ export type Scalars = {
   Boolean: boolean
   Int: number
   Float: number
-  Date: Date
+  DateTime: Date
 }
 
-export enum Language {
-  Chinese = 'Chinese',
-  English = 'English',
-  Arabic = 'Arabic',
-  Portugese = 'Portugese',
-  French = 'French',
+export type Language = {
+  __typename?: 'Language'
+  iso: Scalars['String']
 }
 
-export type Member = {
-  __typename: 'Member'
+export type VideoCategory = {
+  __typename?: 'VideoCategory'
   id: Scalars['ID']
-  handle: Scalars['String']
-}
-
-export type Channel = {
-  __typename: 'Channel'
-  id: Scalars['ID']
-  createdAt: Scalars['Date']
-  handle: Scalars['String']
-  description: Scalars['String']
-  coverPhotoUrl?: Maybe<Scalars['String']>
-  avatarPhotoUrl?: Maybe<Scalars['String']>
-  owner: Member
-  isPublic: Scalars['Boolean']
-  isCurated: Scalars['Boolean']
-  language?: Maybe<Language>
-  videos: Array<Video>
-  follows?: Maybe<Scalars['Int']>
+  name?: Maybe<Scalars['String']>
+  videos?: Maybe<Array<Video>>
 }
 
-export type Category = {
-  __typename: 'Category'
+export type License = {
+  __typename?: 'License'
   id: Scalars['ID']
-  name: Scalars['String']
-  videos?: Maybe<Array<Video>>
+  code?: Maybe<Scalars['Int']>
+  attribution?: Maybe<Scalars['String']>
+  customText?: Maybe<Scalars['String']>
 }
 
-export type JoystreamMediaLocation = {
-  __typename: 'JoystreamMediaLocation'
-  dataObjectId: Scalars['String']
+export type PageInfo = {
+  __typename?: 'PageInfo'
+  hasNextPage: Scalars['Boolean']
+  hasPreviousPage: Scalars['Boolean']
+  startCursor?: Maybe<Scalars['String']>
+  endCursor?: Maybe<Scalars['String']>
 }
 
-export type HttpMediaLocation = {
-  __typename: 'HttpMediaLocation'
-  url: Scalars['String']
+export enum AssetAvailability {
+  Accepted = 'ACCEPTED',
+  Pending = 'PENDING',
+  Invalid = 'INVALID',
 }
 
-export type MediaLocation = JoystreamMediaLocation | HttpMediaLocation
-
-export type KnownLicense = {
-  __typename: 'KnownLicense'
-  code: Scalars['String']
-  name?: Maybe<Scalars['String']>
-  description?: Maybe<Scalars['String']>
-  url?: Maybe<Scalars['String']>
+export enum LiaisonJudgement {
+  Pending = 'PENDING',
+  Accepted = 'ACCEPTED',
+  Rejected = 'REJECTED',
 }
 
-export type UserDefinedLicense = {
-  __typename: 'UserDefinedLicense'
-  content: Scalars['String']
+export enum WorkerType {
+  Gateway = 'GATEWAY',
+  Storage = 'STORAGE',
 }
 
-export type License = UserDefinedLicense | KnownLicense
-
-export type LicenseEntity = {
-  __typename: 'LicenseEntity'
+export type Worker = {
+  __typename?: 'Worker'
   id: Scalars['ID']
-  type: License
-  attribution?: Maybe<Scalars['String']>
-  videoLicense?: Maybe<Array<Video>>
+  workerId: Scalars['String']
+  type: WorkerType
+  metadata?: Maybe<Scalars['String']>
+  isActive: Scalars['Boolean']
 }
 
-export type VideoMedia = {
-  __typename: 'VideoMedia'
+export type DataObject = {
+  __typename?: 'DataObject'
   id: Scalars['ID']
-  pixelWidth: Scalars['Int']
-  pixelHeight: Scalars['Int']
-  size?: Maybe<Scalars['Float']>
-  location: MediaLocation
+  createdAt: Scalars['DateTime']
+  size: Scalars['Float']
+  liaison?: Maybe<Worker>
+  liaisonJudgement: LiaisonJudgement
+  ipfsContentId: Scalars['String']
+  joystreamContentId: Scalars['String']
 }
 
-export type Video = {
-  __typename: 'Video'
+export type Membership = {
+  __typename?: 'Membership'
   id: Scalars['ID']
-  channel: Channel
-  category: Category
-  title: Scalars['String']
-  description: Scalars['String']
-  views?: Maybe<Scalars['Int']>
-  duration: Scalars['Int']
-  skippableIntroDuration?: Maybe<Scalars['Int']>
-  thumbnailUrl: Scalars['String']
-  Language?: Maybe<Language>
-  media: VideoMedia
-  hasMarketing?: Maybe<Scalars['Boolean']>
-  createdAt: Scalars['Date']
-  createdAtBlockHeight: Scalars['Float']
-  publishedBeforeJoystream?: Maybe<Scalars['String']>
-  isPublic: Scalars['Boolean']
-  isCurated: Scalars['Boolean']
-  isExplicit: Scalars['Boolean']
-  license: LicenseEntity
+  handle: Scalars['String']
+  avatarUri?: Maybe<Scalars['String']>
+  controllerAccount: Scalars['String']
+  about?: Maybe<Scalars['String']>
+  channels: Array<Channel>
 }
 
-export type CoverVideo = {
-  __typename: 'CoverVideo'
-  id: Scalars['ID']
-  video: Video
-  coverDescription: Scalars['String']
-  coverCutMedia: VideoMedia
+export type MembershipWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>
+  handle?: Maybe<Scalars['String']>
 }
 
-export type FeaturedVideo = {
-  __typename: 'FeaturedVideo'
-  id: Scalars['ID']
-  video: Video
+export type MembershipWhereInput = {
+  controllerAccount_eq?: Maybe<Scalars['ID']>
+  controllerAccount_in?: Maybe<Array<Scalars['ID']>>
 }
 
-export type SearchResult = Video | Channel
-
-export type SearchFtsOutput = {
-  __typename: 'SearchFTSOutput'
-  item: SearchResult
-  rank: Scalars['Float']
-  isTypeOf: Scalars['String']
-  highlight: Scalars['String']
-}
-
-export type PageInfo = {
-  __typename: 'PageInfo'
-  hasNextPage: Scalars['Boolean']
-  hasPreviousPage: Scalars['Boolean']
-  startCursor?: Maybe<Scalars['String']>
-  endCursor?: Maybe<Scalars['String']>
+export type Channel = {
+  __typename?: 'Channel'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  ownerMember?: Maybe<Membership>
+  videos: Array<Video>
+  isCensored: Scalars['Boolean']
+  title?: Maybe<Scalars['String']>
+  description?: Maybe<Scalars['String']>
+  isPublic?: Maybe<Scalars['Boolean']>
+  language?: Maybe<Language>
+  coverPhotoDataObject?: Maybe<DataObject>
+  coverPhotoUrls: Array<Scalars['String']>
+  coverPhotoAvailability: AssetAvailability
+  avatarPhotoDataObject?: Maybe<DataObject>
+  avatarPhotoUrls: Array<Scalars['String']>
+  avatarPhotoAvailability: AssetAvailability
+  follows?: Maybe<Scalars['Int']>
 }
 
 export type ChannelWhereInput = {
+  id_in?: Maybe<Array<Scalars['ID']>>
+  ownerMemberId_eq?: Maybe<Scalars['ID']>
   isCurated_eq?: Maybe<Scalars['Boolean']>
   isPublic_eq?: Maybe<Scalars['Boolean']>
-  id_in?: Maybe<Array<Scalars['ID']>>
+  isCensored_eq?: Maybe<Scalars['Boolean']>
+  coverPhotoAvailability_eq?: Maybe<AssetAvailability>
+  avatarPhotoAvailability_eq?: Maybe<AssetAvailability>
 }
 
 export type ChannelWhereUniqueInput = {
@@ -164,29 +137,68 @@ export enum ChannelOrderByInput {
 }
 
 export type ChannelEdge = {
-  __typename: 'ChannelEdge'
+  __typename?: 'ChannelEdge'
   node: Channel
   cursor: Scalars['String']
 }
 
 export type ChannelConnection = {
-  __typename: 'ChannelConnection'
+  __typename?: 'ChannelConnection'
   edges: Array<ChannelEdge>
   pageInfo: PageInfo
   totalCount: Scalars['Int']
 }
 
-export type CategoryWhereUniqueInput = {
+export type VideoMediaMetadata = {
+  __typename?: 'VideoMediaMetadata'
+  id: Scalars['ID']
+  pixelWidth?: Maybe<Scalars['Int']>
+  pixelHeight?: Maybe<Scalars['Int']>
+  size?: Maybe<Scalars['Int']>
+}
+
+export type Video = {
+  __typename?: 'Video'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  channel: Channel
+  isCensored: Scalars['Boolean']
+  isFeatured: Scalars['Boolean']
+  publishedBeforeJoystream?: Maybe<Scalars['DateTime']>
+  title?: Maybe<Scalars['String']>
+  description?: Maybe<Scalars['String']>
+  category?: Maybe<VideoCategory>
+  language?: Maybe<Language>
+  hasMarketing?: Maybe<Scalars['Boolean']>
+  isExplicit?: Maybe<Scalars['Boolean']>
+  isPublic?: Maybe<Scalars['Boolean']>
+  license?: Maybe<License>
+  thumbnailPhotoDataObject?: Maybe<DataObject>
+  thumbnailPhotoUrls: Array<Scalars['String']>
+  thumbnailPhotoAvailability: AssetAvailability
+  mediaDataObject?: Maybe<DataObject>
+  mediaUrls: Array<Scalars['String']>
+  mediaAvailability: AssetAvailability
+  mediaMetadata: VideoMediaMetadata
+  duration?: Maybe<Scalars['Int']>
+  skippableIntroDuration?: Maybe<Scalars['Int']>
+  views?: Maybe<Scalars['Int']>
+}
+
+export type VideoCategoryWhereUniqueInput = {
   id: Scalars['ID']
 }
 
 export type VideoWhereInput = {
   categoryId_eq?: Maybe<Scalars['ID']>
-  channelId_in?: Maybe<Array<Maybe<Scalars['ID']>>>
+  channelId_in?: Maybe<Array<Scalars['ID']>>
   channelId_eq?: Maybe<Scalars['ID']>
-  createdAt_gte?: Maybe<Scalars['Date']>
-  isCurated_eq?: Maybe<Scalars['Boolean']>
+  thumbnailPhotoAvailability_eq?: Maybe<AssetAvailability>
+  mediaAvailability_eq?: Maybe<AssetAvailability>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  isFeatured_eq?: Maybe<Scalars['Boolean']>
   isPublic_eq?: Maybe<Scalars['Boolean']>
+  isCensored_eq?: Maybe<Scalars['Boolean']>
   id_in?: Maybe<Array<Scalars['ID']>>
 }
 
@@ -200,34 +212,71 @@ export enum VideoOrderByInput {
 }
 
 export type VideoEdge = {
-  __typename: 'VideoEdge'
+  __typename?: 'VideoEdge'
   node: Video
   cursor: Scalars['String']
 }
 
 export type VideoConnection = {
-  __typename: 'VideoConnection'
+  __typename?: 'VideoConnection'
   edges: Array<VideoEdge>
   pageInfo: PageInfo
   totalCount: Scalars['Int']
 }
 
-export enum FeaturedVideoOrderByInput {
+export type WorkerWhereInput = {
+  metadata_contains?: Maybe<Scalars['String']>
+  isActive_eq?: Maybe<Scalars['Boolean']>
+  type_eq?: Maybe<WorkerType>
+}
+
+export type WorkerWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
+export enum WorkerOrderByInput {
   CreatedAtAsc = 'createdAt_ASC',
   CreatedAtDesc = 'createdAt_DESC',
 }
 
+export type CoverVideo = {
+  __typename?: 'CoverVideo'
+  id: Scalars['ID']
+  video: Video
+  coverDescription: Scalars['String']
+  coverCutMediaMetadata: VideoMediaMetadata
+  coverCutMediaDataObject?: Maybe<DataObject>
+  coverCutMediaAvailability: AssetAvailability
+  coverCutMediaUrl?: Maybe<Scalars['String']>
+}
+
+export type SearchResult = Video | Channel
+
+export type SearchFtsOutput = {
+  __typename?: 'SearchFTSOutput'
+  item: SearchResult
+  rank: Scalars['Float']
+  isTypeOf: Scalars['String']
+  highlight: Scalars['String']
+}
+
+export type ProcessorState = {
+  __typename?: 'ProcessorState'
+  lastCompleteBlock: Scalars['Float']
+  lastProcessedEvent: Scalars['String']
+  indexerHead: Scalars['Float']
+  chainHead: Scalars['Float']
+}
+
 export type Query = {
-  __typename: 'Query'
+  __typename?: 'Query'
   /** Get follows counts for a list of channels */
   batchedChannelFollows: Array<Maybe<ChannelFollowsInfo>>
   /** Get views counts for a list of channels */
   batchedChannelsViews: Array<Maybe<EntityViewsInfo>>
   /** Get views counts for a list of videos */
   batchedVideoViews: Array<Maybe<EntityViewsInfo>>
-  categories: Array<Category>
-  category?: Maybe<Category>
-  channel?: Maybe<Channel>
+  channelByUniqueInput?: Maybe<Channel>
   /** Get follows count for a single channel */
   channelFollows?: Maybe<ChannelFollowsInfo>
   /** Get views count for a single channel */
@@ -235,13 +284,17 @@ export type Query = {
   channels: Array<Channel>
   channelsConnection: ChannelConnection
   coverVideo: CoverVideo
-  featuredVideos: Array<FeaturedVideo>
+  membershipByUniqueInput?: Maybe<Membership>
+  memberships: Array<Membership>
   search: Array<SearchFtsOutput>
-  video?: Maybe<Video>
+  videoByUniqueInput?: Maybe<Video>
+  videoCategories: Array<VideoCategory>
   /** Get views count for a single video */
   videoViews?: Maybe<EntityViewsInfo>
-  videos: Array<Video>
+  videos?: Maybe<Array<Video>>
   videosConnection: VideoConnection
+  workerByUniqueInput?: Maybe<Worker>
+  workers?: Maybe<Array<Worker>>
 }
 
 export type QueryBatchedChannelFollowsArgs = {
@@ -256,11 +309,7 @@ export type QueryBatchedVideoViewsArgs = {
   videoIdList: Array<Scalars['ID']>
 }
 
-export type QueryCategoryArgs = {
-  where: CategoryWhereUniqueInput
-}
-
-export type QueryChannelArgs = {
+export type QueryChannelByUniqueInputArgs = {
   where: ChannelWhereUniqueInput
 }
 
@@ -283,8 +332,12 @@ export type QueryChannelsConnectionArgs = {
   orderBy?: Maybe<ChannelOrderByInput>
 }
 
-export type QueryFeaturedVideosArgs = {
-  orderBy?: Maybe<FeaturedVideoOrderByInput>
+export type QueryMembershipByUniqueInputArgs = {
+  where: MembershipWhereUniqueInput
+}
+
+export type QueryMembershipsArgs = {
+  where: MembershipWhereInput
 }
 
 export type QuerySearchArgs = {
@@ -292,7 +345,7 @@ export type QuerySearchArgs = {
   text: Scalars['String']
 }
 
-export type QueryVideoArgs = {
+export type QueryVideoByUniqueInputArgs = {
   where: VideoWhereUniqueInput
 }
 
@@ -301,7 +354,10 @@ export type QueryVideoViewsArgs = {
 }
 
 export type QueryVideosArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
   where?: Maybe<VideoWhereInput>
+  orderBy?: Maybe<VideoOrderByInput>
 }
 
 export type QueryVideosConnectionArgs = {
@@ -311,20 +367,35 @@ export type QueryVideosConnectionArgs = {
   orderBy?: Maybe<VideoOrderByInput>
 }
 
+export type QueryWorkerByUniqueInputArgs = {
+  where: WorkerWhereUniqueInput
+}
+
+export type QueryWorkersArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<WorkerWhereInput>
+}
+
+export type Subscription = {
+  __typename?: 'Subscription'
+  stateSubscription: ProcessorState
+}
+
 export type ChannelFollowsInfo = {
-  __typename: 'ChannelFollowsInfo'
+  __typename?: 'ChannelFollowsInfo'
   follows: Scalars['Int']
   id: Scalars['ID']
 }
 
 export type EntityViewsInfo = {
-  __typename: 'EntityViewsInfo'
+  __typename?: 'EntityViewsInfo'
   id: Scalars['ID']
   views: Scalars['Int']
 }
 
 export type Mutation = {
-  __typename: 'Mutation'
+  __typename?: 'Mutation'
   /** Add a single view to the target video's count */
   addVideoView: EntityViewsInfo
   /** Add a single follow to the target channel */

+ 34 - 25
src/api/queries/__generated__/categories.generated.tsx

@@ -2,55 +2,64 @@ import * as Types from './baseTypes.generated'
 
 import { gql } from '@apollo/client'
 import * as Apollo from '@apollo/client'
-export type CategoryFieldsFragment = { __typename: 'Category'; id: string; name: string }
+export type VideoCategoryFieldsFragment = { __typename?: 'VideoCategory'; id: string; name?: Types.Maybe<string> }
 
-export type GetCategoriesQueryVariables = Types.Exact<{ [key: string]: never }>
+export type GetVideoCategoriesQueryVariables = Types.Exact<{ [key: string]: never }>
 
-export type GetCategoriesQuery = {
-  __typename: 'Query'
-  categories: Array<{ __typename: 'Category' } & CategoryFieldsFragment>
+export type GetVideoCategoriesQuery = {
+  __typename?: 'Query'
+  videoCategories: Array<{ __typename?: 'VideoCategory' } & VideoCategoryFieldsFragment>
 }
 
-export const CategoryFieldsFragmentDoc = gql`
-  fragment CategoryFields on Category {
+export const VideoCategoryFieldsFragmentDoc = gql`
+  fragment VideoCategoryFields on VideoCategory {
     id
     name
   }
 `
-export const GetCategoriesDocument = gql`
-  query GetCategories {
-    categories {
-      ...CategoryFields
+export const GetVideoCategoriesDocument = gql`
+  query GetVideoCategories {
+    videoCategories {
+      ...VideoCategoryFields
     }
   }
-  ${CategoryFieldsFragmentDoc}
+  ${VideoCategoryFieldsFragmentDoc}
 `
 
 /**
- * __useGetCategoriesQuery__
+ * __useGetVideoCategoriesQuery__
  *
- * To run a query within a React component, call `useGetCategoriesQuery` and pass it any options that fit your needs.
- * When your component renders, `useGetCategoriesQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * To run a query within a React component, call `useGetVideoCategoriesQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetVideoCategoriesQuery` returns an object from Apollo Client that contains loading, error, and data properties
  * you can use to render your UI.
  *
  * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
  *
  * @example
- * const { data, loading, error } = useGetCategoriesQuery({
+ * const { data, loading, error } = useGetVideoCategoriesQuery({
  *   variables: {
  *   },
  * });
  */
-export function useGetCategoriesQuery(
-  baseOptions?: Apollo.QueryHookOptions<GetCategoriesQuery, GetCategoriesQueryVariables>
+export function useGetVideoCategoriesQuery(
+  baseOptions?: Apollo.QueryHookOptions<GetVideoCategoriesQuery, GetVideoCategoriesQueryVariables>
 ) {
-  return Apollo.useQuery<GetCategoriesQuery, GetCategoriesQueryVariables>(GetCategoriesDocument, baseOptions)
+  return Apollo.useQuery<GetVideoCategoriesQuery, GetVideoCategoriesQueryVariables>(
+    GetVideoCategoriesDocument,
+    baseOptions
+  )
 }
-export function useGetCategoriesLazyQuery(
-  baseOptions?: Apollo.LazyQueryHookOptions<GetCategoriesQuery, GetCategoriesQueryVariables>
+export function useGetVideoCategoriesLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetVideoCategoriesQuery, GetVideoCategoriesQueryVariables>
 ) {
-  return Apollo.useLazyQuery<GetCategoriesQuery, GetCategoriesQueryVariables>(GetCategoriesDocument, baseOptions)
+  return Apollo.useLazyQuery<GetVideoCategoriesQuery, GetVideoCategoriesQueryVariables>(
+    GetVideoCategoriesDocument,
+    baseOptions
+  )
 }
-export type GetCategoriesQueryHookResult = ReturnType<typeof useGetCategoriesQuery>
-export type GetCategoriesLazyQueryHookResult = ReturnType<typeof useGetCategoriesLazyQuery>
-export type GetCategoriesQueryResult = Apollo.QueryResult<GetCategoriesQuery, GetCategoriesQueryVariables>
+export type GetVideoCategoriesQueryHookResult = ReturnType<typeof useGetVideoCategoriesQuery>
+export type GetVideoCategoriesLazyQueryHookResult = ReturnType<typeof useGetVideoCategoriesLazyQuery>
+export type GetVideoCategoriesQueryResult = Apollo.QueryResult<
+  GetVideoCategoriesQuery,
+  GetVideoCategoriesQueryVariables
+>

+ 148 - 77
src/api/queries/__generated__/channels.generated.tsx

@@ -1,85 +1,103 @@
 import * as Types from './baseTypes.generated'
 
+import { DataObjectFieldsFragment, DataObjectFieldsFragmentDoc } from './shared.generated'
 import { gql } from '@apollo/client'
+
 import * as Apollo from '@apollo/client'
 export type BasicChannelFieldsFragment = {
-  __typename: 'Channel'
+  __typename?: 'Channel'
   id: string
-  handle: string
-  avatarPhotoUrl?: Types.Maybe<string>
+  title?: Types.Maybe<string>
+  createdAt: Date
+  avatarPhotoUrls: Array<string>
+  avatarPhotoAvailability: Types.AssetAvailability
+  avatarPhotoDataObject?: Types.Maybe<{ __typename?: 'DataObject' } & DataObjectFieldsFragment>
 }
 
 export type AllChannelFieldsFragment = {
-  __typename: 'Channel'
-  id: string
-  handle: string
-  avatarPhotoUrl?: Types.Maybe<string>
-  coverPhotoUrl?: Types.Maybe<string>
+  __typename?: 'Channel'
+  description?: Types.Maybe<string>
   follows?: Types.Maybe<number>
-}
+  isPublic?: Types.Maybe<boolean>
+  isCensored: boolean
+  coverPhotoUrls: Array<string>
+  coverPhotoAvailability: Types.AssetAvailability
+  language?: Types.Maybe<{ __typename?: 'Language'; iso: string }>
+  coverPhotoDataObject?: Types.Maybe<{ __typename?: 'DataObject' } & DataObjectFieldsFragment>
+} & BasicChannelFieldsFragment
 
 export type GetBasicChannelQueryVariables = Types.Exact<{
-  id: Types.Scalars['ID']
+  where: Types.ChannelWhereUniqueInput
 }>
 
 export type GetBasicChannelQuery = {
-  __typename: 'Query'
-  channel?: Types.Maybe<{ __typename: 'Channel' } & BasicChannelFieldsFragment>
+  __typename?: 'Query'
+  channelByUniqueInput?: Types.Maybe<{ __typename?: 'Channel' } & BasicChannelFieldsFragment>
 }
 
 export type GetChannelQueryVariables = Types.Exact<{
-  id: Types.Scalars['ID']
+  where: Types.ChannelWhereUniqueInput
 }>
 
 export type GetChannelQuery = {
-  __typename: 'Query'
-  channel?: Types.Maybe<{ __typename: 'Channel' } & AllChannelFieldsFragment>
+  __typename?: 'Query'
+  channelByUniqueInput?: Types.Maybe<{ __typename?: 'Channel' } & AllChannelFieldsFragment>
 }
 
-export type GetChannelVideoCountQueryVariables = Types.Exact<{
-  channelId: Types.Scalars['ID']
+export type GetVideoCountQueryVariables = Types.Exact<{
+  where?: Types.Maybe<Types.VideoWhereInput>
 }>
 
-export type GetChannelVideoCountQuery = {
-  __typename: 'Query'
-  videosConnection: { __typename: 'VideoConnection'; totalCount: number }
+export type GetVideoCountQuery = {
+  __typename?: 'Query'
+  videosConnection: { __typename?: 'VideoConnection'; totalCount: number }
 }
 
 export type GetChannelsQueryVariables = Types.Exact<{
-  id_in: Array<Types.Scalars['ID']> | Types.Scalars['ID']
+  where?: Types.Maybe<Types.ChannelWhereInput>
 }>
 
 export type GetChannelsQuery = {
-  __typename: 'Query'
-  channels: Array<{ __typename: 'Channel' } & AllChannelFieldsFragment>
+  __typename?: 'Query'
+  channels: Array<{ __typename?: 'Channel' } & AllChannelFieldsFragment>
 }
 
 export type GetChannelsConnectionQueryVariables = Types.Exact<{
   first?: Types.Maybe<Types.Scalars['Int']>
   after?: Types.Maybe<Types.Scalars['String']>
+  where?: Types.Maybe<Types.ChannelWhereInput>
 }>
 
 export type GetChannelsConnectionQuery = {
-  __typename: 'Query'
+  __typename?: 'Query'
   channelsConnection: {
-    __typename: 'ChannelConnection'
+    __typename?: 'ChannelConnection'
     totalCount: number
     edges: Array<{
-      __typename: 'ChannelEdge'
+      __typename?: 'ChannelEdge'
       cursor: string
-      node: { __typename: 'Channel'; createdAt: Date } & AllChannelFieldsFragment
+      node: { __typename?: 'Channel' } & AllChannelFieldsFragment
     }>
-    pageInfo: { __typename: 'PageInfo'; hasNextPage: boolean; endCursor?: Types.Maybe<string> }
+    pageInfo: { __typename?: 'PageInfo'; hasNextPage: boolean; endCursor?: Types.Maybe<string> }
   }
 }
 
+export type GetChannelFollowsQueryVariables = Types.Exact<{
+  channelId: Types.Scalars['ID']
+}>
+
+export type GetChannelFollowsQuery = {
+  __typename?: 'Query'
+  channelFollows?: Types.Maybe<{ __typename?: 'ChannelFollowsInfo'; id: string; follows: number }>
+}
+
 export type FollowChannelMutationVariables = Types.Exact<{
   channelId: Types.Scalars['ID']
 }>
 
 export type FollowChannelMutation = {
-  __typename: 'Mutation'
-  followChannel: { __typename: 'ChannelFollowsInfo'; id: string; follows: number }
+  __typename?: 'Mutation'
+  followChannel: { __typename?: 'ChannelFollowsInfo'; id: string; follows: number }
 }
 
 export type UnfollowChannelMutationVariables = Types.Exact<{
@@ -87,29 +105,45 @@ export type UnfollowChannelMutationVariables = Types.Exact<{
 }>
 
 export type UnfollowChannelMutation = {
-  __typename: 'Mutation'
-  unfollowChannel: { __typename: 'ChannelFollowsInfo'; id: string; follows: number }
+  __typename?: 'Mutation'
+  unfollowChannel: { __typename?: 'ChannelFollowsInfo'; id: string; follows: number }
 }
 
 export const BasicChannelFieldsFragmentDoc = gql`
   fragment BasicChannelFields on Channel {
     id
-    handle
-    avatarPhotoUrl
+    title
+    createdAt
+    avatarPhotoUrls
+    avatarPhotoAvailability
+    avatarPhotoDataObject {
+      ...DataObjectFields
+    }
   }
+  ${DataObjectFieldsFragmentDoc}
 `
 export const AllChannelFieldsFragmentDoc = gql`
   fragment AllChannelFields on Channel {
-    id
-    handle
-    avatarPhotoUrl
-    coverPhotoUrl
+    ...BasicChannelFields
+    description
     follows
+    isPublic
+    isCensored
+    language {
+      iso
+    }
+    coverPhotoUrls
+    coverPhotoAvailability
+    coverPhotoDataObject {
+      ...DataObjectFields
+    }
   }
+  ${BasicChannelFieldsFragmentDoc}
+  ${DataObjectFieldsFragmentDoc}
 `
 export const GetBasicChannelDocument = gql`
-  query GetBasicChannel($id: ID!) {
-    channel(where: { id: $id }) {
+  query GetBasicChannel($where: ChannelWhereUniqueInput!) {
+    channelByUniqueInput(where: $where) {
       ...BasicChannelFields
     }
   }
@@ -128,7 +162,7 @@ export const GetBasicChannelDocument = gql`
  * @example
  * const { data, loading, error } = useGetBasicChannelQuery({
  *   variables: {
- *      id: // value for 'id'
+ *      where: // value for 'where'
  *   },
  * });
  */
@@ -146,8 +180,8 @@ export type GetBasicChannelQueryHookResult = ReturnType<typeof useGetBasicChanne
 export type GetBasicChannelLazyQueryHookResult = ReturnType<typeof useGetBasicChannelLazyQuery>
 export type GetBasicChannelQueryResult = Apollo.QueryResult<GetBasicChannelQuery, GetBasicChannelQueryVariables>
 export const GetChannelDocument = gql`
-  query GetChannel($id: ID!) {
-    channel(where: { id: $id }) {
+  query GetChannel($where: ChannelWhereUniqueInput!) {
+    channelByUniqueInput(where: $where) {
       ...AllChannelFields
     }
   }
@@ -166,7 +200,7 @@ export const GetChannelDocument = gql`
  * @example
  * const { data, loading, error } = useGetChannelQuery({
  *   variables: {
- *      id: // value for 'id'
+ *      where: // value for 'where'
  *   },
  * });
  */
@@ -181,55 +215,46 @@ export function useGetChannelLazyQuery(
 export type GetChannelQueryHookResult = ReturnType<typeof useGetChannelQuery>
 export type GetChannelLazyQueryHookResult = ReturnType<typeof useGetChannelLazyQuery>
 export type GetChannelQueryResult = Apollo.QueryResult<GetChannelQuery, GetChannelQueryVariables>
-export const GetChannelVideoCountDocument = gql`
-  query GetChannelVideoCount($channelId: ID!) {
-    videosConnection(first: 0, where: { channelId_eq: $channelId }) {
+export const GetVideoCountDocument = gql`
+  query GetVideoCount($where: VideoWhereInput) {
+    videosConnection(first: 0, where: $where) {
       totalCount
     }
   }
 `
 
 /**
- * __useGetChannelVideoCountQuery__
+ * __useGetVideoCountQuery__
  *
- * To run a query within a React component, call `useGetChannelVideoCountQuery` and pass it any options that fit your needs.
- * When your component renders, `useGetChannelVideoCountQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * To run a query within a React component, call `useGetVideoCountQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetVideoCountQuery` returns an object from Apollo Client that contains loading, error, and data properties
  * you can use to render your UI.
  *
  * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
  *
  * @example
- * const { data, loading, error } = useGetChannelVideoCountQuery({
+ * const { data, loading, error } = useGetVideoCountQuery({
  *   variables: {
- *      channelId: // value for 'channelId'
+ *      where: // value for 'where'
  *   },
  * });
  */
-export function useGetChannelVideoCountQuery(
-  baseOptions: Apollo.QueryHookOptions<GetChannelVideoCountQuery, GetChannelVideoCountQueryVariables>
+export function useGetVideoCountQuery(
+  baseOptions?: Apollo.QueryHookOptions<GetVideoCountQuery, GetVideoCountQueryVariables>
 ) {
-  return Apollo.useQuery<GetChannelVideoCountQuery, GetChannelVideoCountQueryVariables>(
-    GetChannelVideoCountDocument,
-    baseOptions
-  )
+  return Apollo.useQuery<GetVideoCountQuery, GetVideoCountQueryVariables>(GetVideoCountDocument, baseOptions)
 }
-export function useGetChannelVideoCountLazyQuery(
-  baseOptions?: Apollo.LazyQueryHookOptions<GetChannelVideoCountQuery, GetChannelVideoCountQueryVariables>
+export function useGetVideoCountLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetVideoCountQuery, GetVideoCountQueryVariables>
 ) {
-  return Apollo.useLazyQuery<GetChannelVideoCountQuery, GetChannelVideoCountQueryVariables>(
-    GetChannelVideoCountDocument,
-    baseOptions
-  )
+  return Apollo.useLazyQuery<GetVideoCountQuery, GetVideoCountQueryVariables>(GetVideoCountDocument, baseOptions)
 }
-export type GetChannelVideoCountQueryHookResult = ReturnType<typeof useGetChannelVideoCountQuery>
-export type GetChannelVideoCountLazyQueryHookResult = ReturnType<typeof useGetChannelVideoCountLazyQuery>
-export type GetChannelVideoCountQueryResult = Apollo.QueryResult<
-  GetChannelVideoCountQuery,
-  GetChannelVideoCountQueryVariables
->
+export type GetVideoCountQueryHookResult = ReturnType<typeof useGetVideoCountQuery>
+export type GetVideoCountLazyQueryHookResult = ReturnType<typeof useGetVideoCountLazyQuery>
+export type GetVideoCountQueryResult = Apollo.QueryResult<GetVideoCountQuery, GetVideoCountQueryVariables>
 export const GetChannelsDocument = gql`
-  query GetChannels($id_in: [ID!]!) {
-    channels(where: { id_in: $id_in }) {
+  query GetChannels($where: ChannelWhereInput) {
+    channels(where: $where) {
       ...AllChannelFields
     }
   }
@@ -248,11 +273,13 @@ export const GetChannelsDocument = gql`
  * @example
  * const { data, loading, error } = useGetChannelsQuery({
  *   variables: {
- *      id_in: // value for 'id_in'
+ *      where: // value for 'where'
  *   },
  * });
  */
-export function useGetChannelsQuery(baseOptions: Apollo.QueryHookOptions<GetChannelsQuery, GetChannelsQueryVariables>) {
+export function useGetChannelsQuery(
+  baseOptions?: Apollo.QueryHookOptions<GetChannelsQuery, GetChannelsQueryVariables>
+) {
   return Apollo.useQuery<GetChannelsQuery, GetChannelsQueryVariables>(GetChannelsDocument, baseOptions)
 }
 export function useGetChannelsLazyQuery(
@@ -264,13 +291,12 @@ export type GetChannelsQueryHookResult = ReturnType<typeof useGetChannelsQuery>
 export type GetChannelsLazyQueryHookResult = ReturnType<typeof useGetChannelsLazyQuery>
 export type GetChannelsQueryResult = Apollo.QueryResult<GetChannelsQuery, GetChannelsQueryVariables>
 export const GetChannelsConnectionDocument = gql`
-  query GetChannelsConnection($first: Int, $after: String) {
-    channelsConnection(first: $first, after: $after, orderBy: createdAt_DESC) {
+  query GetChannelsConnection($first: Int, $after: String, $where: ChannelWhereInput) {
+    channelsConnection(first: $first, after: $after, where: $where, orderBy: createdAt_DESC) {
       edges {
         cursor
         node {
           ...AllChannelFields
-          createdAt
         }
       }
       pageInfo {
@@ -297,6 +323,7 @@ export const GetChannelsConnectionDocument = gql`
  *   variables: {
  *      first: // value for 'first'
  *      after: // value for 'after'
+ *      where: // value for 'where'
  *   },
  * });
  */
@@ -322,6 +349,50 @@ export type GetChannelsConnectionQueryResult = Apollo.QueryResult<
   GetChannelsConnectionQuery,
   GetChannelsConnectionQueryVariables
 >
+export const GetChannelFollowsDocument = gql`
+  query GetChannelFollows($channelId: ID!) {
+    channelFollows(channelId: $channelId) {
+      id
+      follows
+    }
+  }
+`
+
+/**
+ * __useGetChannelFollowsQuery__
+ *
+ * To run a query within a React component, call `useGetChannelFollowsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetChannelFollowsQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetChannelFollowsQuery({
+ *   variables: {
+ *      channelId: // value for 'channelId'
+ *   },
+ * });
+ */
+export function useGetChannelFollowsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>
+) {
+  return Apollo.useQuery<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>(
+    GetChannelFollowsDocument,
+    baseOptions
+  )
+}
+export function useGetChannelFollowsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>(
+    GetChannelFollowsDocument,
+    baseOptions
+  )
+}
+export type GetChannelFollowsQueryHookResult = ReturnType<typeof useGetChannelFollowsQuery>
+export type GetChannelFollowsLazyQueryHookResult = ReturnType<typeof useGetChannelFollowsLazyQuery>
+export type GetChannelFollowsQueryResult = Apollo.QueryResult<GetChannelFollowsQuery, GetChannelFollowsQueryVariables>
 export const FollowChannelDocument = gql`
   mutation FollowChannel($channelId: ID!) {
     followChannel(channelId: $channelId) {

+ 123 - 0
src/api/queries/__generated__/memberships.generated.tsx

@@ -0,0 +1,123 @@
+import * as Types from './baseTypes.generated'
+
+import { BasicChannelFieldsFragment, BasicChannelFieldsFragmentDoc } from './channels.generated'
+import { gql } from '@apollo/client'
+
+import * as Apollo from '@apollo/client'
+export type BasicMembershipFieldsFragment = {
+  __typename?: 'Membership'
+  id: string
+  handle: string
+  avatarUri?: Types.Maybe<string>
+  about?: Types.Maybe<string>
+  controllerAccount: string
+  channels: Array<{ __typename?: 'Channel' } & BasicChannelFieldsFragment>
+}
+
+export type GetMembershipQueryVariables = Types.Exact<{
+  where: Types.MembershipWhereUniqueInput
+}>
+
+export type GetMembershipQuery = {
+  __typename?: 'Query'
+  membershipByUniqueInput?: Types.Maybe<{ __typename?: 'Membership' } & BasicMembershipFieldsFragment>
+}
+
+export type GetMembershipsQueryVariables = Types.Exact<{
+  where: Types.MembershipWhereInput
+}>
+
+export type GetMembershipsQuery = {
+  __typename?: 'Query'
+  memberships: Array<{ __typename?: 'Membership' } & BasicMembershipFieldsFragment>
+}
+
+export const BasicMembershipFieldsFragmentDoc = gql`
+  fragment BasicMembershipFields on Membership {
+    id
+    handle
+    avatarUri
+    about
+    controllerAccount
+    channels {
+      ...BasicChannelFields
+    }
+  }
+  ${BasicChannelFieldsFragmentDoc}
+`
+export const GetMembershipDocument = gql`
+  query GetMembership($where: MembershipWhereUniqueInput!) {
+    membershipByUniqueInput(where: $where) {
+      ...BasicMembershipFields
+    }
+  }
+  ${BasicMembershipFieldsFragmentDoc}
+`
+
+/**
+ * __useGetMembershipQuery__
+ *
+ * To run a query within a React component, call `useGetMembershipQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetMembershipQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetMembershipQuery({
+ *   variables: {
+ *      where: // value for 'where'
+ *   },
+ * });
+ */
+export function useGetMembershipQuery(
+  baseOptions: Apollo.QueryHookOptions<GetMembershipQuery, GetMembershipQueryVariables>
+) {
+  return Apollo.useQuery<GetMembershipQuery, GetMembershipQueryVariables>(GetMembershipDocument, baseOptions)
+}
+export function useGetMembershipLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetMembershipQuery, GetMembershipQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetMembershipQuery, GetMembershipQueryVariables>(GetMembershipDocument, baseOptions)
+}
+export type GetMembershipQueryHookResult = ReturnType<typeof useGetMembershipQuery>
+export type GetMembershipLazyQueryHookResult = ReturnType<typeof useGetMembershipLazyQuery>
+export type GetMembershipQueryResult = Apollo.QueryResult<GetMembershipQuery, GetMembershipQueryVariables>
+export const GetMembershipsDocument = gql`
+  query GetMemberships($where: MembershipWhereInput!) {
+    memberships(where: $where) {
+      ...BasicMembershipFields
+    }
+  }
+  ${BasicMembershipFieldsFragmentDoc}
+`
+
+/**
+ * __useGetMembershipsQuery__
+ *
+ * To run a query within a React component, call `useGetMembershipsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetMembershipsQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetMembershipsQuery({
+ *   variables: {
+ *      where: // value for 'where'
+ *   },
+ * });
+ */
+export function useGetMembershipsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetMembershipsQuery, GetMembershipsQueryVariables>
+) {
+  return Apollo.useQuery<GetMembershipsQuery, GetMembershipsQueryVariables>(GetMembershipsDocument, baseOptions)
+}
+export function useGetMembershipsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetMembershipsQuery, GetMembershipsQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetMembershipsQuery, GetMembershipsQueryVariables>(GetMembershipsDocument, baseOptions)
+}
+export type GetMembershipsQueryHookResult = ReturnType<typeof useGetMembershipsQuery>
+export type GetMembershipsLazyQueryHookResult = ReturnType<typeof useGetMembershipsLazyQuery>
+export type GetMembershipsQueryResult = Apollo.QueryResult<GetMembershipsQuery, GetMembershipsQueryVariables>

+ 53 - 0
src/api/queries/__generated__/queryNode.generated.tsx

@@ -0,0 +1,53 @@
+import * as Types from './baseTypes.generated'
+
+import { gql } from '@apollo/client'
+import * as Apollo from '@apollo/client'
+export type GetQueryNodeStateSubscriptionVariables = Types.Exact<{ [key: string]: never }>
+
+export type GetQueryNodeStateSubscription = {
+  __typename?: 'Subscription'
+  stateSubscription: {
+    __typename?: 'ProcessorState'
+    chainHead: number
+    indexerHead: number
+    lastCompleteBlock: number
+    lastProcessedEvent: string
+  }
+}
+
+export const GetQueryNodeStateDocument = gql`
+  subscription GetQueryNodeState {
+    stateSubscription {
+      chainHead
+      indexerHead
+      lastCompleteBlock
+      lastProcessedEvent
+    }
+  }
+`
+
+/**
+ * __useGetQueryNodeStateSubscription__
+ *
+ * To run a query within a React component, call `useGetQueryNodeStateSubscription` and pass it any options that fit your needs.
+ * When your component renders, `useGetQueryNodeStateSubscription` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetQueryNodeStateSubscription({
+ *   variables: {
+ *   },
+ * });
+ */
+export function useGetQueryNodeStateSubscription(
+  baseOptions?: Apollo.SubscriptionHookOptions<GetQueryNodeStateSubscription, GetQueryNodeStateSubscriptionVariables>
+) {
+  return Apollo.useSubscription<GetQueryNodeStateSubscription, GetQueryNodeStateSubscriptionVariables>(
+    GetQueryNodeStateDocument,
+    baseOptions
+  )
+}
+export type GetQueryNodeStateSubscriptionHookResult = ReturnType<typeof useGetQueryNodeStateSubscription>
+export type GetQueryNodeStateSubscriptionResult = Apollo.SubscriptionResult<GetQueryNodeStateSubscription>

+ 3 - 5
src/api/queries/__generated__/search.generated.tsx

@@ -10,11 +10,10 @@ export type SearchQueryVariables = Types.Exact<{
 }>
 
 export type SearchQuery = {
-  __typename: 'Query'
+  __typename?: 'Query'
   search: Array<{
-    __typename: 'SearchFTSOutput'
-    rank: number
-    item: ({ __typename: 'Video' } & VideoFieldsFragment) | ({ __typename: 'Channel' } & BasicChannelFieldsFragment)
+    __typename?: 'SearchFTSOutput'
+    item: ({ __typename?: 'Video' } & VideoFieldsFragment) | ({ __typename?: 'Channel' } & BasicChannelFieldsFragment)
   }>
 }
 
@@ -29,7 +28,6 @@ export const SearchDocument = gql`
           ...BasicChannelFields
         }
       }
-      rank
     }
   }
   ${VideoFieldsFragmentDoc}

+ 30 - 0
src/api/queries/__generated__/shared.generated.tsx

@@ -0,0 +1,30 @@
+import * as Types from './baseTypes.generated'
+
+import { BasicWorkerFieldsFragment, BasicWorkerFieldsFragmentDoc } from './workers.generated'
+import { gql } from '@apollo/client'
+
+export type DataObjectFieldsFragment = {
+  __typename?: 'DataObject'
+  id: string
+  createdAt: Date
+  size: number
+  liaisonJudgement: Types.LiaisonJudgement
+  ipfsContentId: string
+  joystreamContentId: string
+  liaison?: Types.Maybe<{ __typename?: 'Worker' } & BasicWorkerFieldsFragment>
+}
+
+export const DataObjectFieldsFragmentDoc = gql`
+  fragment DataObjectFields on DataObject {
+    id
+    createdAt
+    size
+    liaison {
+      ...BasicWorkerFields
+    }
+    liaisonJudgement
+    ipfsContentId
+    joystreamContentId
+  }
+  ${BasicWorkerFieldsFragmentDoc}
+`

+ 142 - 152
src/api/queries/__generated__/videos.generated.tsx

@@ -1,137 +1,134 @@
 import * as Types from './baseTypes.generated'
 
+import { DataObjectFieldsFragment, DataObjectFieldsFragmentDoc } from './shared.generated'
 import { BasicChannelFieldsFragment, BasicChannelFieldsFragmentDoc } from './channels.generated'
 import { gql } from '@apollo/client'
 
 import * as Apollo from '@apollo/client'
-export type VideoMediaFieldsFragment = {
-  __typename: 'VideoMedia'
+export type VideoMediaMetadataFieldsFragment = {
+  __typename?: 'VideoMediaMetadata'
   id: string
-  pixelHeight: number
-  pixelWidth: number
-  location:
-    | { __typename: 'JoystreamMediaLocation'; dataObjectId: string }
-    | { __typename: 'HttpMediaLocation'; url: string }
+  pixelHeight?: Types.Maybe<number>
+  pixelWidth?: Types.Maybe<number>
 }
 
 export type LicenseFieldsFragment = {
-  __typename: 'LicenseEntity'
+  __typename?: 'License'
   id: string
+  code?: Types.Maybe<number>
   attribution?: Types.Maybe<string>
-  type:
-    | { __typename: 'UserDefinedLicense'; content: string }
-    | { __typename: 'KnownLicense'; code: string; url?: Types.Maybe<string> }
+  customText?: Types.Maybe<string>
 }
 
 export type VideoFieldsFragment = {
-  __typename: 'Video'
+  __typename?: 'Video'
   id: string
-  title: string
-  description: string
+  title?: Types.Maybe<string>
+  description?: Types.Maybe<string>
   views?: Types.Maybe<number>
-  duration: number
-  thumbnailUrl: string
+  duration?: Types.Maybe<number>
   createdAt: Date
-  category: { __typename: 'Category'; id: string }
-  media: { __typename: 'VideoMedia' } & VideoMediaFieldsFragment
-  channel: { __typename: 'Channel'; id: string; avatarPhotoUrl?: Types.Maybe<string>; handle: string }
-  license: { __typename: 'LicenseEntity' } & LicenseFieldsFragment
+  isPublic?: Types.Maybe<boolean>
+  isExplicit?: Types.Maybe<boolean>
+  isFeatured: boolean
+  hasMarketing?: Types.Maybe<boolean>
+  isCensored: boolean
+  publishedBeforeJoystream?: Types.Maybe<Date>
+  mediaUrls: Array<string>
+  mediaAvailability: Types.AssetAvailability
+  thumbnailPhotoUrls: Array<string>
+  thumbnailPhotoAvailability: Types.AssetAvailability
+  category?: Types.Maybe<{ __typename?: 'VideoCategory'; id: string }>
+  language?: Types.Maybe<{ __typename?: 'Language'; iso: string }>
+  mediaMetadata: { __typename?: 'VideoMediaMetadata' } & VideoMediaMetadataFieldsFragment
+  mediaDataObject?: Types.Maybe<{ __typename?: 'DataObject' } & DataObjectFieldsFragment>
+  thumbnailPhotoDataObject?: Types.Maybe<{ __typename?: 'DataObject' } & DataObjectFieldsFragment>
+  channel: { __typename?: 'Channel' } & BasicChannelFieldsFragment
+  license?: Types.Maybe<{ __typename?: 'License' } & LicenseFieldsFragment>
 }
 
 export type GetVideoQueryVariables = Types.Exact<{
-  id: Types.Scalars['ID']
+  where: Types.VideoWhereUniqueInput
 }>
 
 export type GetVideoQuery = {
-  __typename: 'Query'
-  video?: Types.Maybe<
-    { __typename: 'Video'; channel: { __typename: 'Channel' } & BasicChannelFieldsFragment } & VideoFieldsFragment
-  >
+  __typename?: 'Query'
+  videoByUniqueInput?: Types.Maybe<{ __typename?: 'Video' } & VideoFieldsFragment>
 }
 
 export type GetVideosConnectionQueryVariables = Types.Exact<{
   first?: Types.Maybe<Types.Scalars['Int']>
   after?: Types.Maybe<Types.Scalars['String']>
-  categoryId?: Types.Maybe<Types.Scalars['ID']>
-  channelId?: Types.Maybe<Types.Scalars['ID']>
-  channelIdIn?: Types.Maybe<Array<Types.Maybe<Types.Scalars['ID']>> | Types.Maybe<Types.Scalars['ID']>>
-  createdAtGte?: Types.Maybe<Types.Scalars['Date']>
   orderBy?: Types.Maybe<Types.VideoOrderByInput>
+  where?: Types.Maybe<Types.VideoWhereInput>
 }>
 
 export type GetVideosConnectionQuery = {
-  __typename: 'Query'
+  __typename?: 'Query'
   videosConnection: {
-    __typename: 'VideoConnection'
+    __typename?: 'VideoConnection'
     totalCount: number
-    edges: Array<{ __typename: 'VideoEdge'; cursor: string; node: { __typename: 'Video' } & VideoFieldsFragment }>
-    pageInfo: { __typename: 'PageInfo'; hasNextPage: boolean; endCursor?: Types.Maybe<string> }
+    edges: Array<{ __typename?: 'VideoEdge'; cursor: string; node: { __typename?: 'Video' } & VideoFieldsFragment }>
+    pageInfo: { __typename?: 'PageInfo'; hasNextPage: boolean; endCursor?: Types.Maybe<string> }
   }
 }
 
 export type GetVideosQueryVariables = Types.Exact<{
-  id_in: Array<Types.Scalars['ID']> | Types.Scalars['ID']
+  offset?: Types.Maybe<Types.Scalars['Int']>
+  limit?: Types.Maybe<Types.Scalars['Int']>
+  where?: Types.Maybe<Types.VideoWhereInput>
+  orderBy?: Types.Maybe<Types.VideoOrderByInput>
 }>
 
-export type GetVideosQuery = { __typename: 'Query'; videos: Array<{ __typename: 'Video' } & VideoFieldsFragment> }
-
-export type GetFeaturedVideosQueryVariables = Types.Exact<{ [key: string]: never }>
-
-export type GetFeaturedVideosQuery = {
-  __typename: 'Query'
-  featuredVideos: Array<{ __typename: 'FeaturedVideo'; video: { __typename: 'Video' } & VideoFieldsFragment }>
+export type GetVideosQuery = {
+  __typename?: 'Query'
+  videos?: Types.Maybe<Array<{ __typename?: 'Video' } & VideoFieldsFragment>>
 }
 
 export type GetCoverVideoQueryVariables = Types.Exact<{ [key: string]: never }>
 
 export type GetCoverVideoQuery = {
-  __typename: 'Query'
+  __typename?: 'Query'
   coverVideo: {
-    __typename: 'CoverVideo'
+    __typename?: 'CoverVideo'
     coverDescription: string
-    video: { __typename: 'Video' } & VideoFieldsFragment
-    coverCutMedia: { __typename: 'VideoMedia' } & VideoMediaFieldsFragment
+    video: { __typename?: 'Video' } & VideoFieldsFragment
+    coverCutMediaMetadata: { __typename?: 'VideoMediaMetadata' } & VideoMediaMetadataFieldsFragment
   }
 }
 
+export type GetVideoViewsQueryVariables = Types.Exact<{
+  videoId: Types.Scalars['ID']
+}>
+
+export type GetVideoViewsQuery = {
+  __typename?: 'Query'
+  videoViews?: Types.Maybe<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>
+}
+
 export type AddVideoViewMutationVariables = Types.Exact<{
   videoId: Types.Scalars['ID']
   channelId: Types.Scalars['ID']
 }>
 
 export type AddVideoViewMutation = {
-  __typename: 'Mutation'
-  addVideoView: { __typename: 'EntityViewsInfo'; id: string; views: number }
+  __typename?: 'Mutation'
+  addVideoView: { __typename?: 'EntityViewsInfo'; id: string; views: number }
 }
 
-export const VideoMediaFieldsFragmentDoc = gql`
-  fragment VideoMediaFields on VideoMedia {
+export const VideoMediaMetadataFieldsFragmentDoc = gql`
+  fragment VideoMediaMetadataFields on VideoMediaMetadata {
     id
     pixelHeight
     pixelWidth
-    location {
-      ... on HttpMediaLocation {
-        url
-      }
-      ... on JoystreamMediaLocation {
-        dataObjectId
-      }
-    }
   }
 `
 export const LicenseFieldsFragmentDoc = gql`
-  fragment LicenseFields on LicenseEntity {
+  fragment LicenseFields on License {
     id
+    code
     attribution
-    type {
-      ... on KnownLicense {
-        code
-        url
-      }
-      ... on UserDefinedLicense {
-        content
-      }
-    }
+    customText
   }
 `
 export const VideoFieldsFragmentDoc = gql`
@@ -144,34 +141,48 @@ export const VideoFieldsFragmentDoc = gql`
     }
     views
     duration
-    thumbnailUrl
     createdAt
-    media {
-      ...VideoMediaFields
+    isPublic
+    isExplicit
+    isFeatured
+    hasMarketing
+    isCensored
+    language {
+      iso
+    }
+    publishedBeforeJoystream
+    mediaMetadata {
+      ...VideoMediaMetadataFields
+    }
+    mediaUrls
+    mediaAvailability
+    mediaDataObject {
+      ...DataObjectFields
+    }
+    thumbnailPhotoUrls
+    thumbnailPhotoAvailability
+    thumbnailPhotoDataObject {
+      ...DataObjectFields
     }
     channel {
-      id
-      avatarPhotoUrl
-      handle
+      ...BasicChannelFields
     }
     license {
       ...LicenseFields
     }
   }
-  ${VideoMediaFieldsFragmentDoc}
+  ${VideoMediaMetadataFieldsFragmentDoc}
+  ${DataObjectFieldsFragmentDoc}
+  ${BasicChannelFieldsFragmentDoc}
   ${LicenseFieldsFragmentDoc}
 `
 export const GetVideoDocument = gql`
-  query GetVideo($id: ID!) {
-    video(where: { id: $id }) {
+  query GetVideo($where: VideoWhereUniqueInput!) {
+    videoByUniqueInput(where: $where) {
       ...VideoFields
-      channel {
-        ...BasicChannelFields
-      }
     }
   }
   ${VideoFieldsFragmentDoc}
-  ${BasicChannelFieldsFragmentDoc}
 `
 
 /**
@@ -186,7 +197,7 @@ export const GetVideoDocument = gql`
  * @example
  * const { data, loading, error } = useGetVideoQuery({
  *   variables: {
- *      id: // value for 'id'
+ *      where: // value for 'where'
  *   },
  * });
  */
@@ -203,24 +214,10 @@ export const GetVideosConnectionDocument = gql`
   query GetVideosConnection(
     $first: Int
     $after: String
-    $categoryId: ID
-    $channelId: ID
-    $channelIdIn: [ID]
-    $createdAtGte: Date
     $orderBy: VideoOrderByInput = createdAt_DESC
+    $where: VideoWhereInput
   ) {
-    videosConnection(
-      first: $first
-      after: $after
-      where: {
-        categoryId_eq: $categoryId
-        channelId_eq: $channelId
-        isCurated_eq: false
-        channelId_in: $channelIdIn
-        createdAt_gte: $createdAtGte
-      }
-      orderBy: $orderBy
-    ) {
+    videosConnection(first: $first, after: $after, where: $where, orderBy: $orderBy) {
       edges {
         cursor
         node {
@@ -251,11 +248,8 @@ export const GetVideosConnectionDocument = gql`
  *   variables: {
  *      first: // value for 'first'
  *      after: // value for 'after'
- *      categoryId: // value for 'categoryId'
- *      channelId: // value for 'channelId'
- *      channelIdIn: // value for 'channelIdIn'
- *      createdAtGte: // value for 'createdAtGte'
  *      orderBy: // value for 'orderBy'
+ *      where: // value for 'where'
  *   },
  * });
  */
@@ -282,8 +276,8 @@ export type GetVideosConnectionQueryResult = Apollo.QueryResult<
   GetVideosConnectionQueryVariables
 >
 export const GetVideosDocument = gql`
-  query GetVideos($id_in: [ID!]!) {
-    videos(where: { id_in: $id_in }) {
+  query GetVideos($offset: Int, $limit: Int, $where: VideoWhereInput, $orderBy: VideoOrderByInput = createdAt_DESC) {
+    videos(offset: $offset, limit: $limit, where: $where, orderBy: $orderBy) {
       ...VideoFields
     }
   }
@@ -302,11 +296,14 @@ export const GetVideosDocument = gql`
  * @example
  * const { data, loading, error } = useGetVideosQuery({
  *   variables: {
- *      id_in: // value for 'id_in'
+ *      offset: // value for 'offset'
+ *      limit: // value for 'limit'
+ *      where: // value for 'where'
+ *      orderBy: // value for 'orderBy'
  *   },
  * });
  */
-export function useGetVideosQuery(baseOptions: Apollo.QueryHookOptions<GetVideosQuery, GetVideosQueryVariables>) {
+export function useGetVideosQuery(baseOptions?: Apollo.QueryHookOptions<GetVideosQuery, GetVideosQueryVariables>) {
   return Apollo.useQuery<GetVideosQuery, GetVideosQueryVariables>(GetVideosDocument, baseOptions)
 }
 export function useGetVideosLazyQuery(
@@ -317,95 +314,88 @@ export function useGetVideosLazyQuery(
 export type GetVideosQueryHookResult = ReturnType<typeof useGetVideosQuery>
 export type GetVideosLazyQueryHookResult = ReturnType<typeof useGetVideosLazyQuery>
 export type GetVideosQueryResult = Apollo.QueryResult<GetVideosQuery, GetVideosQueryVariables>
-export const GetFeaturedVideosDocument = gql`
-  query GetFeaturedVideos {
-    featuredVideos(orderBy: createdAt_DESC) {
+export const GetCoverVideoDocument = gql`
+  query GetCoverVideo {
+    coverVideo {
       video {
         ...VideoFields
       }
+      coverDescription
+      coverCutMediaMetadata {
+        ...VideoMediaMetadataFields
+      }
     }
   }
   ${VideoFieldsFragmentDoc}
+  ${VideoMediaMetadataFieldsFragmentDoc}
 `
 
 /**
- * __useGetFeaturedVideosQuery__
+ * __useGetCoverVideoQuery__
  *
- * To run a query within a React component, call `useGetFeaturedVideosQuery` and pass it any options that fit your needs.
- * When your component renders, `useGetFeaturedVideosQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * To run a query within a React component, call `useGetCoverVideoQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetCoverVideoQuery` returns an object from Apollo Client that contains loading, error, and data properties
  * you can use to render your UI.
  *
  * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
  *
  * @example
- * const { data, loading, error } = useGetFeaturedVideosQuery({
+ * const { data, loading, error } = useGetCoverVideoQuery({
  *   variables: {
  *   },
  * });
  */
-export function useGetFeaturedVideosQuery(
-  baseOptions?: Apollo.QueryHookOptions<GetFeaturedVideosQuery, GetFeaturedVideosQueryVariables>
+export function useGetCoverVideoQuery(
+  baseOptions?: Apollo.QueryHookOptions<GetCoverVideoQuery, GetCoverVideoQueryVariables>
 ) {
-  return Apollo.useQuery<GetFeaturedVideosQuery, GetFeaturedVideosQueryVariables>(
-    GetFeaturedVideosDocument,
-    baseOptions
-  )
+  return Apollo.useQuery<GetCoverVideoQuery, GetCoverVideoQueryVariables>(GetCoverVideoDocument, baseOptions)
 }
-export function useGetFeaturedVideosLazyQuery(
-  baseOptions?: Apollo.LazyQueryHookOptions<GetFeaturedVideosQuery, GetFeaturedVideosQueryVariables>
+export function useGetCoverVideoLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetCoverVideoQuery, GetCoverVideoQueryVariables>
 ) {
-  return Apollo.useLazyQuery<GetFeaturedVideosQuery, GetFeaturedVideosQueryVariables>(
-    GetFeaturedVideosDocument,
-    baseOptions
-  )
+  return Apollo.useLazyQuery<GetCoverVideoQuery, GetCoverVideoQueryVariables>(GetCoverVideoDocument, baseOptions)
 }
-export type GetFeaturedVideosQueryHookResult = ReturnType<typeof useGetFeaturedVideosQuery>
-export type GetFeaturedVideosLazyQueryHookResult = ReturnType<typeof useGetFeaturedVideosLazyQuery>
-export type GetFeaturedVideosQueryResult = Apollo.QueryResult<GetFeaturedVideosQuery, GetFeaturedVideosQueryVariables>
-export const GetCoverVideoDocument = gql`
-  query GetCoverVideo {
-    coverVideo {
-      video {
-        ...VideoFields
-      }
-      coverDescription
-      coverCutMedia {
-        ...VideoMediaFields
-      }
+export type GetCoverVideoQueryHookResult = ReturnType<typeof useGetCoverVideoQuery>
+export type GetCoverVideoLazyQueryHookResult = ReturnType<typeof useGetCoverVideoLazyQuery>
+export type GetCoverVideoQueryResult = Apollo.QueryResult<GetCoverVideoQuery, GetCoverVideoQueryVariables>
+export const GetVideoViewsDocument = gql`
+  query GetVideoViews($videoId: ID!) {
+    videoViews(videoId: $videoId) {
+      id
+      views
     }
   }
-  ${VideoFieldsFragmentDoc}
-  ${VideoMediaFieldsFragmentDoc}
 `
 
 /**
- * __useGetCoverVideoQuery__
+ * __useGetVideoViewsQuery__
  *
- * To run a query within a React component, call `useGetCoverVideoQuery` and pass it any options that fit your needs.
- * When your component renders, `useGetCoverVideoQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * To run a query within a React component, call `useGetVideoViewsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetVideoViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties
  * you can use to render your UI.
  *
  * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
  *
  * @example
- * const { data, loading, error } = useGetCoverVideoQuery({
+ * const { data, loading, error } = useGetVideoViewsQuery({
  *   variables: {
+ *      videoId: // value for 'videoId'
  *   },
  * });
  */
-export function useGetCoverVideoQuery(
-  baseOptions?: Apollo.QueryHookOptions<GetCoverVideoQuery, GetCoverVideoQueryVariables>
+export function useGetVideoViewsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetVideoViewsQuery, GetVideoViewsQueryVariables>
 ) {
-  return Apollo.useQuery<GetCoverVideoQuery, GetCoverVideoQueryVariables>(GetCoverVideoDocument, baseOptions)
+  return Apollo.useQuery<GetVideoViewsQuery, GetVideoViewsQueryVariables>(GetVideoViewsDocument, baseOptions)
 }
-export function useGetCoverVideoLazyQuery(
-  baseOptions?: Apollo.LazyQueryHookOptions<GetCoverVideoQuery, GetCoverVideoQueryVariables>
+export function useGetVideoViewsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetVideoViewsQuery, GetVideoViewsQueryVariables>
 ) {
-  return Apollo.useLazyQuery<GetCoverVideoQuery, GetCoverVideoQueryVariables>(GetCoverVideoDocument, baseOptions)
+  return Apollo.useLazyQuery<GetVideoViewsQuery, GetVideoViewsQueryVariables>(GetVideoViewsDocument, baseOptions)
 }
-export type GetCoverVideoQueryHookResult = ReturnType<typeof useGetCoverVideoQuery>
-export type GetCoverVideoLazyQueryHookResult = ReturnType<typeof useGetCoverVideoLazyQuery>
-export type GetCoverVideoQueryResult = Apollo.QueryResult<GetCoverVideoQuery, GetCoverVideoQueryVariables>
+export type GetVideoViewsQueryHookResult = ReturnType<typeof useGetVideoViewsQuery>
+export type GetVideoViewsLazyQueryHookResult = ReturnType<typeof useGetVideoViewsLazyQuery>
+export type GetVideoViewsQueryResult = Apollo.QueryResult<GetVideoViewsQuery, GetVideoViewsQueryVariables>
 export const AddVideoViewDocument = gql`
   mutation AddVideoView($videoId: ID!, $channelId: ID!) {
     addVideoView(videoId: $videoId, channelId: $channelId) {

+ 116 - 0
src/api/queries/__generated__/workers.generated.tsx

@@ -0,0 +1,116 @@
+import * as Types from './baseTypes.generated'
+
+import { gql } from '@apollo/client'
+import * as Apollo from '@apollo/client'
+export type BasicWorkerFieldsFragment = {
+  __typename?: 'Worker'
+  id: string
+  workerId: string
+  metadata?: Types.Maybe<string>
+  isActive: boolean
+  type: Types.WorkerType
+}
+
+export type GetWorkerQueryVariables = Types.Exact<{
+  where: Types.WorkerWhereUniqueInput
+}>
+
+export type GetWorkerQuery = {
+  __typename?: 'Query'
+  workerByUniqueInput?: Types.Maybe<{ __typename?: 'Worker' } & BasicWorkerFieldsFragment>
+}
+
+export type GetWorkersQueryVariables = Types.Exact<{
+  limit?: Types.Maybe<Types.Scalars['Int']>
+  offset?: Types.Maybe<Types.Scalars['Int']>
+  where?: Types.Maybe<Types.WorkerWhereInput>
+}>
+
+export type GetWorkersQuery = {
+  __typename?: 'Query'
+  workers?: Types.Maybe<Array<{ __typename?: 'Worker' } & BasicWorkerFieldsFragment>>
+}
+
+export const BasicWorkerFieldsFragmentDoc = gql`
+  fragment BasicWorkerFields on Worker {
+    id
+    workerId
+    metadata
+    isActive
+    type
+  }
+`
+export const GetWorkerDocument = gql`
+  query GetWorker($where: WorkerWhereUniqueInput!) {
+    workerByUniqueInput(where: $where) {
+      ...BasicWorkerFields
+    }
+  }
+  ${BasicWorkerFieldsFragmentDoc}
+`
+
+/**
+ * __useGetWorkerQuery__
+ *
+ * To run a query within a React component, call `useGetWorkerQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetWorkerQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetWorkerQuery({
+ *   variables: {
+ *      where: // value for 'where'
+ *   },
+ * });
+ */
+export function useGetWorkerQuery(baseOptions: Apollo.QueryHookOptions<GetWorkerQuery, GetWorkerQueryVariables>) {
+  return Apollo.useQuery<GetWorkerQuery, GetWorkerQueryVariables>(GetWorkerDocument, baseOptions)
+}
+export function useGetWorkerLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetWorkerQuery, GetWorkerQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetWorkerQuery, GetWorkerQueryVariables>(GetWorkerDocument, baseOptions)
+}
+export type GetWorkerQueryHookResult = ReturnType<typeof useGetWorkerQuery>
+export type GetWorkerLazyQueryHookResult = ReturnType<typeof useGetWorkerLazyQuery>
+export type GetWorkerQueryResult = Apollo.QueryResult<GetWorkerQuery, GetWorkerQueryVariables>
+export const GetWorkersDocument = gql`
+  query GetWorkers($limit: Int, $offset: Int, $where: WorkerWhereInput) {
+    workers(limit: $limit, offset: $offset, where: $where) {
+      ...BasicWorkerFields
+    }
+  }
+  ${BasicWorkerFieldsFragmentDoc}
+`
+
+/**
+ * __useGetWorkersQuery__
+ *
+ * To run a query within a React component, call `useGetWorkersQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetWorkersQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetWorkersQuery({
+ *   variables: {
+ *      limit: // value for 'limit'
+ *      offset: // value for 'offset'
+ *      where: // value for 'where'
+ *   },
+ * });
+ */
+export function useGetWorkersQuery(baseOptions?: Apollo.QueryHookOptions<GetWorkersQuery, GetWorkersQueryVariables>) {
+  return Apollo.useQuery<GetWorkersQuery, GetWorkersQueryVariables>(GetWorkersDocument, baseOptions)
+}
+export function useGetWorkersLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetWorkersQuery, GetWorkersQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetWorkersQuery, GetWorkersQueryVariables>(GetWorkersDocument, baseOptions)
+}
+export type GetWorkersQueryHookResult = ReturnType<typeof useGetWorkersQuery>
+export type GetWorkersLazyQueryHookResult = ReturnType<typeof useGetWorkersLazyQuery>
+export type GetWorkersQueryResult = Apollo.QueryResult<GetWorkersQuery, GetWorkersQueryVariables>

+ 4 - 4
src/api/queries/categories.graphql

@@ -1,10 +1,10 @@
-fragment CategoryFields on Category {
+fragment VideoCategoryFields on VideoCategory {
   id
   name
 }
 
-query GetCategories {
-  categories {
-    ...CategoryFields
+query GetVideoCategories {
+  videoCategories {
+    ...VideoCategoryFields
   }
 }

+ 41 - 17
src/api/queries/channels.graphql

@@ -1,48 +1,62 @@
 fragment BasicChannelFields on Channel {
   id
-  handle
-  avatarPhotoUrl
+  title
+  createdAt
+
+  avatarPhotoUrls
+  avatarPhotoAvailability
+  avatarPhotoDataObject {
+    ...DataObjectFields
+  }
 }
 
 fragment AllChannelFields on Channel {
-  id
-  handle
-  avatarPhotoUrl
-  coverPhotoUrl
+  ...BasicChannelFields
+  description
   follows
+  isPublic
+  isCensored
+  language {
+    iso
+  }
+
+  coverPhotoUrls
+  coverPhotoAvailability
+  coverPhotoDataObject {
+    ...DataObjectFields
+  }
 }
 
-query GetBasicChannel($id: ID!) {
-  channel(where: { id: $id }) {
+query GetBasicChannel($where: ChannelWhereUniqueInput!) {
+  channelByUniqueInput(where: $where) {
     ...BasicChannelFields
   }
 }
 
-query GetChannel($id: ID!) {
-  channel(where: { id: $id }) {
+query GetChannel($where: ChannelWhereUniqueInput!) {
+  channelByUniqueInput(where: $where) {
     ...AllChannelFields
   }
 }
 
-query GetChannelVideoCount($channelId: ID!) {
-  videosConnection(first: 0, where: { channelId_eq: $channelId }) {
+query GetVideoCount($where: VideoWhereInput) {
+  videosConnection(first: 0, where: $where) {
     totalCount
   }
 }
 
-query GetChannels($id_in: [ID!]!) {
-  channels(where: { id_in: $id_in }) {
+query GetChannels($where: ChannelWhereInput) {
+  channels(where: $where) {
     ...AllChannelFields
   }
 }
 
-query GetChannelsConnection($first: Int, $after: String) {
-  channelsConnection(first: $first, after: $after, orderBy: createdAt_DESC) {
+query GetChannelsConnection($first: Int, $after: String, $where: ChannelWhereInput) {
+  channelsConnection(first: $first, after: $after, where: $where, orderBy: createdAt_DESC) {
     edges {
       cursor
       node {
         ...AllChannelFields
-        createdAt # the node must include the field by which the connection is sorted by
       }
     }
     pageInfo {
@@ -53,6 +67,16 @@ query GetChannelsConnection($first: Int, $after: String) {
   }
 }
 
+### Orion
+
+# modyfying this query name will need a sync-up in `src/api/client/resolvers.ts`
+query GetChannelFollows($channelId: ID!) {
+  channelFollows(channelId: $channelId) {
+    id
+    follows
+  }
+}
+
 mutation FollowChannel($channelId: ID!) {
   followChannel(channelId: $channelId) {
     id

+ 1 - 0
src/api/queries/index.ts

@@ -3,3 +3,4 @@ export * from './__generated__/categories.generated'
 export * from './__generated__/channels.generated'
 export * from './__generated__/search.generated'
 export * from './__generated__/videos.generated'
+export * from './__generated__/memberships.generated'

+ 22 - 0
src/api/queries/memberships.graphql

@@ -0,0 +1,22 @@
+fragment BasicMembershipFields on Membership {
+  id
+  handle
+  avatarUri
+  about
+  controllerAccount
+  channels {
+    ...BasicChannelFields
+  }
+}
+
+query GetMembership($where: MembershipWhereUniqueInput!) {
+  membershipByUniqueInput(where: $where) {
+    ...BasicMembershipFields
+  }
+}
+
+query GetMemberships($where: MembershipWhereInput!) {
+  memberships(where: $where) {
+    ...BasicMembershipFields
+  }
+}

+ 8 - 0
src/api/queries/queryNode.graphql

@@ -0,0 +1,8 @@
+subscription GetQueryNodeState {
+  stateSubscription {
+    chainHead
+    indexerHead
+    lastCompleteBlock
+    lastProcessedEvent
+  }
+}

+ 0 - 1
src/api/queries/search.graphql

@@ -8,6 +8,5 @@ query Search($text: String!) {
         ...BasicChannelFields
       }
     }
-    rank
   }
 }

+ 11 - 0
src/api/queries/shared.graphql

@@ -0,0 +1,11 @@
+fragment DataObjectFields on DataObject {
+  id
+  createdAt
+  size
+  liaison {
+    ...BasicWorkerFields
+  }
+  liaisonJudgement
+  ipfsContentId
+  joystreamContentId
+}

+ 44 - 58
src/api/queries/videos.graphql

@@ -1,29 +1,14 @@
-fragment VideoMediaFields on VideoMedia {
+fragment VideoMediaMetadataFields on VideoMediaMetadata {
   id
   pixelHeight
   pixelWidth
-  location {
-    ... on HttpMediaLocation {
-      url
-    }
-    ... on JoystreamMediaLocation {
-      dataObjectId
-    }
-  }
 }
 
-fragment LicenseFields on LicenseEntity {
+fragment LicenseFields on License {
   id
+  code
   attribution
-  type {
-    ... on KnownLicense {
-      code
-      url
-    }
-    ... on UserDefinedLicense {
-      content
-    }
-  }
+  customText
 }
 
 fragment VideoFields on Video {
@@ -35,51 +20,50 @@ fragment VideoFields on Video {
   }
   views
   duration
-  thumbnailUrl
   createdAt
-  media {
-    ...VideoMediaFields
+  isPublic
+  isExplicit
+  isFeatured
+  hasMarketing
+  isCensored
+  language {
+    iso
+  }
+  publishedBeforeJoystream
+  mediaMetadata {
+    ...VideoMediaMetadataFields
+  }
+  mediaUrls
+  mediaAvailability
+  mediaDataObject {
+    ...DataObjectFields
+  }
+  thumbnailPhotoUrls
+  thumbnailPhotoAvailability
+  thumbnailPhotoDataObject {
+    ...DataObjectFields
   }
   channel {
-    id
-    avatarPhotoUrl
-    handle
+    ...BasicChannelFields
   }
   license {
     ...LicenseFields
   }
 }
 
-query GetVideo($id: ID!) {
-  video(where: { id: $id }) {
+query GetVideo($where: VideoWhereUniqueInput!) {
+  videoByUniqueInput(where: $where) {
     ...VideoFields
-    channel {
-      ...BasicChannelFields
-    }
   }
 }
 
 query GetVideosConnection(
   $first: Int
   $after: String
-  $categoryId: ID
-  $channelId: ID
-  $channelIdIn: [ID]
-  $createdAtGte: Date
   $orderBy: VideoOrderByInput = createdAt_DESC
+  $where: VideoWhereInput
 ) {
-  videosConnection(
-    first: $first
-    after: $after
-    where: {
-      categoryId_eq: $categoryId
-      channelId_eq: $channelId
-      isCurated_eq: false
-      channelId_in: $channelIdIn
-      createdAt_gte: $createdAtGte
-    }
-    orderBy: $orderBy
-  ) {
+  videosConnection(first: $first, after: $after, where: $where, orderBy: $orderBy) {
     edges {
       cursor
       node {
@@ -94,32 +78,34 @@ query GetVideosConnection(
   }
 }
 
-query GetVideos($id_in: [ID!]!) {
-  videos(where: { id_in: $id_in }) {
+query GetVideos($offset: Int, $limit: Int, $where: VideoWhereInput, $orderBy: VideoOrderByInput = createdAt_DESC) {
+  videos(offset: $offset, limit: $limit, where: $where, orderBy: $orderBy) {
     ...VideoFields
   }
 }
 
-query GetFeaturedVideos {
-  featuredVideos(orderBy: createdAt_DESC) {
-    video {
-      ...VideoFields
-    }
-  }
-}
-
 query GetCoverVideo {
   coverVideo {
     video {
       ...VideoFields
     }
     coverDescription
-    coverCutMedia {
-      ...VideoMediaFields
+    coverCutMediaMetadata {
+      ...VideoMediaMetadataFields
     }
   }
 }
 
+### Orion
+
+# modyfying this query name will need a sync-up in `src/api/client/resolvers.ts`
+query GetVideoViews($videoId: ID!) {
+  videoViews(videoId: $videoId) {
+    id
+    views
+  }
+}
+
 mutation AddVideoView($videoId: ID!, $channelId: ID!) {
   addVideoView(videoId: $videoId, channelId: $channelId) {
     id

+ 19 - 0
src/api/queries/workers.graphql

@@ -0,0 +1,19 @@
+fragment BasicWorkerFields on Worker {
+  id
+  workerId
+  metadata
+  isActive
+  type
+}
+
+query GetWorker($where: WorkerWhereUniqueInput!) {
+  workerByUniqueInput(where: $where) {
+    ...BasicWorkerFields
+  }
+}
+
+query GetWorkers($limit: Int, $offset: Int, $where: WorkerWhereInput) {
+  workers(limit: $limit, offset: $offset, where: $where) {
+    ...BasicWorkerFields
+  }
+}

+ 190 - 155
src/api/schemas/extendedQueryNode.graphql

@@ -1,181 +1,125 @@
-scalar Date
+scalar DateTime
 
-enum Language {
-  Chinese
-  English
-  Arabic
-  Portugese
-  French
+type Language {
+  iso: String!
 }
 
-type Member {
+type VideoCategory {
   id: ID!
-  handle: String!
+  name: String
+  videos: [Video!]
 }
 
-type Channel {
+type License {
   id: ID!
-
-  createdAt: Date!
-
-  handle: String!
-
-  description: String!
-
-  coverPhotoUrl: String
-
-  avatarPhotoUrl: String
-
-  owner: Member!
-
-  isPublic: Boolean!
-
-  isCurated: Boolean!
-
-  language: Language
-
-  videos: [Video!]!
-
-  # extended from Orion
-  follows: Int
+  code: Int
+  attribution: String
+  customText: String
 }
 
-type Category {
-  id: ID!
-
-  name: String!
-
-  videos: [Video!]
-}
+type PageInfo {
+  hasNextPage: Boolean!
+  hasPreviousPage: Boolean!
 
-type JoystreamMediaLocation {
-  dataObjectId: String!
+  startCursor: String
+  endCursor: String
 }
 
-type HttpMediaLocation {
-  url: String!
+enum AssetAvailability {
+  ACCEPTED
+  PENDING
+  INVALID
 }
 
-# In the future we can add IPFS, Dat, etc.
-union MediaLocation = JoystreamMediaLocation | HttpMediaLocation
-
-type KnownLicense {
-  code: String!
-  name: String
-  description: String
-  url: String
+enum LiaisonJudgement {
+  PENDING
+  ACCEPTED
+  REJECTED
 }
 
-type UserDefinedLicense {
-  content: String!
+enum WorkerType {
+  GATEWAY
+  STORAGE
 }
 
-union License = UserDefinedLicense | KnownLicense
-
-type LicenseEntity {
-  id: ID!
-  type: License!
-  attribution: String
-  videoLicense: [Video!]
-}
-type VideoMedia {
+type Worker {
+  # unique ID
   id: ID!
+  # ID of worker in the group, can be the same for different workers (in different groups)
+  workerId: String!
 
-  # Resolution width
-  pixelWidth: Int!
-
-  # Resolution height
-  pixelHeight: Int!
-  # Size in bytes
-  size: Float
-
-  # where to find
-  location: MediaLocation!
+  type: WorkerType!
+  metadata: String
+  isActive: Boolean!
 }
 
-type Video {
+type DataObject {
   id: ID!
-
-  channel: Channel!
-
-  category: Category!
-
-  title: String!
-
-  description: String!
-
-  # extended from Orion
-  views: Int
-
-  # In seconds
-  duration: Int!
-
-  # In intro
-  skippableIntroDuration: Int
-
-  thumbnailUrl: String!
-  Language: Language
-
-  media: VideoMedia!
-
-  hasMarketing: Boolean
-
-  # Timestamp of block
-  createdAt: Date!
-  createdAtBlockHeight: Float!
-
-  # Possible time when video was published before Joystream
-  publishedBeforeJoystream: String
-
-  isPublic: Boolean!
-
-  isCurated: Boolean!
-
-  isExplicit: Boolean!
-
-  license: LicenseEntity!
+  createdAt: DateTime!
+  size: Float!
+  # storage provider that accepted the asset
+  liaison: Worker
+  # status of asset as reported by liaison
+  liaisonJudgement: LiaisonJudgement!
+  # IPFS content id
+  ipfsContentId: String!
+  # Joystream runtime content id
+  joystreamContentId: String!
 }
 
-type CoverVideo {
+type Membership {
   id: ID!
-
-  video: Video!
-
-  coverDescription: String!
-
-  coverCutMedia: VideoMedia!
+  handle: String!
+  avatarUri: String
+  controllerAccount: String!
+  about: String
+  channels: [Channel!]!
 }
 
-type FeaturedVideo {
-  id: ID!
-
-  video: Video!
+input MembershipWhereUniqueInput {
+  id: ID
+  handle: String
 }
 
-union SearchResult = Video | Channel
-
-type SearchFTSOutput {
-  item: SearchResult!
+input MembershipWhereInput {
+  controllerAccount_eq: ID
+  controllerAccount_in: [ID!]
+}
 
-  rank: Float!
+type Channel {
+  id: ID!
+  createdAt: DateTime!
 
-  isTypeOf: String!
+  ownerMember: Membership
+  videos: [Video!]!
+  isCensored: Boolean!
 
-  highlight: String!
-}
+  # === metadata ===
+  title: String
+  description: String
+  isPublic: Boolean
+  language: Language
 
-type PageInfo {
-  hasNextPage: Boolean!
-  hasPreviousPage: Boolean!
+  # === assets ===
+  coverPhotoDataObject: DataObject
+  coverPhotoUrls: [String!]!
+  coverPhotoAvailability: AssetAvailability!
+  avatarPhotoDataObject: DataObject
+  avatarPhotoUrls: [String!]!
+  avatarPhotoAvailability: AssetAvailability!
 
-  startCursor: String
-  endCursor: String
+  # === extended from Orion ===
+  follows: Int
 }
 
 input ChannelWhereInput {
+  id_in: [ID!]
+  ownerMemberId_eq: ID
   isCurated_eq: Boolean
   isPublic_eq: Boolean
-  id_in: [ID!]
+  isCensored_eq: Boolean
+  coverPhotoAvailability_eq: AssetAvailability
+  avatarPhotoAvailability_eq: AssetAvailability
 }
 
 input ChannelWhereUniqueInput {
@@ -198,17 +142,64 @@ type ChannelConnection {
   totalCount: Int!
 }
 
-input CategoryWhereUniqueInput {
+type VideoMediaMetadata {
+  id: ID!
+
+  pixelWidth: Int
+  pixelHeight: Int
+  # Size in bytes
+  size: Int
+}
+
+type Video {
+  id: ID!
+  createdAt: DateTime!
+  channel: Channel!
+  isCensored: Boolean!
+  isFeatured: Boolean!
+  publishedBeforeJoystream: DateTime
+
+  # === metadata ===
+  title: String
+  description: String
+  category: VideoCategory
+  language: Language
+  hasMarketing: Boolean
+  isExplicit: Boolean
+  isPublic: Boolean
+  license: License
+
+  # === assets ===
+  thumbnailPhotoDataObject: DataObject
+  thumbnailPhotoUrls: [String!]!
+  thumbnailPhotoAvailability: AssetAvailability!
+  mediaDataObject: DataObject
+  mediaUrls: [String!]!
+  mediaAvailability: AssetAvailability!
+
+  mediaMetadata: VideoMediaMetadata!
+  # In seconds
+  duration: Int
+  skippableIntroDuration: Int
+
+  # === extended from Orion ===
+  views: Int
+}
+
+input VideoCategoryWhereUniqueInput {
   id: ID!
 }
 
 input VideoWhereInput {
   categoryId_eq: ID
-  channelId_in: [ID]
+  channelId_in: [ID!]
   channelId_eq: ID
-  createdAt_gte: Date
-  isCurated_eq: Boolean
+  thumbnailPhotoAvailability_eq: AssetAvailability
+  mediaAvailability_eq: AssetAvailability
+  createdAt_gte: DateTime
+  isFeatured_eq: Boolean
   isPublic_eq: Boolean
+  isCensored_eq: Boolean
   id_in: [ID!]
 }
 
@@ -232,14 +223,56 @@ type VideoConnection {
   totalCount: Int!
 }
 
-enum FeaturedVideoOrderByInput {
+input WorkerWhereInput {
+  metadata_contains: String
+  isActive_eq: Boolean
+  type_eq: WorkerType
+}
+
+input WorkerWhereUniqueInput {
+  id: ID!
+}
+
+enum WorkerOrderByInput {
   createdAt_ASC
   createdAt_DESC
 }
+# Isn't provided by query node yet
+type CoverVideo {
+  id: ID!
+  video: Video!
+  coverDescription: String!
+  coverCutMediaMetadata: VideoMediaMetadata!
+  coverCutMediaDataObject: DataObject
+  coverCutMediaAvailability: AssetAvailability!
+  coverCutMediaUrl: String
+}
+
+union SearchResult = Video | Channel
+
+type SearchFTSOutput {
+  item: SearchResult!
+  rank: Float!
+  isTypeOf: String!
+  highlight: String!
+}
+
+type ProcessorState {
+  lastCompleteBlock: Float!
+  lastProcessedEvent: String!
+  indexerHead: Float!
+  chainHead: Float!
+}
 
 type Query {
+  # Lookup a membership by its ID
+  membershipByUniqueInput(where: MembershipWhereUniqueInput!): Membership
+
+  # Lookup all memberships by account ID
+  memberships(where: MembershipWhereInput!): [Membership!]!
+
   # Lookup a channel by its ID
-  channel(where: ChannelWhereUniqueInput!): Channel
+  channelByUniqueInput(where: ChannelWhereUniqueInput!): Channel
 
   # List all channels by given constraints
   channels(where: ChannelWhereInput): [Channel!]!
@@ -252,27 +285,29 @@ type Query {
     orderBy: ChannelOrderByInput
   ): ChannelConnection!
 
-  # Lookup a channel by its ID
-  category(where: CategoryWhereUniqueInput!): Category
-
-  # List all categories
-  categories: [Category!]!
-
   # Lookup video by its ID
-  video(where: VideoWhereUniqueInput!): Video
+  videoByUniqueInput(where: VideoWhereUniqueInput!): Video
 
   # Lookup videos by where params
-  videos(where: VideoWhereInput): [Video!]!
+  videos(offset: Int, limit: Int, where: VideoWhereInput, orderBy: VideoOrderByInput): [Video!]
 
   # List all videos by given constraints
   videosConnection(first: Int, after: String, where: VideoWhereInput, orderBy: VideoOrderByInput): VideoConnection!
 
+  # List all categories
+  videoCategories: [VideoCategory!]!
+
+  workers(offset: Int, limit: Int, where: WorkerWhereInput): [Worker!]
+
+  workerByUniqueInput(where: WorkerWhereUniqueInput!): Worker
+
   # Get the current cover video
   coverVideo: CoverVideo!
 
-  # List all top trending videos
-  featuredVideos(orderBy: FeaturedVideoOrderByInput): [FeaturedVideo!]!
-
   # Free text search across videos and channels
   search(limit: Int, text: String!): [SearchFTSOutput!]!
 }
+
+type Subscription {
+  stateSubscription: ProcessorState!
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 39 - 0
src/assets/account-creation.svg


+ 11 - 0
src/assets/avatar-silhouette.svg

@@ -0,0 +1,11 @@
+<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="68" cy="68" r="68" fill="#181C20"/>
+<mask id="avatar-silhouette" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="136" height="136">
+<circle cx="68" cy="68" r="68" fill="#181C20"/>
+</mask>
+<g mask="url(#avatar-silhouette)">
+<ellipse cx="68" cy="54.4" rx="24.1778" ry="24.1778" fill="#424E57"/>
+<path d="M25.6889 120.889C25.6889 104.198 39.2199 90.6667 55.9111 90.6667H80.0889C96.7802 90.6667 110.311 104.198 110.311 120.889V179.822C110.311 196.514 96.7802 210.044 80.0889 210.044H55.9111C39.2199 210.044 25.6889 196.514 25.6889 179.822V120.889Z" fill="#424E57"/>
+</g>
+<circle cx="68" cy="68" r="67" stroke="#181C20" stroke-width="2"/>
+</svg>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
src/assets/bg-pattern.svg


+ 22 - 0
src/assets/coins.svg

@@ -0,0 +1,22 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1152 1264">
+  <defs/>
+  <ellipse cx="584.87" cy="564.889" fill="#000" rx="462.243" ry="460.956" transform="rotate(-15 584.87 564.889)"/>
+  <ellipse cx="584.871" cy="564.888" fill="#C4C3C6" rx="405.59" ry="404.302" transform="rotate(-15 584.871 564.888)"/>
+  <path fill="#fff" fill-rule="evenodd" d="M200.413 694.563a414.411 414.411 0 01-6.746-22.588c-58.104-216.848 70.194-439.637 286.563-497.613 216.368-57.976 438.872 70.816 496.976 287.664a414.313 414.313 0 015.452 22.935c-67.029-203.103-281.485-321.009-490.231-265.076-208.745 55.933-335.516 265.271-292.014 474.678z" clip-rule="evenodd"/>
+  <path fill="#000" fill-rule="evenodd" d="M542.083 1018.91a405.675 405.675 0 01-23.151-3.42C299.475 976.574 152.826 766.781 191.381 546.9c38.556-219.881 247.717-366.586 467.174-327.675a409.66 409.66 0 0122.92 4.742C470.642 198.911 274.65 342.934 237.452 555.069c-37.197 212.134 97.991 414.878 304.631 463.841z" clip-rule="evenodd"/>
+  <path fill="#000" fill-rule="evenodd" d="M445.411 986.533C328.849 876.738 274.37 511.565 384.38 384.904 497.033 255.2 689.495 238.048 814.255 346.593c25.179 21.907 45.676 47.259 61.429 74.84a290.038 290.038 0 00-40.974-43.841c-124.76-108.545-347.485 26.404-294.132 197.524 78.918 253.118-58.043 333.88 38.147 409.981l-133.314 1.436z" clip-rule="evenodd"/>
+  <path fill="#fff" fill-rule="evenodd" d="M874.811 412.55a323.426 323.426 0 019.834 15.838c89.294 153.499 35.36 350.322-120.466 439.617-155.827 89.295-354.536 37.246-443.831-116.253a321.44 321.44 0 01-13.692-26.235c93.402 141.152 283.658 186.565 433.996 100.415C887.731 741.65 944.036 561.575 874.811 412.55z" clip-rule="evenodd"/>
+  <path fill="#000" d="M659.831 342.185l-169.145 45.322 15.403 57.486 103.178-27.647 63.373 236.511c5.062 18.889-2.378 38.486-18.157 50.636l-52.594 42.211 34.484 57.655 71.536-56.091c18.942-13.877 32.522-34.239 36.544-56.441 2.839-13.084 1.856-26.902-1.445-39.222l-83.177-310.42z"/>
+  <path fill="#C4C3C6" d="M631.493 365.774L472.298 408.43l14.782 55.167 97.109-26.021 60.817 226.971c4.857 18.128-2.063 36.876-16.878 48.448l-34.991 25.422 32.764 55.418 52.778-38.638c17.788-13.213 30.491-32.666 34.179-53.926 2.615-12.527 1.625-25.775-1.543-37.598l-79.822-297.899z"/>
+  <path fill="#fff" d="M597.298 745.513l-10.555-17.105 37.029-25.616c15.693-11.64 23.263-30.192 18.512-47.921l-59.481-221.988-101.965 27.321-5.517-20.591 119.396-31.992 64.686 241.412c2.567 9.58 3.156 20.374.538 30.66-3.774 17.438-15.566 33.604-31.764 44.79l-30.879 21.03z"/>
+  <path fill="#000" d="M445.824 411.414c151.744 131.53 131.517 391.544-34.352 581.808-78.433 89.968-178.764 152.098-281.515 175.828-43.48 10.04-84.41 9.25-122.268.04-25.805-6.28-44.343-14.35-65.11-25.82 0 0-93.525-39.84-141.107-74.53-49.313-35.95-92.245-104.402-92.245-104.402-80.69-140.742-43.894-348.872 94.947-508.133 165.87-190.263 421.279-246.423 573.023-114.893 19.189 16.633 46.689 51.087 68.627 70.102z"/>
+  <path fill="#C4C3C6" fill-rule="evenodd" d="M-37.274 1093.6c53.835 14.38 202.044-18.91 310.694-114.264 100.806-88.468 164.212-235.294 169.732-324.283 6.499-104.759-57.462-226.039-81.086-257.075 21.626 9.324 36.859 35.221 55.033 50.92 127.496 110.142 109.5 338.165-40.196 509.303-73.761 84.329-165.735 139.109-255.317 159.809-79.179 18.3-104.357 5.76-122.075-3.98-31.324-17.23-36.785-20.43-36.785-20.43z" clip-rule="evenodd"/>
+  <ellipse cx="71.038" cy="691.904" fill="#C4C3C6" rx="378.55" ry="342.498" transform="rotate(-15 71.038 691.904)"/>
+  <ellipse cx="71.038" cy="691.904" fill="#C4C3C6" rx="378.55" ry="342.498" transform="rotate(-15 71.038 691.904)"/>
+  <path fill="#fff" fill-rule="evenodd" d="M-161.879 1026.56c128.65 63.42 314.471 7.85 443.039-141.611 146.483-170.291 163.175-397.987 37.283-508.574a229.492 229.492 0 00-21.707-16.88 229.583 229.583 0 0150.139 33.508c125.892 110.587 109.2 338.283-37.283 508.574-138.39 160.883-343.115 212.973-471.471 124.983z" clip-rule="evenodd"/>
+  <path fill="#fff" fill-rule="evenodd" d="M264.529 399.156c-113.987-49.472-271.726 6.965-375.655 142.495-118.411 154.415-122.132 353.027-8.311 443.612a199.501 199.501 0 0019.523 13.718 199.11 199.11 0 01-44.836-26.922c-113.821-90.585-110.101-289.197 8.31-443.612 111.868-145.884 286.082-200.128 400.969-129.291z" clip-rule="evenodd"/>
+  <path fill="#000" fill-rule="evenodd" d="M25.898 991.077c75.167-22.988 149.965-77.108 206.238-156.664 111.48-157.603 108.04-355.348-7.682-441.675-19.449-14.509-40.831-24.947-63.477-31.525 44.784 4.299 86.124 20.917 119.683 50.8 107.987 96.16 94.514 293.27-30.094 440.258-64.448 76.022-146.002 123.747-224.668 138.806z" clip-rule="evenodd"/>
+  <path fill="#000" d="M226.374 535.349l-20.517-31.244L38.82 484.589 6.818 505.411l2.735 28.422 77.02 23.689L-9.01 730.097c-31.634 57.118 0 0 0 0s-17.134 41.333-39.797 47.405c-22.664 6.073-47.385 4.463-47.385 4.463-9.987 17.706 62.93 35.65 40.48 32.504l-51.344-9.587-23.219 31.19 9.36 34.932 77.106 16.577c26.267 4.913 53.063.343 73.365-13.554 12.392-7.963 22.154-19.528 28.82-31.71l167.997-306.968z"/>
+  <path fill="#C4C3C6" d="M207.094 503.192L19.54 452.432l-29.266 49.244 113.283 35.601L-21.482 758.96c-9.986 17.705-31.06 26.498-53.51 23.353l-51.342-9.588-13.86 66.122 77.106 16.577c26.267 4.913 53.063.344 73.365-13.554 12.392-7.963 22.154-19.528 28.821-31.71l167.996-306.968z"/>
+  <path fill="#fff" d="M-119.73 845.572l2.05-9.758 68.854 12.827c30.108 4.206 58.372-7.584 71.772-31.31l167.776-297.063-151.914-47.656 6.617-11.119 170.682 45.488L46.533 813.587c-6.73 12.168-16.57 23.726-29.045 31.698-20.441 13.91-47.39 18.528-73.786 13.685l-63.432-13.398z"/>
+</svg>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 13 - 0
src/assets/empty-videos-illustration.svg


+ 5 - 0
src/assets/joystream-logo.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40">
+  <defs/>
+  <rect width="40" height="40" fill="#4038FF" rx="20"/>
+  <path fill="#fff" fill-rule="evenodd" d="M22.5 9h1.96v9.25c0 2.25-.84 4.3-2.23 5.87.17-.75.27-1.53.27-2.33V9zm-6.41 16.12a5.3 5.3 0 01-4.05 1.97l.59-1.97h3.46zm1.17-3.34v-.2h-3.6l-.58 1.97h3.88c.2-.55.3-1.15.3-1.77zM26.03 9H28v5.71c0 2.25-.84 4.3-2.23 5.87.17-.75.26-1.53.26-2.33V9zm-5.19 12.8V9h-1.97v12.8a6.9 6.9 0 01-6.89 6.9h-.4L11 30.65h.98a8.87 8.87 0 008.86-8.86z" clip-rule="evenodd"/>
+</svg>

+ 5 - 0
src/assets/polkadot-logo.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="15 15 140 140" style="zoom:1">
+  <defs/>
+  <circle cx="85" cy="85" r="70" fill="#ff8c00"/>
+  <path fill="#fff" d="M85 34.7c-20.8 0-37.8 16.9-37.8 37.8 0 4.2.7 8.3 2 12.3.9 2.7 3.9 4.2 6.7 3.3 2.7-.9 4.2-3.9 3.3-6.7-1.1-3.1-1.6-6.4-1.5-9.7.4-14.1 11.8-25.7 25.9-26.4 15.7-.8 28.7 11.7 28.7 27.2 0 14.5-11.4 26.4-25.7 27.2 0 0-5.3.3-7.9.7-1.3.2-2.3.4-3 .5-.3.1-.6-.2-.5-.5l.9-4.4L81 73.4c.6-2.8-1.2-5.6-4-6.2-2.8-.6-5.6 1.2-6.2 4 0 0-11.8 55-11.9 55.6-.6 2.8 1.2 5.6 4 6.2 2.8.6 5.6-1.2 6.2-4 .1-.6 1.7-7.9 1.7-7.9 1.2-5.6 5.8-9.7 11.2-10.4 1.2-.2 5.9-.5 5.9-.5a37.84 37.84 0 0034.9-37.7c0-20.9-17-37.8-37.8-37.8zm2.7 87a6.3 6.3 0 00-7.5 4.9 6.3 6.3 0 004.9 7.5c3.4.7 6.8-1.4 7.5-4.9.7-3.5-1.4-6.8-4.9-7.5z"/>
+</svg>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 3 - 0
src/assets/signin-illustration.svg


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 65 - 0
src/assets/theater-mask.svg


BIN
src/assets/tile-example.png


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 30 - 0
src/assets/transaction-illustration.svg


BIN
src/assets/video-example.png


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 16 - 0
src/assets/well-blue.svg


+ 4 - 3
src/components/BackgroundPattern.tsx

@@ -2,7 +2,7 @@ import React from 'react'
 import styled from '@emotion/styled'
 
 import { ReactComponent as BackgroundPatternSVG } from '@/assets/bg-pattern.svg'
-import { breakpoints, zIndex, transitions } from '@/shared/theme'
+import { zIndex, transitions, media } from '@/shared/theme'
 
 const PATTERN_OFFSET = {
   SMALL: '-150px',
@@ -26,12 +26,13 @@ const StyledBackgroundPattern = styled(BackgroundPatternSVG)`
   position: absolute;
   transform: scale(1.3);
 
-  @media screen and (min-width: ${breakpoints.small}) {
+  ${media.small} {
     display: block;
     top: 73px;
     right: ${PATTERN_OFFSET.SMALL};
   }
-  @media screen and (min-width: ${breakpoints.medium}) {
+
+  ${media.medium} {
     right: ${PATTERN_OFFSET.MEDIUM};
   }
 `

+ 7 - 12
src/components/ChannelGallery.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 import styled from '@emotion/styled'
 
-import { ChannelPreviewBase, Gallery } from '@/shared/components'
+import { Gallery } from '@/shared/components'
 import ChannelPreview from './ChannelPreview'
 import { sizes } from '@/shared/theme'
 import { BasicChannelFieldsFragment } from '@/api/queries'
@@ -15,24 +15,19 @@ type ChannelGalleryProps = {
 
 const PLACEHOLDERS_COUNT = 12
 
-const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, channels, loading, onChannelClick }) => {
+const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, channels = [], loading, onChannelClick }) => {
   if (!loading && channels?.length === 0) {
     return null
   }
 
-  const handleClick = (id: string) => {
-    if (onChannelClick) {
-      onChannelClick(id)
-    }
-  }
+  const createClickHandler = (id?: string) => () => id && onChannelClick && onChannelClick(id)
 
+  const placeholderItems = Array.from({ length: loading ? PLACEHOLDERS_COUNT : 0 }, () => ({ id: undefined }))
   return (
     <Gallery title={title} itemWidth={220} exactWidth={true} paddingLeft={sizes(2, true)} paddingTop={sizes(2, true)}>
-      {loading
-        ? Array.from({ length: PLACEHOLDERS_COUNT }).map((_, idx) => (
-            <ChannelPreviewBase key={`channel-placeholder-${idx}`} />
-          ))
-        : channels?.map(({ id }) => <StyledChannelPreview id={id} key={id} onClick={() => handleClick(id)} />)}
+      {[...channels, ...placeholderItems].map((channel, idx) => (
+        <StyledChannelPreview key={idx} id={channel.id} onClick={createClickHandler(channel.id)} />
+      ))}
     </Gallery>
   )
 }

+ 12 - 11
src/components/ChannelLink/ChannelLink.tsx

@@ -1,9 +1,10 @@
 import React from 'react'
 import Avatar, { AvatarSize } from '@/shared/components/Avatar'
-import routes from '@/config/routes'
+import { absoluteRoutes } from '@/config/routes'
 import { Container, Handle, HandlePlaceholder } from './ChannelLink.style'
 import { useBasicChannel } from '@/api/hooks'
 import { BasicChannelFieldsFragment } from '@/api/queries'
+import { useAsset } from '@/hooks'
 
 type ChannelLinkProps = {
   id?: string
@@ -26,22 +27,22 @@ const ChannelLink: React.FC<ChannelLinkProps> = ({
   className,
 }) => {
   const { channel } = useBasicChannel(id || '', { fetchPolicy: 'cache-first', skip: !id })
+  const { getAssetUrl } = useAsset()
 
   const displayedChannel = overrideChannel || channel
 
+  const avatarPhotoUrl = getAssetUrl(
+    displayedChannel?.avatarPhotoAvailability,
+    displayedChannel?.avatarPhotoUrls,
+    displayedChannel?.avatarPhotoDataObject
+  )
+
   return (
-    <Container to={routes.channel(id)} disabled={!id || noLink} className={className}>
-      {!hideAvatar && (
-        <Avatar
-          handle={displayedChannel?.handle}
-          imageUrl={displayedChannel?.avatarPhotoUrl}
-          loading={!displayedChannel}
-          size={avatarSize}
-        />
-      )}
+    <Container to={absoluteRoutes.viewer.channel(id)} disabled={!id || noLink} className={className}>
+      {!hideAvatar && <Avatar imageUrl={avatarPhotoUrl} loading={!displayedChannel} size={avatarSize} />}
       {!hideHandle &&
         (displayedChannel ? (
-          <Handle withAvatar={!hideAvatar}>{displayedChannel.handle}</Handle>
+          <Handle withAvatar={!hideAvatar}>{displayedChannel.title}</Handle>
         ) : (
           <HandlePlaceholder withAvatar={!hideAvatar} height={16} width={150} />
         ))}

+ 13 - 5
src/components/ChannelPreview.tsx

@@ -1,8 +1,9 @@
 import React from 'react'
-import ChannelPreviewBase from '../shared/components/ChannelPreview/ChannelPreviewBase'
+import { ChannelPreviewBase } from '@/shared/components'
 import { useChannel } from '@/api/hooks'
 import { useChannelVideoCount } from '@/api/hooks/channel'
-import routes from '@/config/routes'
+import { absoluteRoutes } from '@/config/routes'
+import { useAsset } from '@/hooks'
 
 type ChannelPreviewProps = {
   id?: string
@@ -17,12 +18,19 @@ export const ChannelPreview: React.FC<ChannelPreviewProps> = ({ id, className, o
     skip: !id,
   })
   const isLoading = loading || id === undefined
+  const { getAssetUrl } = useAsset()
+
+  const avatarPhotoUrl = getAssetUrl(
+    channel?.avatarPhotoAvailability,
+    channel?.avatarPhotoUrls,
+    channel?.avatarPhotoDataObject
+  )
   return (
     <ChannelPreviewBase
       className={className}
-      avatarUrl={channel?.avatarPhotoUrl ?? undefined}
-      handle={channel?.handle}
-      channelHref={id ? routes.channel(id) : undefined}
+      avatarUrl={avatarPhotoUrl}
+      title={channel?.title}
+      channelHref={id ? absoluteRoutes.viewer.channel(id) : undefined}
       videoCount={videoCount}
       loading={isLoading}
       onClick={onClick}

+ 41 - 35
src/components/CoverVideo/CoverVideo.style.ts

@@ -1,9 +1,8 @@
 import styled from '@emotion/styled'
 import { darken, fluidRange } from 'polished'
-
-import { Button, Placeholder, Text } from '@/shared/components'
-import { breakpoints, colors, sizes } from '@/shared/theme'
-import { css, keyframes } from '@emotion/core'
+import { Button, IconButton, Placeholder, Text } from '@/shared/components'
+import { breakpoints, colors, sizes, media } from '@/shared/theme'
+import { css, keyframes } from '@emotion/react'
 import ChannelLink from '../ChannelLink'
 
 const CONTENT_OVERLAP_MAP = {
@@ -23,19 +22,23 @@ export const Container = styled.section`
 
   // because of the fixed aspect ratio, as the viewport width grows, the media will occupy more height as well
   // so that the media doesn't take too big of a portion of the space, we let the content overlap the media via a negative margin
-  @media screen and (min-width: ${breakpoints.small}) {
+  ${media.small} {
     margin-bottom: -${CONTENT_OVERLAP_MAP.SMALL}px;
   }
-  @media screen and (min-width: ${breakpoints.medium}) {
+
+  ${media.medium} {
     margin-bottom: -${CONTENT_OVERLAP_MAP.MEDIUM}px;
   }
-  @media screen and (min-width: ${breakpoints.large}) {
+
+  ${media.large} {
     margin-bottom: -${CONTENT_OVERLAP_MAP.LARGE}px;
   }
-  @media screen and (min-width: ${breakpoints.xlarge}) {
+
+  ${media.xlarge} {
     margin-bottom: -${CONTENT_OVERLAP_MAP.XLARGE}px;
   }
-  @media screen and (min-width: ${breakpoints.xxlarge}) {
+
+  ${media.xxlarge} {
     margin-bottom: -${CONTENT_OVERLAP_MAP.XXLARGE}px;
   }
 `
@@ -84,7 +87,7 @@ export const HorizontalGradientOverlay = styled.div`
   display: none;
   background: linear-gradient(90deg, rgba(0, 0, 0, 0.8) 11.76%, rgba(0, 0, 0, 0) 100%);
 
-  @media screen and (min-width: ${breakpoints.small}) {
+  ${media.small} {
     display: block;
   }
 `
@@ -95,7 +98,8 @@ export const VerticalGradientOverlay = styled.div`
   // as the content overlaps the media more and more as the viewport width grows, we need to hide some part of the media with a gradient
   // this helps with keeping a consistent background behind a page content - we don't want the media to peek out in the content spacing
   background: linear-gradient(0deg, black 0%, rgba(0, 0, 0, 0) ${GRADIENT_HEIGHT / 2}px);
-  @media screen and (min-width: ${breakpoints.small}) {
+
+  ${media.small} {
     background: linear-gradient(
       0deg,
       black 0%,
@@ -103,7 +107,8 @@ export const VerticalGradientOverlay = styled.div`
       rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.SMALL - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
     );
   }
-  @media screen and (min-width: ${breakpoints.medium}) {
+
+  ${media.medium} {
     background: linear-gradient(
       0deg,
       black 0%,
@@ -111,7 +116,8 @@ export const VerticalGradientOverlay = styled.div`
       rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.MEDIUM - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
     );
   }
-  @media screen and (min-width: ${breakpoints.large}) {
+
+  ${media.large} {
     background: linear-gradient(
       0deg,
       black 0%,
@@ -119,7 +125,8 @@ export const VerticalGradientOverlay = styled.div`
       rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.LARGE - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
     );
   }
-  @media screen and (min-width: ${breakpoints.xlarge}) {
+
+  ${media.xlarge} {
     background: linear-gradient(
       0deg,
       black 0%,
@@ -127,7 +134,8 @@ export const VerticalGradientOverlay = styled.div`
       rgba(0, 0, 0, 0) ${CONTENT_OVERLAP_MAP.XLARGE - GRADIENT_OVERLAP + GRADIENT_HEIGHT}px
     );
   }
-  @media screen and (min-width: ${breakpoints.xxlarge}) {
+
+  ${media.xxlarge} {
     background: linear-gradient(
       0deg,
       black 0%,
@@ -142,26 +150,26 @@ export const InfoContainer = styled.div<{ isLoading: boolean }>`
   margin-top: -${sizes(8)};
   padding-bottom: ${sizes(12)};
 
-  @media screen and (min-width: ${breakpoints.small}) {
+  ${media.small} {
     position: absolute;
     margin: 0;
     padding-bottom: 0;
     bottom: ${CONTENT_OVERLAP_MAP.SMALL + INFO_BOTTOM_MARGIN / 4}px;
   }
 
-  @media screen and (min-width: ${breakpoints.medium}) {
+  ${media.medium} {
     bottom: ${CONTENT_OVERLAP_MAP.MEDIUM + INFO_BOTTOM_MARGIN / 2}px;
   }
 
-  @media screen and (min-width: ${breakpoints.large}) {
+  ${media.large} {
     bottom: ${CONTENT_OVERLAP_MAP.LARGE + INFO_BOTTOM_MARGIN}px;
   }
 
-  @media screen and (min-width: ${breakpoints.xlarge}) {
+  ${media.xlarge} {
     bottom: ${CONTENT_OVERLAP_MAP.XLARGE + INFO_BOTTOM_MARGIN}px;
   }
 
-  @media screen and (min-width: ${breakpoints.xxlarge}) {
+  ${media.xxlarge} {
     bottom: ${CONTENT_OVERLAP_MAP.XXLARGE + INFO_BOTTOM_MARGIN}px;
   }
 `
@@ -175,7 +183,8 @@ export const TitleContainer = styled.div`
     text-decoration: none;
   }
   margin-bottom: ${sizes(8)};
-  @media screen and (min-width: ${breakpoints.medium}) {
+
+  ${media.medium} {
     margin-bottom: ${sizes(10)};
   }
 
@@ -194,14 +203,16 @@ export const Title = styled(Text)`
 
   display: inline-block;
   margin-bottom: ${sizes(4)};
-  @media screen and (min-width: ${breakpoints.medium}) {
+
+  ${media.medium} {
     margin-bottom: ${sizes(5)};
   }
 `
 
 export const TitlePlaceholder = styled(Placeholder)`
   margin-bottom: ${sizes(4)};
-  @media screen and (min-width: ${breakpoints.medium}) {
+
+  ${media.medium} {
     margin-bottom: ${sizes(5)};
   }
 `
@@ -210,22 +221,17 @@ export const ControlsContainer = styled.div`
   min-height: ${BUTTONS_HEIGHT_PX};
 `
 
-export const PlayButton = styled(Button)<{ playing: boolean }>`
+export const ButtonsContainer = styled.div`
+  display: flex;
+`
+
+export const PlayButton = styled(Button)`
   width: 140px;
   height: ${BUTTONS_HEIGHT_PX};
-  justify-content: flex-start;
-
-  svg {
-    margin-left: ${sizes(3)};
-  }
-
-  span {
-    margin-top: -3px;
-    margin-left: ${({ playing }) => (playing ? sizes(2) : sizes(3))};
-  }
 `
 
-export const SoundButton = styled(Button)`
+export const SoundButton = styled(IconButton)`
   margin-left: ${sizes(4)};
   height: ${BUTTONS_HEIGHT_PX};
+  width: ${BUTTONS_HEIGHT_PX};
 `

+ 25 - 8
src/components/CoverVideo/CoverVideo.tsx

@@ -15,13 +15,16 @@ import {
   TitlePlaceholder,
   PlayerPlaceholder,
   ControlsContainer,
+  ButtonsContainer,
 } from './CoverVideo.style'
 import { CSSTransition } from 'react-transition-group'
-import routes from '@/config/routes'
+import { absoluteRoutes } from '@/config/routes'
 import { Placeholder, VideoPlayer } from '@/shared/components'
 import { Link } from 'react-router-dom'
 import { transitions } from '@/shared/theme'
 import { useCoverVideo } from '@/api/hooks'
+import { SvgPlayerPause, SvgPlayerPlay, SvgPlayerSoundOff, SvgPlayerSoundOn } from '@/shared/icons'
+import { useAsset } from '@/hooks'
 
 const VIDEO_PLAYBACK_DELAY = 1250
 
@@ -31,6 +34,7 @@ const CoverVideo: React.FC = () => {
   const [videoPlaying, setVideoPlaying] = useState(false)
   const [displayControls, setDisplayControls] = useState(false)
   const [soundMuted, setSoundMuted] = useState(true)
+  const { getAssetUrl } = useAsset()
 
   const handlePlaybackDataLoaded = () => {
     setTimeout(() => {
@@ -55,6 +59,13 @@ const CoverVideo: React.FC = () => {
     setVideoPlaying(false)
   }
 
+  const thumbnailPhotoUrl = getAssetUrl(
+    data.video?.thumbnailPhotoAvailability,
+    data.video?.thumbnailPhotoUrls,
+    data.video?.thumbnailPhotoDataObject
+  )
+  const mediaUrl = getAssetUrl(data.video?.mediaAvailability, data.video?.mediaUrls, data.video?.mediaDataObject)
+
   return (
     <Container>
       <MediaWrapper>
@@ -66,11 +77,11 @@ const CoverVideo: React.FC = () => {
                 isInBackground
                 muted={soundMuted}
                 playing={videoPlaying}
-                posterUrl={data.video.thumbnailUrl}
+                posterUrl={thumbnailPhotoUrl}
                 onDataLoaded={handlePlaybackDataLoaded}
                 onPlay={handlePlay}
                 onPause={handlePause}
-                src={data.coverCutMedia.location}
+                src={mediaUrl}
               />
             ) : (
               <PlayerPlaceholder />
@@ -90,7 +101,7 @@ const CoverVideo: React.FC = () => {
         <TitleContainer>
           {data ? (
             <>
-              <Link to={routes.video(data.video.id)}>
+              <Link to={absoluteRoutes.viewer.video(data.video.id)}>
                 <Title variant="h2">{data.video.title}</Title>
               </Link>
               <span>{data.coverDescription}</span>
@@ -111,12 +122,18 @@ const CoverVideo: React.FC = () => {
             unmountOnExit
             appear
           >
-            <div>
-              <PlayButton onClick={handlePlayPauseClick} icon={videoPlaying ? 'pause' : 'play'} playing={videoPlaying}>
+            <ButtonsContainer>
+              <PlayButton
+                onClick={handlePlayPauseClick}
+                icon={videoPlaying ? <SvgPlayerPause /> : <SvgPlayerPlay />}
+                size="large"
+              >
                 {videoPlaying ? 'Pause' : 'Play'}
               </PlayButton>
-              <SoundButton onClick={handleSoundToggleClick} icon={!soundMuted ? 'sound-on' : 'sound-off'} />
-            </div>
+              <SoundButton onClick={handleSoundToggleClick} size="large">
+                {!soundMuted ? <SvgPlayerSoundOn /> : <SvgPlayerSoundOff />}
+              </SoundButton>
+            </ButtonsContainer>
           </CSSTransition>
         </ControlsContainer>
       </InfoContainer>

+ 81 - 0
src/components/Dialogs/ActionDialog/ActionDialog.stories.tsx

@@ -0,0 +1,81 @@
+import React, { useState } from 'react'
+import ActionDialog, { ActionDialogProps } from './ActionDialog'
+import { Story, Meta } from '@storybook/react'
+import { Button } from '@/shared/components'
+import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+
+export default {
+  title: 'General/ActionDialog',
+  component: ActionDialog,
+  args: {
+    showAdditionalAction: false,
+  },
+  argTypes: {
+    exitButton: { defaultValue: true },
+    primaryButtonText: { defaultValue: 'hello darkness' },
+    secondaryButtonText: { defaultValue: 'my old friend' },
+    showDialog: { table: { disable: true } },
+    additionalActionsNode: { table: { disable: true } },
+    warning: { defaultValue: false },
+    error: { defaultValue: false },
+  },
+  decorators: [
+    (Story) => (
+      <OverlayManagerProvider>
+        <Story />
+      </OverlayManagerProvider>
+    ),
+  ],
+} as Meta
+
+type StoryProps = ActionDialogProps & {
+  showAdditionalAction?: boolean
+}
+
+const additionalActionNode = (
+  <div>
+    <span>Action</span>
+  </div>
+)
+
+const content = (
+  <div>
+    <p style={{ marginTop: 0 }}>This is an example page content</p>
+    <p style={{ marginBottom: 0 }}>It consists of 2 paragraphs</p>
+  </div>
+)
+
+const RegularTemplate: Story<StoryProps> = ({ showAdditionalAction, ...args }) => {
+  return (
+    <ActionDialog {...args} showDialog={true} additionalActionsNode={showAdditionalAction && additionalActionNode} />
+  )
+}
+export const Regular = RegularTemplate.bind({})
+
+const ContentTemplate: Story<StoryProps> = ({ showAdditionalAction, ...args }) => {
+  return (
+    <ActionDialog {...args} showDialog={true} additionalActionsNode={showAdditionalAction && additionalActionNode}>
+      {content}
+    </ActionDialog>
+  )
+}
+export const WithContent = ContentTemplate.bind({})
+
+const TransitionTemplate: Story<StoryProps> = ({ showAdditionalAction, ...args }) => {
+  const [showDialog, setShowDialog] = useState(false)
+
+  return (
+    <>
+      <Button onClick={() => setShowDialog(true)}>Open Dialog</Button>
+      <ActionDialog
+        {...args}
+        onExitClick={() => setShowDialog(false)}
+        showDialog={showDialog}
+        additionalActionsNode={showAdditionalAction && additionalActionNode}
+      >
+        {content}
+      </ActionDialog>
+    </>
+  )
+}
+export const Transition = TransitionTemplate.bind({})

+ 107 - 0
src/components/Dialogs/ActionDialog/ActionDialog.style.ts

@@ -0,0 +1,107 @@
+import styled from '@emotion/styled'
+import { css } from '@emotion/react'
+import { media, sizes, colors, typography } from '@/shared/theme'
+import { Button } from '@/shared/components'
+
+type ButtonProps = {
+  error?: boolean
+  warning?: boolean
+}
+
+export const ButtonsContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+
+  > * + * {
+    margin-top: ${sizes(2)};
+  }
+
+  ${media.small} {
+    flex-direction: row-reverse;
+    margin-left: auto;
+
+    > * + * {
+      margin-top: 0;
+      margin-right: ${sizes(2)};
+    }
+
+    * + & {
+      margin-top: 0;
+    }
+  }
+`
+
+export const ActionsContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+
+  padding-top: ${sizes(6)};
+
+  ${media.small} {
+    flex-direction: row;
+    align-items: center;
+  }
+`
+
+export const AdditionalActionsContainer = styled.div`
+  width: 100%;
+  margin-bottom: ${sizes(6)};
+
+  ${media.small} {
+    margin-bottom: 0;
+    margin-right: ${sizes(6)};
+  }
+`
+
+const buttonColorsFromProps = ({ error, warning }: ButtonProps) => {
+  let color, bgColor, borderColor, bgActiveColor, borderActiveColor
+
+  if (warning) {
+    color = 'var(--warning-font-color)'
+    bgColor = 'var(--warning-bg-color)'
+    borderColor = 'var(--warning-bg-color)'
+    bgActiveColor = 'var(--warning-bg-active-color)'
+    borderActiveColor = 'var(--warning-border-active-color)'
+  }
+  if (error) {
+    color = 'var(--error-font-color)'
+    bgColor = 'var(--error-bg-color)'
+    borderColor = 'var(--error-bg-color)'
+    bgActiveColor = 'var(--error-bg-active-color)'
+    borderActiveColor = 'var(--error-border-active-color)'
+  }
+
+  const boxShadow = error || warning ? `inset 0px 0px 0px 1px ${borderActiveColor}` : 'none'
+
+  return css`
+    color: ${color};
+    background-color: ${bgColor};
+    border-color: ${borderColor};
+    &:hover {
+      color: ${color};
+      background-color: ${bgColor};
+      border-color: ${borderColor};
+      box-shadow: none;
+    }
+    &:active {
+      color: ${color};
+      background-color: ${bgActiveColor};
+      border-color: ${borderActiveColor};
+      box-shadow: ${boxShadow};
+    }
+  `
+}
+
+export const StyledPrimaryButton = styled(Button)<ButtonProps>`
+  --warning-bg-color: #f49525;
+  --warning-bg-active-color: #da7b0b;
+  --warning-border-active-color: #49290440;
+  --warning-font-color: #492904;
+
+  --error-bg-color: #e53333;
+  --error-bg-active-color: #cc1a1a;
+  --error-border-active-color: #44090966;
+  --error-font-color: #440909;
+
+  ${buttonColorsFromProps}
+`

+ 67 - 0
src/components/Dialogs/ActionDialog/ActionDialog.tsx

@@ -0,0 +1,67 @@
+import React from 'react'
+import BaseDialog, { BaseDialogProps } from '../BaseDialog'
+import {
+  ActionsContainer,
+  ButtonsContainer,
+  AdditionalActionsContainer,
+  StyledPrimaryButton,
+} from './ActionDialog.style'
+import { Button } from '@/shared/components'
+
+export type ActionDialogProps = {
+  additionalActionsNode?: React.ReactNode
+  primaryButtonText?: string
+  secondaryButtonText?: string
+  primaryButtonDisabled?: boolean
+  secondaryButtonDisabled?: boolean
+  onPrimaryButtonClick?: (e: React.MouseEvent) => void
+  onSecondaryButtonClick?: (e: React.MouseEvent) => void
+  warning?: boolean
+  error?: boolean
+} & BaseDialogProps
+
+const ActionDialog: React.FC<ActionDialogProps> = ({
+  additionalActionsNode,
+  primaryButtonText,
+  secondaryButtonText,
+  primaryButtonDisabled,
+  secondaryButtonDisabled,
+  onPrimaryButtonClick,
+  onSecondaryButtonClick,
+  warning,
+  error,
+  children,
+  ...baseDialogProps
+}) => {
+  const hasAnyAction = additionalActionsNode || primaryButtonText || secondaryButtonText
+
+  return (
+    <BaseDialog {...baseDialogProps}>
+      {children}
+      {hasAnyAction && (
+        <ActionsContainer>
+          {additionalActionsNode && <AdditionalActionsContainer>{additionalActionsNode}</AdditionalActionsContainer>}
+          <ButtonsContainer>
+            {primaryButtonText && (
+              <StyledPrimaryButton
+                onClick={onPrimaryButtonClick}
+                warning={warning}
+                error={error}
+                disabled={primaryButtonDisabled}
+              >
+                {primaryButtonText}
+              </StyledPrimaryButton>
+            )}
+            {secondaryButtonText && (
+              <Button variant="secondary" onClick={onSecondaryButtonClick} disabled={secondaryButtonDisabled}>
+                {secondaryButtonText}
+              </Button>
+            )}
+          </ButtonsContainer>
+        </ActionsContainer>
+      )}
+    </BaseDialog>
+  )
+}
+
+export default ActionDialog

+ 4 - 0
src/components/Dialogs/ActionDialog/index.ts

@@ -0,0 +1,4 @@
+import ActionDialog, { ActionDialogProps } from './ActionDialog'
+
+export default ActionDialog
+export type { ActionDialogProps }

+ 39 - 0
src/components/Dialogs/BaseDialog/BaseDialog.stories.tsx

@@ -0,0 +1,39 @@
+import React, { useState } from 'react'
+import Dialog, { BaseDialogProps } from './BaseDialog'
+import { Story, Meta } from '@storybook/react'
+import { Button } from '@/shared/components'
+import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+
+export default {
+  title: 'General/BaseDialog',
+  component: Dialog,
+  argTypes: {
+    exitButton: { defaultValue: true },
+  },
+  decorators: [
+    (Story) => (
+      <OverlayManagerProvider>
+        <Story />
+      </OverlayManagerProvider>
+    ),
+  ],
+} as Meta
+
+const RegularTemplate: Story<BaseDialogProps> = ({ exitButton }) => {
+  return <Dialog exitButton={exitButton} showDialog={true} />
+}
+
+export const Regular = RegularTemplate.bind({})
+
+const TransitionTemplate: Story<BaseDialogProps> = ({ exitButton }) => {
+  const [showDialog, setShowDialog] = useState(false)
+
+  return (
+    <>
+      <Button onClick={() => setShowDialog(true)}>Open Dialog</Button>
+      <Dialog exitButton={exitButton} onExitClick={() => setShowDialog(false)} showDialog={showDialog} />
+    </>
+  )
+}
+
+export const Transition = TransitionTemplate.bind({})

+ 32 - 0
src/components/Dialogs/BaseDialog/BaseDialog.style.ts

@@ -0,0 +1,32 @@
+import styled from '@emotion/styled'
+import { IconButton } from '@/shared/components'
+import { colors, sizes, media } from '@/shared/theme'
+
+export const StyledContainer = styled.div`
+  --dialog-padding: ${sizes(4)};
+  ${media.small} {
+    --dialog-padding: ${sizes(6)};
+  }
+
+  position: relative;
+  width: 90%;
+  max-width: 440px;
+  min-height: 150px;
+  max-height: 75vh;
+  overflow: auto;
+  margin: ${sizes(16)} auto;
+  color: ${colors.white};
+  background-color: ${colors.gray[700]};
+  padding: var(--dialog-padding);
+  box-shadow: 0 8px 8px rgba(0, 0, 0, 0.12), 0 24px 40px rgba(0, 0, 0, 0.16);
+
+  ${media.medium} {
+    margin: ${sizes(32)} auto;
+  }
+`
+
+export const StyledExitButton = styled(IconButton)`
+  position: absolute;
+  top: var(--dialog-padding);
+  right: var(--dialog-padding);
+`

+ 53 - 0
src/components/Dialogs/BaseDialog/BaseDialog.tsx

@@ -0,0 +1,53 @@
+import React, { useEffect } from 'react'
+import { Portal } from '@/components'
+import { useOverlayManager } from '@/hooks/useOverlayManager'
+import { CSSTransition } from 'react-transition-group'
+import { StyledContainer, StyledExitButton } from './BaseDialog.style'
+import { transitions } from '@/shared/theme'
+import { SvgGlyphClose } from '@/shared/icons'
+
+export type BaseDialogProps = {
+  showDialog?: boolean
+  exitButton?: boolean
+  onExitClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
+  className?: string
+}
+
+const BaseDialog: React.FC<BaseDialogProps> = ({ children, showDialog, exitButton = true, onExitClick, className }) => {
+  const {
+    overlayContainerRef,
+    lockScroll,
+    unlockScroll,
+    openOverlayContainer,
+    closeOverlayContainer,
+  } = useOverlayManager()
+
+  useEffect(() => {
+    if (!showDialog) {
+      return
+    }
+    lockScroll()
+    openOverlayContainer()
+    return () => {
+      unlockScroll()
+      closeOverlayContainer()
+    }
+  }, [showDialog, lockScroll, unlockScroll, openOverlayContainer, closeOverlayContainer])
+
+  return (
+    <Portal containerRef={overlayContainerRef}>
+      <CSSTransition in={showDialog} timeout={250} classNames={transitions.names.dialog} mountOnEnter unmountOnExit>
+        <StyledContainer className={className}>
+          {exitButton && (
+            <StyledExitButton aria-label="close dialog" onClick={onExitClick} variant="tertiary">
+              <SvgGlyphClose />
+            </StyledExitButton>
+          )}
+          {children}
+        </StyledContainer>
+      </CSSTransition>
+    </Portal>
+  )
+}
+
+export default BaseDialog

+ 4 - 0
src/components/Dialogs/BaseDialog/index.ts

@@ -0,0 +1,4 @@
+import BaseDialog, { BaseDialogProps } from './BaseDialog'
+
+export default BaseDialog
+export type { BaseDialogProps }

+ 102 - 0
src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx

@@ -0,0 +1,102 @@
+import React, { useState, useRef } from 'react'
+import { Story, Meta } from '@storybook/react'
+import { ImageCropData, AssetDimensions } from '@/types/cropper'
+import ImageCropDialog, { ImageCropDialogImperativeHandle, ImageCropDialogProps } from './ImageCropDialog'
+import { Avatar, Placeholder } from '@/shared/components'
+import { OverlayManagerProvider } from '@/hooks'
+import { css } from '@emotion/react'
+import styled from '@emotion/styled/'
+
+export default {
+  title: 'General/ImageCropDialog',
+  component: ImageCropDialog,
+  argTypes: {
+    showDialog: { table: { disable: true } },
+    imageType: { table: { disable: true } },
+  },
+  decorators: [
+    (Story) => (
+      <OverlayManagerProvider>
+        <Story />
+      </OverlayManagerProvider>
+    ),
+  ],
+} as Meta
+
+const RegularTemplate: Story<ImageCropDialogProps> = () => {
+  const avatarDialogRef = useRef<ImageCropDialogImperativeHandle>(null)
+  const thumbnailDialogRef = useRef<ImageCropDialogImperativeHandle>(null)
+  const coverDialogRef = useRef<ImageCropDialogImperativeHandle>(null)
+  const [avatarImageUrl, setAvatarImageUrl] = useState<string | null>(null)
+  const [thumbnailImageUrl, setThumbnailImageUrl] = useState<string | null>(null)
+  const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
+
+  const handleAvatarConfirm = (
+    blob: Blob,
+    url: string,
+    assetDimensions: AssetDimensions,
+    imageCropData: ImageCropData
+  ) => {
+    setAvatarImageUrl(url)
+  }
+
+  const handleThumbnailConfirm = (
+    blob: Blob,
+    url: string,
+    assetDimensions: AssetDimensions,
+    imageCropData: ImageCropData
+  ) => {
+    setThumbnailImageUrl(url)
+  }
+
+  const handleCoverConfirm = (
+    blob: Blob,
+    url: string,
+    assetDimensions: AssetDimensions,
+    imageCropData: ImageCropData
+  ) => {
+    setCoverImageUrl(url)
+  }
+
+  return (
+    <div
+      css={css`
+        display: flex;
+        flex-direction: column;
+        align-items: start;
+        > * {
+          margin-bottom: 24px !important;
+        }
+      `}
+    >
+      <Avatar imageUrl={avatarImageUrl} editable onEditClick={() => avatarDialogRef.current?.open()} size="cover" />
+
+      {thumbnailImageUrl ? (
+        <Image src={thumbnailImageUrl} onClick={() => thumbnailDialogRef.current?.open()} />
+      ) : (
+        <ImagePlaceholder onClick={() => thumbnailDialogRef.current?.open()} />
+      )}
+      {coverImageUrl ? (
+        <Image src={coverImageUrl} onClick={() => coverDialogRef.current?.open()} />
+      ) : (
+        <ImagePlaceholder onClick={() => coverDialogRef.current?.open()} />
+      )}
+
+      <ImageCropDialog imageType="avatar" onConfirm={handleAvatarConfirm} ref={avatarDialogRef} />
+      <ImageCropDialog imageType="videoThumbnail" onConfirm={handleThumbnailConfirm} ref={thumbnailDialogRef} />
+      <ImageCropDialog imageType="cover" onConfirm={handleCoverConfirm} ref={coverDialogRef} />
+    </div>
+  )
+}
+export const Regular = RegularTemplate.bind({})
+
+const ImagePlaceholder = styled(Placeholder)`
+  width: 600px;
+  min-height: 200px;
+  cursor: pointer;
+`
+
+const Image = styled.img`
+  width: 600px;
+  cursor: pointer;
+`

+ 95 - 0
src/components/Dialogs/ImageCropDialog/ImageCropDialog.style.ts

@@ -0,0 +1,95 @@
+import { Placeholder, Text } from '@/shared/components'
+import Slider from '@/shared/components/Slider'
+import { colors, sizes } from '@/shared/theme'
+import { css } from '@emotion/react'
+import styled from '@emotion/styled'
+import ActionDialog from '../ActionDialog'
+
+export const StyledActionDialog = styled(ActionDialog)`
+  max-width: 536px;
+`
+
+const roundedCropperCss = css`
+  .cropper-view-box,
+  .cropper-face {
+    border-radius: 50%;
+  }
+`
+
+export const HeaderContainer = styled.div`
+  padding-bottom: ${sizes(4)};
+  border-bottom: 1px solid ${colors.gray[500]};
+`
+
+export const HeaderText = styled(Text)`
+  padding: ${sizes(2.5)} 0;
+`
+
+export const AlignInfoContainer = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: ${sizes(4)} 0;
+`
+
+export const AlignInfo = styled(Text)`
+  margin-left: ${sizes(2)};
+`
+
+export const HiddenInput = styled.input`
+  opacity: 0;
+  visibility: hidden;
+  position: fixed;
+  top: -99999px;
+  left: -99999px;
+`
+
+const cropAreaSizeCss = css`
+  width: 100%;
+  height: 256px;
+`
+
+export const CropPlaceholder = styled(Placeholder)`
+  ${cropAreaSizeCss};
+`
+
+export const CropContainer = styled.div<{ rounded?: boolean }>`
+  ${cropAreaSizeCss};
+
+  ${({ rounded }) => rounded && roundedCropperCss};
+  .cropper-view-box {
+    outline: none;
+
+    ::after {
+      content: '';
+      display: block;
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      border-radius: ${({ rounded }) => (rounded ? '50%' : '0')};
+
+      box-shadow: inset 0 0 0 2px ${colors.transparentWhite[32]};
+    }
+  }
+
+  .cropper-modal {
+    background-color: ${colors.transparentBlack[54]};
+  }
+`
+
+export const StyledImage = styled.img`
+  display: block;
+
+  max-width: 100%;
+`
+
+export const ZoomControl = styled.div`
+  display: flex;
+  align-items: center;
+`
+
+export const StyledSlider = styled(Slider)`
+  margin: 0 ${sizes(4)};
+  flex: 1;
+`

+ 146 - 0
src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx

@@ -0,0 +1,146 @@
+import { IconButton } from '@/shared/components'
+import { ImageCropData, AssetDimensions } from '@/types/cropper'
+import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'
+import { ActionDialogProps } from '../ActionDialog'
+import { CropperImageType, useCropper } from './cropper'
+import {
+  AlignInfo,
+  AlignInfoContainer,
+  CropContainer,
+  HeaderContainer,
+  HeaderText,
+  HiddenInput,
+  StyledActionDialog,
+  StyledImage,
+  StyledSlider,
+  ZoomControl,
+} from './ImageCropDialog.style'
+import { SvgGlyphPan, SvgGlyphZoomIn, SvgGlyphZoomOut } from '@/shared/icons'
+import { validateImage } from '@/utils/image'
+
+export type ImageCropDialogProps = {
+  imageType: CropperImageType
+  onConfirm: (
+    croppedBlob: Blob,
+    croppedUrl: string,
+    assetDimensions: AssetDimensions,
+    imageCropData: ImageCropData
+  ) => void
+  onError?: (error: Error) => void
+} & Pick<ActionDialogProps, 'onExitClick'>
+
+export type ImageCropDialogImperativeHandle = {
+  open: (file?: File | Blob) => void
+}
+
+const ImageCropDialogComponent: React.ForwardRefRenderFunction<
+  ImageCropDialogImperativeHandle,
+  ImageCropDialogProps
+> = ({ imageType, onConfirm, onExitClick, onError }, ref) => {
+  const [showDialog, setShowDialog] = useState(false)
+  const inputRef = useRef<HTMLInputElement>(null)
+  const [imageEl, setImageEl] = useState<HTMLImageElement | null>(null)
+  const [editedImageHref, setEditedImageHref] = useState<string | null>(null)
+  const { currentZoom, zoomRange, zoomStep, handleZoomChange, cropImage } = useCropper({ imageEl, imageType })
+
+  // not great - ideally we'd have a data flow trigger this via prop change
+  // however, since there's no way to detect whether the file pick succeeds, the component wouldn't be able to report back whether it was actually opened
+  // because of that we're letting the consumer trigger the open manually
+  useImperativeHandle(ref, () => ({
+    open: (file) => {
+      if (file) {
+        const fileUrl = URL.createObjectURL(file)
+        setEditedImageHref(fileUrl)
+        setShowDialog(true)
+      } else {
+        inputRef.current?.click()
+      }
+    },
+  }))
+
+  const imageElRefCallback = (el: HTMLImageElement) => {
+    setImageEl(el)
+  }
+
+  const resetDialog = useCallback(() => {
+    setShowDialog(false)
+    setEditedImageHref(null)
+    if (inputRef.current) {
+      // clear the file input so onChange is triggered if the same file is selected again
+      inputRef.current.value = ''
+    }
+  }, [])
+
+  const handleFileChange = async () => {
+    const files = inputRef.current?.files
+    if (!files?.length) {
+      console.error('no files selected')
+      return
+    }
+    try {
+      const selectedFile = files[0]
+      await validateImage(selectedFile)
+      const fileUrl = URL.createObjectURL(selectedFile)
+      setEditedImageHref(fileUrl)
+      setShowDialog(true)
+    } catch (error) {
+      onError?.(error)
+      console.error(error)
+    }
+  }
+
+  const handleConfirmClick = async () => {
+    const [blob, url, assetDimensions, imageCropData] = await cropImage()
+    resetDialog()
+    onConfirm(blob, url, assetDimensions, imageCropData)
+  }
+
+  const zoomControlNode = (
+    <ZoomControl>
+      <IconButton variant="tertiary" onClick={() => handleZoomChange(currentZoom - zoomStep)}>
+        <SvgGlyphZoomOut />
+      </IconButton>
+      <StyledSlider
+        value={currentZoom}
+        onChange={handleZoomChange}
+        min={zoomRange[0]}
+        max={zoomRange[1]}
+        step={zoomStep}
+      />
+      <IconButton variant="tertiary" onClick={() => handleZoomChange(currentZoom + zoomStep)}>
+        <SvgGlyphZoomIn />
+      </IconButton>
+    </ZoomControl>
+  )
+
+  return (
+    <>
+      <HiddenInput type="file" accept="image/*" onChange={handleFileChange} ref={inputRef} />
+      <StyledActionDialog
+        showDialog={showDialog && !!editedImageHref}
+        primaryButtonText="Confirm"
+        onPrimaryButtonClick={handleConfirmClick}
+        onExitClick={resetDialog}
+        additionalActionsNode={zoomControlNode}
+      >
+        <HeaderContainer>
+          <HeaderText variant="h6">Crop and position</HeaderText>
+        </HeaderContainer>
+        <AlignInfoContainer>
+          <SvgGlyphPan />
+          <AlignInfo variant="body2">Drag and adjust image position</AlignInfo>
+        </AlignInfoContainer>
+        {editedImageHref && (
+          <CropContainer rounded={imageType === 'avatar'}>
+            <StyledImage src={editedImageHref} ref={imageElRefCallback} />
+          </CropContainer>
+        )}
+      </StyledActionDialog>
+    </>
+  )
+}
+
+const ImageCropDialog = forwardRef(ImageCropDialogComponent)
+ImageCropDialog.displayName = 'ImageCropDialog'
+
+export default ImageCropDialog

+ 171 - 0
src/components/Dialogs/ImageCropDialog/cropper.ts

@@ -0,0 +1,171 @@
+import { useEffect, useState } from 'react'
+import Cropper from 'cropperjs'
+import { AssetDimensions, ImageCropData } from '@/types/cropper'
+import 'cropperjs/dist/cropper.min.css'
+
+const MAX_ZOOM = 3
+
+export type CropperImageType = 'avatar' | 'videoThumbnail' | 'cover'
+
+type UseCropperOpts = {
+  imageEl: HTMLImageElement | null
+  imageType: CropperImageType
+}
+
+const ASPECT_RATIO_PER_TYPE: Record<CropperImageType, number> = {
+  avatar: 1,
+  videoThumbnail: 16 / 9,
+  cover: 4,
+}
+
+const CANVAS_OPTS_PER_TYPE: Record<CropperImageType, Cropper.GetCroppedCanvasOptions> = {
+  avatar: {
+    minWidth: 128,
+    minHeight: 128,
+    width: 256,
+    height: 256,
+    maxWidth: 1024,
+    maxHeight: 1024,
+  },
+  videoThumbnail: {
+    minWidth: 1280,
+    minHeight: 720,
+    width: 1920,
+    height: 1080,
+    maxWidth: 3840,
+    maxHeight: 2160,
+  },
+  cover: {
+    minWidth: 1920,
+    minHeight: 480,
+    width: 1920,
+    height: 480,
+    maxWidth: 3840,
+    maxHeight: 960,
+  },
+}
+
+export const useCropper = ({ imageEl, imageType }: UseCropperOpts) => {
+  const [cropper, setCropper] = useState<Cropper | null>(null)
+  const [currentZoom, setCurrentZoom] = useState(0)
+  const [zoomRange, setZoomRange] = useState<[number, number]>([0, 1])
+
+  const zoomStep = (zoomRange[1] - zoomRange[0]) / 20
+
+  const handleZoomChange = (zoom: number) => {
+    const [minZoom, maxZoom] = zoomRange
+
+    // keep zoom value in zoom range
+    const getCorrectZoomValue = (zoom: number) => {
+      if (zoom <= minZoom) {
+        return minZoom
+      }
+      if (zoom >= maxZoom) {
+        return maxZoom
+      }
+      return zoom
+    }
+
+    const correctZoom = getCorrectZoomValue(zoom)
+
+    setCurrentZoom(correctZoom)
+    cropper?.zoomTo(correctZoom)
+  }
+
+  // initialize
+  useEffect(() => {
+    if (!imageEl) {
+      return
+    }
+
+    const handleReady = (event: Cropper.ReadyEvent<HTMLImageElement>) => {
+      const { cropper } = event.currentTarget
+      const { width: cropBoxWidth, height: cropBoxHeight } = cropper.getCropBoxData()
+      const { naturalWidth: imageWidth, naturalHeight: imageHeight } = cropper.getImageData()
+
+      const minZoom = normalizeZoomValue(Math.max(cropBoxWidth / imageWidth, cropBoxHeight / imageHeight))
+      const maxZoom = normalizeZoomValue(minZoom * MAX_ZOOM)
+
+      setZoomRange([minZoom, maxZoom])
+
+      const middleZoom = minZoom + (maxZoom - minZoom) / 2
+      cropper.zoomTo(middleZoom)
+    }
+
+    const cropper = new Cropper(imageEl, {
+      viewMode: 1,
+      dragMode: 'move',
+      cropBoxResizable: false,
+      cropBoxMovable: false,
+      aspectRatio: ASPECT_RATIO_PER_TYPE[imageType],
+      guides: false,
+      center: false,
+      background: false,
+      autoCropArea: 0.9,
+      toggleDragModeOnDblclick: false,
+      ready: handleReady,
+    })
+    setCropper(cropper)
+
+    return () => {
+      cropper.destroy()
+    }
+  }, [imageEl, imageType])
+
+  // handle zoom event
+  useEffect(() => {
+    if (!imageEl) {
+      return
+    }
+
+    const handleZoomEvent = (event: Event) => {
+      const { ratio } = (event as Cropper.ZoomEvent<HTMLImageElement>).detail
+      const [minZoom, maxZoom] = zoomRange
+      const normalizedRatio = normalizeZoomValue(ratio)
+
+      if (normalizedRatio < minZoom || normalizedRatio > maxZoom) {
+        event.preventDefault()
+        return
+      }
+
+      setCurrentZoom(normalizedRatio)
+    }
+
+    imageEl.addEventListener('zoom', handleZoomEvent)
+
+    return () => {
+      imageEl.removeEventListener('zoom', handleZoomEvent)
+    }
+  }, [imageEl, zoomRange])
+
+  const cropImage = async (): Promise<[Blob, string, AssetDimensions, ImageCropData]> => {
+    return new Promise((resolve, reject) => {
+      if (!cropper) {
+        reject(new Error('No cropper instance'))
+        return
+      }
+
+      const imageCropData = cropper.getCropBoxData()
+      const canvas = cropper.getCroppedCanvas(CANVAS_OPTS_PER_TYPE[imageType])
+      const assetDimensions = {
+        width: canvas.width,
+        height: canvas.height,
+      }
+      canvas.toBlob((blob) => {
+        if (!blob) {
+          console.error('Empty blob from cropped canvas', { blob })
+          return
+        }
+        const url = URL.createObjectURL(blob)
+        resolve([blob, url, assetDimensions, imageCropData])
+      }, 'image/jpeg')
+    })
+  }
+
+  return { currentZoom, zoomRange, zoomStep, handleZoomChange, cropImage }
+}
+
+const normalizeZoomValue = (value: number) => {
+  const base = 100
+  return Math.floor(value * base) / base
+}

+ 4 - 0
src/components/Dialogs/ImageCropDialog/index.ts

@@ -0,0 +1,4 @@
+import ImageCropDialog, { ImageCropDialogProps, ImageCropDialogImperativeHandle } from './ImageCropDialog'
+
+export default ImageCropDialog
+export type { ImageCropDialogProps, ImageCropDialogImperativeHandle }

+ 35 - 0
src/components/Dialogs/MessageDialog/MessageDialog.stories.tsx

@@ -0,0 +1,35 @@
+import React from 'react'
+import MessageDialog, { MessageDialogProps } from './MessageDialog'
+import { Story, Meta } from '@storybook/react'
+import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+
+export default {
+  title: 'General/MessageDialog',
+  component: MessageDialog,
+  args: {
+    showAdditionalAction: false,
+  },
+  argTypes: {
+    title: { defaultValue: 'There is an information of the utmost importance!' },
+    description: { defaultValue: 'que me traten como dama. Aunque de eso se me olvide cuando estamos en la cama.' },
+    exitButton: { defaultValue: true },
+    primaryButtonText: { defaultValue: 'Confirm' },
+    secondaryButtonText: { defaultValue: 'Cancel' },
+    showDialog: { table: { disable: true } },
+    warning: { defaultValue: false },
+    error: { defaultValue: false },
+    variant: { control: { type: 'select', options: ['info', 'success', 'warning', 'error'] } },
+  },
+  decorators: [
+    (Story) => (
+      <OverlayManagerProvider>
+        <Story />
+      </OverlayManagerProvider>
+    ),
+  ],
+} as Meta
+
+const RegularTemplate: Story<MessageDialogProps> = ({ ...args }) => {
+  return <MessageDialog {...args} showDialog={true} />
+}
+export const Regular = RegularTemplate.bind({})

+ 18 - 0
src/components/Dialogs/MessageDialog/MessageDialog.style.ts

@@ -0,0 +1,18 @@
+import styled from '@emotion/styled'
+import { colors, sizes } from '@/shared/theme'
+import { Text } from '@/shared/components'
+
+export const MessageIconWrapper = styled.div`
+  margin-bottom: ${sizes(4)};
+`
+
+export const StyledTitleText = styled(Text)`
+  width: 90%;
+  margin-bottom: ${sizes(3)};
+  word-wrap: break-word;
+`
+
+export const StyledDescriptionText = styled(Text)`
+  color: ${colors.gray[300]};
+  word-wrap: break-word;
+`

+ 40 - 0
src/components/Dialogs/MessageDialog/MessageDialog.tsx

@@ -0,0 +1,40 @@
+import React, { ReactNode } from 'react'
+import ActionDialog, { ActionDialogProps } from '../ActionDialog/ActionDialog'
+import { StyledTitleText, StyledDescriptionText, MessageIconWrapper } from './MessageDialog.style'
+import { SvgOutlineError, SvgOutlineSuccess, SvgOutlineWarning } from '@/shared/icons'
+
+type DialogVariant = 'success' | 'warning' | 'error' | 'info'
+
+export type MessageDialogProps = {
+  variant?: DialogVariant
+  title?: string
+  description?: string
+  icon?: React.ReactElement
+} & ActionDialogProps
+
+const VARIANT_TO_ICON: Record<DialogVariant, ReactNode | null> = {
+  success: <SvgOutlineSuccess />,
+  warning: <SvgOutlineWarning />,
+  error: <SvgOutlineError />,
+  info: null,
+}
+
+const MessageDialog: React.FC<MessageDialogProps> = ({
+  title,
+  description,
+  variant = 'info',
+  icon,
+  ...actionDialogProps
+}) => {
+  const iconNode = icon || VARIANT_TO_ICON[variant]
+
+  return (
+    <ActionDialog {...actionDialogProps}>
+      {iconNode && <MessageIconWrapper>{iconNode}</MessageIconWrapper>}
+      {title && <StyledTitleText variant="h4">{title}</StyledTitleText>}
+      <StyledDescriptionText variant="body2">{description}</StyledDescriptionText>
+    </ActionDialog>
+  )
+}
+
+export default MessageDialog

+ 4 - 0
src/components/Dialogs/MessageDialog/index.ts

@@ -0,0 +1,4 @@
+import MessageDialog, { MessageDialogProps } from './MessageDialog'
+
+export default MessageDialog
+export type { MessageDialogProps }

+ 64 - 0
src/components/Dialogs/Multistepper/Multistepper.stories.tsx

@@ -0,0 +1,64 @@
+import React, { useState } from 'react'
+import Multistepper from './Multistepper'
+import { Story, Meta } from '@storybook/react'
+import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+import { Button } from '@/shared/components'
+
+export default {
+  title: 'General/Multistepper',
+  component: Multistepper,
+  argTypes: {
+    exitButton: { defaultValue: true },
+  },
+  decorators: [
+    (Story) => (
+      <OverlayManagerProvider>
+        <Story />
+      </OverlayManagerProvider>
+    ),
+  ],
+} as Meta
+type ElementType = {
+  step: string
+  handleStep: (idx: number) => void
+  currentStepIdx: number
+}
+const Element: React.FC<ElementType> = ({ step, handleStep, currentStepIdx }) => {
+  return (
+    <>
+      <h1>Hello! This is a {step} step!</h1>
+      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+        <Button onClick={() => handleStep(currentStepIdx - 1)}>Previous step</Button>
+        <Button onClick={() => handleStep(currentStepIdx + 1)}>Next step</Button>
+      </div>
+    </>
+  )
+}
+
+const RegularTemplate: Story = (args) => {
+  const [currentStepIdx, setCurrentStep] = useState(0)
+  const handleStepChange = (idx: number) => {
+    if (idx < 0 || idx > steps.length - 1) {
+      return
+    }
+    setCurrentStep(idx)
+  }
+  const steps = [
+    {
+      title: 'Add Polkadot plugin',
+      element: <Element step="first" handleStep={handleStepChange} currentStepIdx={currentStepIdx} />,
+    },
+    {
+      title: 'Create or select a polkadot account',
+      element: <Element step="second" handleStep={handleStepChange} currentStepIdx={currentStepIdx} />,
+    },
+    {
+      title: 'Get FREE tokens and start a channel',
+      element: <Element step="third" handleStep={handleStepChange} currentStepIdx={currentStepIdx} />,
+    },
+  ]
+
+  return <Multistepper showDialog={true} steps={steps} currentStepIdx={currentStepIdx} {...args} />
+}
+
+export const Regular = RegularTemplate.bind({})

+ 100 - 0
src/components/Dialogs/Multistepper/Multistepper.style.ts

@@ -0,0 +1,100 @@
+import styled from '@emotion/styled'
+import BaseDialog from '../BaseDialog'
+import { Text } from '@/shared/components'
+import { colors, sizes, media, typography } from '@/shared/theme'
+import { SvgGlyphChevronRight } from '@/shared/icons'
+
+type CircleProps = {
+  isFilled?: boolean
+  isActive?: boolean
+}
+
+type StyledStepInfoProps = {
+  isActive?: boolean
+}
+
+export const StyledDialog = styled(BaseDialog)`
+  max-width: 740px;
+`
+
+export const StyledHeader = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+
+  border-bottom: 1px solid ${colors.gray[500]};
+  margin: 0 calc(-1 * var(--dialog-padding));
+  // account for close button
+  padding: 0 calc(var(--dialog-padding) + 40px) var(--dialog-padding) var(--dialog-padding);
+
+  hr {
+    display: none;
+
+    ${media.small} {
+      display: inline;
+      width: 16px;
+      height: 1px;
+      border: none;
+      background-color: ${colors.gray[400]};
+      margin: 0 ${sizes(4)};
+      flex-shrink: 1;
+    }
+  }
+`
+
+export const StyledStepsInfoContainer = styled.div`
+  display: grid;
+
+  ${media.small} {
+    width: 100%;
+    grid-template-columns: repeat(6, auto);
+    align-items: center;
+    grid-column-gap: ${sizes(4)};
+  }
+`
+export const StyledStepInfo = styled.div<StyledStepInfoProps>`
+  display: ${({ isActive }) => (isActive ? 'flex' : 'none')};
+  align-items: center;
+
+  ${media.small} {
+    display: flex;
+  }
+`
+export const StyledCircle = styled.div<CircleProps>`
+  display: flex;
+  flex-shrink: 0;
+  justify-content: center;
+  align-items: center;
+  width: 32px;
+  height: 32px;
+  border-radius: 100%;
+  background-color: ${colors.gray[400]};
+  background-color: ${({ isActive }) => isActive && colors.blue[500]};
+  color: ${colors.gray[50]};
+`
+export const StyledStepInfoText = styled.div<StyledStepInfoProps>`
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1;
+  justify-content: center;
+  font-weight: ${typography.weights.semibold};
+  margin-left: ${sizes(2)};
+`
+
+export const StyledStepTitle = styled(Text)`
+  margin-top: ${sizes(1)};
+`
+
+export const StyledChevron = styled(SvgGlyphChevronRight)`
+  margin: 0 ${sizes(1)};
+  flex-shrink: 0;
+
+  display: none;
+  ${media.small} {
+    display: block;
+  }
+
+  > path {
+    stroke: ${colors.gray[500]};
+  }
+`

+ 60 - 0
src/components/Dialogs/Multistepper/Multistepper.tsx

@@ -0,0 +1,60 @@
+import React, { Fragment } from 'react'
+import { BaseDialogProps } from '../BaseDialog'
+import {
+  StyledDialog,
+  StyledHeader,
+  StyledStepsInfoContainer,
+  StyledStepInfo,
+  StyledCircle,
+  StyledStepInfoText,
+  StyledChevron,
+  StyledStepTitle,
+} from './Multistepper.style'
+import { SvgGlyphCheck } from '@/shared/icons'
+import { Text } from '@/shared/components'
+
+type Step = {
+  title: string
+  element: React.ReactNode
+}
+
+type MultistepperProps = {
+  steps: Step[]
+  currentStepIdx?: number
+} & BaseDialogProps
+
+const Multistepper: React.FC<MultistepperProps> = ({ steps, currentStepIdx = 0, ...dialogProps }) => {
+  return (
+    <StyledDialog {...dialogProps}>
+      <StyledHeader>
+        <StyledStepsInfoContainer>
+          {steps.map((step, idx) => {
+            const isActive = idx === currentStepIdx
+            const isCompleted = currentStepIdx > idx
+            const isLast = idx === steps.length - 1
+
+            return (
+              <Fragment key={idx}>
+                <StyledStepInfo isActive={isActive}>
+                  <StyledCircle isFilled={isActive || isCompleted} isActive={isActive}>
+                    {isCompleted ? <SvgGlyphCheck /> : idx + 1}
+                  </StyledCircle>
+                  <StyledStepInfoText isActive={isActive}>
+                    <Text variant="caption" secondary>
+                      Step {idx + 1}
+                    </Text>
+                    <StyledStepTitle variant="overhead">{step.title}</StyledStepTitle>
+                  </StyledStepInfoText>
+                </StyledStepInfo>
+                {isLast ? null : <StyledChevron />}
+              </Fragment>
+            )
+          })}
+        </StyledStepsInfoContainer>
+      </StyledHeader>
+      {steps[currentStepIdx].element}
+    </StyledDialog>
+  )
+}
+
+export default Multistepper

+ 3 - 0
src/components/Dialogs/Multistepper/index.ts

@@ -0,0 +1,3 @@
+import Multistepper from './Multistepper'
+
+export default Multistepper

+ 22 - 0
src/components/Dialogs/TransactionDialog/TransactionDialog.stories.tsx

@@ -0,0 +1,22 @@
+import React from 'react'
+import TransactionDialog, { TransactionDialogProps } from './TransactionDialog'
+import { Meta, Story } from '@storybook/react'
+import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+import { ExtrinsicStatus } from '@/joystream-lib'
+
+export default {
+  title: 'General/TransactionDialog',
+  component: TransactionDialog,
+  decorators: [
+    (Story) => (
+      <OverlayManagerProvider>
+        <Story />
+      </OverlayManagerProvider>
+    ),
+  ],
+} as Meta
+
+const RegularTemplate: Story<TransactionDialogProps> = ({ ...args }) => {
+  return <TransactionDialog {...args} status={ExtrinsicStatus.Unsigned} />
+}
+export const Regular = RegularTemplate.bind({})

+ 54 - 0
src/components/Dialogs/TransactionDialog/TransactionDialog.style.ts

@@ -0,0 +1,54 @@
+import styled from '@emotion/styled'
+import { sizes, media, colors, transitions } from '@/shared/theme'
+import { ReactComponent as TransactionIllustration } from '@/assets/transaction-illustration.svg'
+import Spinner from '@/shared/components/Spinner'
+import { css } from '@emotion/react'
+
+type StepProps = {
+  isActive?: boolean
+}
+export const StepsBar = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(10px, 1fr));
+  grid-gap: ${sizes(1)};
+  width: 100%;
+  height: ${sizes(2)};
+`
+
+export const Step = styled.div<StepProps>`
+  background-color: ${({ isActive }) => (isActive ? colors.gray[400] : colors.gray[600])};
+  height: 100%;
+  transition: background-color ${transitions.timings.regular} ${transitions.easing};
+  :hover {
+    ${({ isActive }) =>
+      !isActive &&
+      css`
+        background-color: ${colors.gray[500]};
+      `};
+  }
+`
+
+export const TextContainer = styled.div`
+  margin-top: ${sizes(40)};
+  position: relative;
+`
+
+export const StyledTransactionIllustration = styled(TransactionIllustration)`
+  position: absolute;
+  top: ${sizes(2)};
+  left: -50px;
+
+  ${media.small} {
+    left: 0;
+  }
+`
+
+export const StyledSpinner = styled(Spinner)`
+  position: absolute;
+  top: ${sizes(8)};
+  left: ${sizes(6)};
+`

+ 109 - 0
src/components/Dialogs/TransactionDialog/TransactionDialog.tsx

@@ -0,0 +1,109 @@
+import React from 'react'
+import ActionDialog, { ActionDialogProps } from '../ActionDialog/ActionDialog'
+import { TextContainer, StyledTransactionIllustration, StyledSpinner, StepsBar, Step } from './TransactionDialog.style'
+import { StyledTitleText, StyledDescriptionText } from '../MessageDialog/MessageDialog.style'
+import { ExtrinsicStatus } from '@/joystream-lib'
+import MessageDialog from '../MessageDialog'
+import { Tooltip } from '@/shared/components'
+
+export type TransactionDialogProps = Pick<ActionDialogProps, 'className'> & {
+  status: ExtrinsicStatus | null
+  successTitle: string
+  successDescription: string
+  onClose: () => void
+}
+
+const TRANSACTION_STEPS_DETAILS = {
+  [ExtrinsicStatus.ProcessingAssets]: {
+    title: 'Processing assets...',
+    description:
+      "Please wait till all your assets get processed. This can take up to 1 minute, depending on asset size and your machine's computing power.",
+    tooltip: '',
+  },
+  [ExtrinsicStatus.Unsigned]: {
+    title: 'Waiting for signature...',
+    description: 'Please sign the transaction using the Polkadot browser extension.',
+    tooltip: 'Signature',
+  },
+  [ExtrinsicStatus.Signed]: {
+    title: 'Waiting for confirmation...',
+    description:
+      'Your transaction has been signed and sent. Please wait for the blockchain confirmation. This should take about 15 seconds.',
+    tooltip: 'Confirmation',
+  },
+  [ExtrinsicStatus.Syncing]: {
+    title: 'Waiting for data propagation...',
+    description:
+      "Your transaction has been accepted and included into the blockchain. Please wait till it's picked up by the indexing node. This should take up to 15 seconds.",
+    tooltip: 'Propagation',
+  },
+}
+
+const TransactionDialog: React.FC<TransactionDialogProps> = ({
+  status,
+  successTitle,
+  successDescription,
+  onClose,
+  ...actionDialogProps
+}) => {
+  if (status === ExtrinsicStatus.Error) {
+    return (
+      <MessageDialog
+        showDialog
+        variant="error"
+        title="Something went wrong..."
+        description="Some unexpected error was encountered. If this persists, our Discord community may be a good place to find some help."
+        secondaryButtonText="Close"
+        onSecondaryButtonClick={onClose}
+      />
+    )
+  }
+
+  if (status === ExtrinsicStatus.Completed) {
+    return (
+      <MessageDialog
+        showDialog
+        variant="success"
+        title={successTitle}
+        description={successDescription}
+        secondaryButtonText="Close"
+        onSecondaryButtonClick={onClose}
+      />
+    )
+  }
+
+  const stepDetails = status != null ? TRANSACTION_STEPS_DETAILS[status] : null
+
+  const canCancel = status === ExtrinsicStatus.ProcessingAssets || ExtrinsicStatus.Unsigned
+
+  const transactionStepsWithoutProcessingAssets = Object.values(TRANSACTION_STEPS_DETAILS).filter(
+    (step) => step.title !== TRANSACTION_STEPS_DETAILS[ExtrinsicStatus.ProcessingAssets].title
+  )
+
+  return (
+    <ActionDialog
+      showDialog={status != null}
+      onSecondaryButtonClick={onClose}
+      secondaryButtonText="Cancel"
+      secondaryButtonDisabled={!canCancel}
+      exitButton={false}
+      {...actionDialogProps}
+    >
+      <StepsBar>
+        {transactionStepsWithoutProcessingAssets.map(({ title, tooltip }, idx) => (
+          <Tooltip key={idx} text={tooltip} placement="top-end">
+            <Step isActive={!!status && status > idx} />
+          </Tooltip>
+        ))}
+      </StepsBar>
+      <StyledTransactionIllustration />
+      <StyledSpinner />
+      <TextContainer>
+        <StyledTitleText variant="h4">{stepDetails?.title}</StyledTitleText>
+        <StyledDescriptionText variant="body2">{stepDetails?.description}</StyledDescriptionText>
+      </TextContainer>
+    </ActionDialog>
+  )
+}
+
+export default TransactionDialog

+ 4 - 0
src/components/Dialogs/TransactionDialog/index.ts

@@ -0,0 +1,4 @@
+import TransactionDialog, { TransactionDialogProps } from './TransactionDialog'
+
+export default TransactionDialog
+export type { TransactionDialogProps }

+ 9 - 0
src/components/Dialogs/index.ts

@@ -0,0 +1,9 @@
+import ActionDialog from './ActionDialog'
+import MessageDialog from './MessageDialog'
+import BaseDialog, { BaseDialogProps } from './BaseDialog'
+import Multistepper from './Multistepper'
+import ImageCropDialog, { ImageCropDialogImperativeHandle } from './ImageCropDialog'
+import TransactionDialog from './TransactionDialog'
+
+export { BaseDialog, ActionDialog, Multistepper, ImageCropDialog, TransactionDialog, MessageDialog }
+export type { BaseDialogProps, ImageCropDialogImperativeHandle }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است