Browse Source

merge improvements & tweaks

improvements & tweaks
Klaudiusz Dembler 3 years ago
parent
commit
dd8df921e8
100 changed files with 642 additions and 789 deletions
  1. 0 3
      .babelrc
  2. 18 12
      .env
  3. 39 5
      .eslintrc.js
  4. 2 0
      .prettierignore
  5. 1 0
      .storybook/main.js
  6. 2 9
      .storybook/preview-head.html
  7. 5 12
      .storybook/preview.jsx
  8. 17 0
      .stylelintrc.js
  9. 0 17
      __generated__/globalTypes.ts
  10. 3 2
      config-overrides.js
  11. 0 105
      netlify-plugins/contextual-env/index.js
  12. 0 6
      netlify-plugins/contextual-env/manifest.yml
  13. 0 8
      netlify-plugins/contextual-env/package.json
  14. 0 147
      netlify-plugins/contextual-env/yarn.lock
  15. 0 9
      netlify.toml
  16. 55 42
      package.json
  17. 0 6
      public/fonts.css
  18. 2 8
      public/index.html
  19. 12 12
      src/App.tsx
  20. 24 6
      src/MainLayout.tsx
  21. 2 1
      src/api/client/index.ts
  22. 44 10
      src/api/client/resolvers.ts
  23. 2 2
      src/api/client/transforms/index.ts
  24. 38 0
      src/api/client/transforms/orionViews.ts
  25. 1 1
      src/api/hooks/categories.ts
  26. 2 2
      src/api/hooks/membership.ts
  27. 1 1
      src/api/hooks/search.ts
  28. 4 4
      src/api/hooks/video.ts
  29. 2 1
      src/api/queries/__generated__/channels.generated.tsx
  30. 2 1
      src/api/queries/__generated__/memberships.generated.tsx
  31. 4 2
      src/api/queries/__generated__/search.generated.tsx
  32. 2 1
      src/api/queries/__generated__/shared.generated.tsx
  33. 60 2
      src/api/queries/__generated__/videos.generated.tsx
  34. 8 0
      src/api/queries/videos.graphql
  35. BIN
      src/assets/fonts/Inter-Bold.ttf
  36. BIN
      src/assets/fonts/Inter-ExtraLight-BETA.ttf
  37. BIN
      src/assets/fonts/Inter-Regular.ttf
  38. 2 4
      src/components/BackgroundPattern.tsx
  39. 2 4
      src/components/ChannelGallery.tsx
  40. 2 3
      src/components/ChannelGrid.tsx
  41. 10 14
      src/components/ChannelLink/ChannelLink.tsx
  42. 1 3
      src/components/ChannelLink/index.ts
  43. 3 10
      src/components/ChannelPreview.tsx
  44. 10 7
      src/components/CoverVideo/CoverVideo.style.ts
  45. 11 16
      src/components/CoverVideo/CoverVideo.tsx
  46. 2 1
      src/components/CoverVideo/coverVideoData.ts
  47. 1 3
      src/components/CoverVideo/index.ts
  48. 3 3
      src/components/Dialogs/ActionDialog/ActionDialog.stories.tsx
  49. 3 2
      src/components/Dialogs/ActionDialog/ActionDialog.style.ts
  50. 3 5
      src/components/Dialogs/ActionDialog/ActionDialog.tsx
  51. 1 4
      src/components/Dialogs/ActionDialog/index.ts
  52. 3 3
      src/components/Dialogs/BaseDialog/BaseDialog.stories.tsx
  53. 2 2
      src/components/Dialogs/BaseDialog/BaseDialog.style.ts
  54. 10 5
      src/components/Dialogs/BaseDialog/BaseDialog.tsx
  55. 1 4
      src/components/Dialogs/BaseDialog/index.ts
  56. 13 23
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx
  57. 3 4
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.style.ts
  58. 6 7
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx
  59. 2 1
      src/components/Dialogs/ImageCropDialog/cropper.ts
  60. 1 4
      src/components/Dialogs/ImageCropDialog/index.ts
  61. 3 3
      src/components/Dialogs/MessageDialog/MessageDialog.stories.tsx
  62. 3 5
      src/components/Dialogs/MessageDialog/MessageDialog.tsx
  63. 1 4
      src/components/Dialogs/MessageDialog/index.ts
  64. 3 3
      src/components/Dialogs/Multistepper/Multistepper.stories.tsx
  65. 5 7
      src/components/Dialogs/Multistepper/Multistepper.style.ts
  66. 4 6
      src/components/Dialogs/Multistepper/Multistepper.tsx
  67. 1 3
      src/components/Dialogs/Multistepper/index.ts
  68. 2 2
      src/components/Dialogs/TransactionDialog/TransactionDialog.stories.tsx
  69. 3 3
      src/components/Dialogs/TransactionDialog/TransactionDialog.style.ts
  70. 6 9
      src/components/Dialogs/TransactionDialog/TransactionDialog.tsx
  71. 1 4
      src/components/Dialogs/TransactionDialog/index.ts
  72. 6 9
      src/components/Dialogs/index.ts
  73. 4 6
      src/components/ErrorFallback.tsx
  74. 9 6
      src/components/InfiniteGrids/InfiniteChannelGrid.tsx
  75. 20 8
      src/components/InfiniteGrids/InfiniteVideoGrid.tsx
  76. 2 4
      src/components/InfiniteGrids/index.ts
  77. 5 3
      src/components/InfiniteGrids/useInfiniteGrid.ts
  78. 6 9
      src/components/InterruptedVideosGallery.tsx
  79. 1 1
      src/components/Link/Link.style.ts
  80. 1 3
      src/components/Link/Link.tsx
  81. 1 3
      src/components/Link/index.ts
  82. 8 8
      src/components/NoConnectionIndicator/NoConnectionIndicator.stories.tsx
  83. 1 1
      src/components/NoConnectionIndicator/NoConnectionIndicator.style.ts
  84. 3 5
      src/components/NoConnectionIndicator/NoConnectionIndicator.tsx
  85. 1 3
      src/components/NoConnectionIndicator/index.ts
  86. 2 3
      src/components/PlaceholderVideoGrid.tsx
  87. 1 3
      src/components/Portal.tsx
  88. 1 1
      src/components/PrivateRoute.tsx
  89. 17 10
      src/components/Sidenav/SidenavBase.style.ts
  90. 11 11
      src/components/Sidenav/SidenavBase.tsx
  91. 15 21
      src/components/Sidenav/StudioSidenav/StudioSidenav.tsx
  92. 1 1
      src/components/Sidenav/StudioSidenav/index.ts
  93. 3 7
      src/components/Sidenav/ViewerSidenav/FollowedChannels.style.ts
  94. 7 9
      src/components/Sidenav/ViewerSidenav/FollowedChannels.tsx
  95. 12 10
      src/components/Sidenav/ViewerSidenav/ViewerSidenav.tsx
  96. 1 1
      src/components/Sidenav/ViewerSidenav/index.ts
  97. 4 6
      src/components/Sidenav/index.ts
  98. 6 4
      src/components/SignInSteps/AccountStep.style.ts
  99. 13 15
      src/components/SignInSteps/AccountStep.tsx
  100. 1 0
      src/components/SignInSteps/ExtensionStep.style.ts

+ 0 - 3
.babelrc

@@ -1,3 +0,0 @@
-{
-  "plugins": ["emotion"]
-}

+ 18 - 12
.env

@@ -1,16 +1,22 @@
 # This file is commited. Do not store secrets here
 
-REACT_APP_ENV=staging
-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
-REACT_APP_ORION_URL=https://orion-staging.joystream.app/graphql
-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_ENV=development
 
-#REACT_APP_ENV=production
-#REACT_APP_QUERY_NODE_URL=https://hydra.joystream.org/graphql
-#REACT_APP_QUERY_NODE_SUBSCRIPTION_URL=wss://hydra.joystream.org/graphql
-#REACT_APP_ORION_URL=https://orion.joystream.org/graphql
-#REACT_APP_NODE_URL=wss://rome-rpc-endpoint.joystream.org:9944/
-#REACT_APP_FAUCET_URL=https://member-faucet.joystream.org/register
+# default target env is development
+REACT_APP_DEVELOPMENT_QUERY_NODE_URL=https://sumer-dev-2.joystream.app/query/server/graphql
+REACT_APP_DEVELOPMENT_QUERY_NODE_SUBSCRIPTION_URL=wss://sumer-dev-2.joystream.app/query/server/graphql
+REACT_APP_DEVELOPMENT_ORION_URL=https://orion-staging.joystream.app/graphql
+REACT_APP_DEVELOPMENT_NODE_URL=wss://sumer-dev-2.joystream.app/rpc
+REACT_APP_DEVELOPMENT_FAUCET_URL=https://sumer-dev-2.joystream.app/members/register
 
+REACT_APP_PRODUCTION_QUERY_NODE_URL=https://hydra.joystream.org/graphql
+REACT_APP_PRODUCTION_QUERY_NODE_SUBSCRIPTION_URL=wss://hydra.joystream.org/graphql
+REACT_APP_PRODUCTION_ORION_URL=https://orion.joystream.org/graphql
+REACT_APP_PRODUCTION_NODE_URL=wss://rome-rpc-endpoint.joystream.org:9944/
+REACT_APP_PRODUCTION_FAUCET_URL=https://member-faucet.joystream.org/register
+
+REACT_APP_MOCKING_QUERY_NODE_URL=/mocked-query-node
+REACT_APP_MOCKING_QUERY_NODE_SUBSCRIPTION_URL=ws://127.0.0.1:9955
+REACT_APP_MOCKING_ORION_URL=/mocked-orion
+REACT_APP_MOCKING_NODE_URL=ws://127.0.0.1:9944
+REACT_APP_MOCKING_FAUCET_URL=/mocked-faucet

+ 39 - 5
.eslintrc.js

@@ -5,12 +5,36 @@ module.exports = {
     es6: true,
     jest: true,
   },
-  extends: ['plugin:react-hooks/recommended', '@joystream/eslint-config'],
-  plugins: ['@emotion'],
+  parser: '@typescript-eslint/parser',
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react/recommended',
+    'plugin:react-hooks/recommended',
+    // turns off the rules which may conflict with prettier
+    'prettier',
+  ],
+  plugins: ['@emotion', '@typescript-eslint'],
+  settings: {
+    react: {
+      version: 'detect',
+    },
+  },
   rules: {
+    // taken care of by typescript
     'react/prop-types': 'off',
+
+    // disable explicit return types
     '@typescript-eslint/explicit-module-boundary-types': 'off',
+
+    // allow "_" prefixed function arguments
+    '@typescript-eslint/no-unused-vars': [
+      'warn',
+      { 'args': 'after-used', 'argsIgnorePattern': '^_', 'ignoreRestSiblings': true },
+    ],
+
     '@typescript-eslint/no-empty-function': 'warn',
+    '@typescript-eslint/class-name-casing': 'off',
     '@typescript-eslint/ban-ts-comment': [
       'error',
       {
@@ -25,9 +49,19 @@ 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'],
+
+    // force using Logger object
+    'no-console': ['warn'],
+
+    // sort import member order
+    // can be removed once https://github.com/trivago/prettier-plugin-sort-imports/pull/44 gets released
+    'sort-imports': [
+      'warn',
+      {
+        'ignoreDeclarationSort': true,
+      },
+    ],
+
     // make sure we use the proper Emotion imports
     '@emotion/pkg-renaming': 'error',
     '@emotion/no-vanilla': 'error',

+ 2 - 0
.prettierignore

@@ -5,3 +5,5 @@ storybook-static/
 .coverage
 tsconfig.json
 public/mockServiceWorker.js
+.history
+docs/

+ 1 - 0
.storybook/main.js

@@ -8,6 +8,7 @@ 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))
+  // eslint-disable-next-line no-constant-condition
   while (true) {
     if (fs.existsSync(path.join(currDir, 'package.json'))) {
       return currDir

+ 2 - 9
.storybook/preview-head.html

@@ -1,12 +1,5 @@
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700&display=swap" rel="stylesheet" />
-<link
-  rel="stylesheet"
-  href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/Optimo-PxGrotesk/PxGroteskRegular-Regular.css"
-/>
-<link
-  rel="stylesheet"
-  href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/Optimo-PxGrotesk/PxGroteskBold-Regular.css"
-/>
+<link rel="stylesheet" href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/PxGrotesk/PxGrotesk.css" />
+<link rel="stylesheet" href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/Inter/inter.css" />
 <style>
   body {
     margin: 0;

+ 5 - 12
.storybook/preview.jsx

@@ -1,10 +1,10 @@
-import { css } from '@emotion/react'
+import styled from '@emotion/styled'
 import React, { useRef } from 'react'
 import useResizeObserver from 'use-resize-observer'
 
 import { GlobalStyle } from '../src/shared/components'
 
-const wrapperStyle = css`
+const Wrapper = styled.div`
   padding: 10px;
   height: calc(100vh - 20px);
 
@@ -13,24 +13,17 @@ const wrapperStyle = css`
   }
 `
 
-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}>
+    <Wrapper ref={ref}>
+      <div style={{ position: 'absolute', fontSize: '12px', right: '4px', top: '4px' }}>
         {width}px x {height}px
       </div>
       <GlobalStyle />
       {styleFn()}
-    </div>
+    </Wrapper>
   )
 }
 

+ 17 - 0
.stylelintrc.js

@@ -0,0 +1,17 @@
+module.exports = {
+  extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
+  defaultSeverity: 'warning',
+  rules: {
+    'function-name-case': null,
+    'custom-property-empty-line-before': null,
+    'unit-no-unknown': null,
+    'declaration-block-no-duplicate-properties': true,
+    'value-keyword-case': null,
+    'rule-empty-line-before': [
+      'always',
+      {
+        'ignore': 'first-nested',
+      },
+    ],
+  },
+}

+ 0 - 17
__generated__/globalTypes.ts

@@ -1,17 +0,0 @@
-/* tslint:disable */
-/* eslint-disable */
-// @generated
-// This file was automatically generated and should not be edited.
-
-//==============================================================
-// START Enums and Input Objects
-//==============================================================
-
-export enum VideoOrderByInput {
-  createdAt_ASC = "createdAt_ASC",
-  createdAt_DESC = "createdAt_DESC",
-}
-
-//==============================================================
-// END Enums and Input Objects
-//==============================================================

+ 3 - 2
config-overrides.js

@@ -1,11 +1,12 @@
 /* eslint-disable @typescript-eslint/no-var-requires */
 const path = require('path')
-const { override, addBabelPreset, addBabelPlugin, addWebpackAlias, addWebpackModuleRule } = require('customize-cra')
+const StylelintPlugin = require('stylelint-webpack-plugin')
+const { override, addBabelPlugin, addWebpackAlias, addWebpackModuleRule, addWebpackPlugin } = require('customize-cra')
 
 module.exports = {
   webpack: override(
     addBabelPlugin('@emotion/babel-plugin'),
-    addBabelPreset('@emotion/babel-preset-css-prop'),
+    addWebpackPlugin(new StylelintPlugin({ files: './src/**/*.{tsx,ts}' })),
     addWebpackAlias({
       '@': path.resolve(__dirname, 'src/'),
     }),

+ 0 - 105
netlify-plugins/contextual-env/index.js

@@ -1,105 +0,0 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
-
-const { Octokit } = require('@octokit/rest')
-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, SITE_NAME } = process.env
-
-    // === get env based on branch/PR ===
-    let env = ENV_PRODUCTION
-    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({
-        productionBranch: production_branch,
-        repoUrl: REPOSITORY_URL,
-        pullNumber: REVIEW_ID,
-      })
-      if (!productionPull) {
-        env = ENV_STAGING
-      }
-    }
-
-    const pluginSummary = `Using ${env} variables`
-    console.log(pluginSummary)
-
-    // === expose all matching env variables ===
-    let pluginText = ''
-    Object.keys(process.env).forEach((varKey) => {
-      if (varKey.startsWith(env)) {
-        const varValue = process.env[varKey]
-        const appVarKey = app_env_prefix ? `${app_env_prefix}${varKey.replace(`${env}_`, '')}` : varKey
-
-        const varLine = `Exposing ${varKey} as ${appVarKey}=${varValue}`
-        pluginText = `${pluginText}\n${varLine}`
-        console.log(varLine)
-
-        process.env[appVarKey] = varValue
-      }
-    })
-
-    // === expose env ===
-    const appEnvKey = `${app_env_prefix}ENV`
-    const envLine = `Exposing ${appEnvKey}=${env}`
-    pluginText = `${pluginText}\n${envLine}`
-    console.log(envLine)
-
-    process.env[appEnvKey] = env
-
-    utils.status.show({
-      title: 'Env variables set',
-      summary: pluginSummary,
-      text: pluginText,
-    })
-  },
-}
-
-const isProductionPull = async ({ productionBranch, repoUrl, pullNumber }) => {
-  try {
-    const baseBranch = await getPullBaseBranch({ repoUrl, pullNumber })
-    return baseBranch === productionBranch
-  } catch (e) {
-    console.log("Couldn't load base branch: ", e)
-  }
-  return true
-}
-
-const getRepoOwnerAndName = (repoUrl) => {
-  const segments = repoUrl.split('/')
-  const owner = segments[segments.length - 2]
-  const name = segments[segments.length - 1]
-  return { owner, name }
-}
-
-const getPullBaseBranch = async ({ repoUrl, pullNumber, timeout = 2000 }) => {
-  const { owner, name } = getRepoOwnerAndName(repoUrl)
-  const octokit = new Octokit()
-
-  const response = await octokit.pulls.get({
-    owner,
-    repo: name,
-    pull_number: pullNumber,
-    request: {
-      timeout,
-    },
-  })
-
-  if (response.status !== 200) {
-    throw new Error('Failed to fetch pull')
-  }
-
-  const base = get(response, 'data.base.ref', null)
-  if (!base) {
-    throw new Error('Failed to access base branch')
-  }
-
-  return base
-}

+ 0 - 6
netlify-plugins/contextual-env/manifest.yml

@@ -1,6 +0,0 @@
-name: contextual-env
-inputs:
-  - name: production_branch
-    description: Branch for which production envariables should be used
-  - name: app_env_prefix
-    description: Prefix to add to env variables for build tool support (e.g. CRA)

+ 0 - 8
netlify-plugins/contextual-env/package.json

@@ -1,8 +0,0 @@
-{
-  "name": "contextual-env",
-  "version": "1.0.0",
-  "dependencies": {
-    "@octokit/rest": "^18.2.1",
-    "lodash": "^4.17.21"
-  }
-}

+ 0 - 147
netlify-plugins/contextual-env/yarn.lock

@@ -1,147 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@octokit/auth-token@^2.4.4":
-  version "2.4.5"
-  resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3"
-  integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==
-  dependencies:
-    "@octokit/types" "^6.0.3"
-
-"@octokit/core@^3.2.3":
-  version "3.2.5"
-  resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.2.5.tgz#57becbd5fd789b0592b915840855f3a5f233d554"
-  integrity sha512-+DCtPykGnvXKWWQI0E1XD+CCeWSBhB6kwItXqfFmNBlIlhczuDPbg+P6BtLnVBaRJDAjv+1mrUJuRsFSjktopg==
-  dependencies:
-    "@octokit/auth-token" "^2.4.4"
-    "@octokit/graphql" "^4.5.8"
-    "@octokit/request" "^5.4.12"
-    "@octokit/types" "^6.0.3"
-    before-after-hook "^2.1.0"
-    universal-user-agent "^6.0.0"
-
-"@octokit/endpoint@^6.0.1":
-  version "6.0.11"
-  resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.11.tgz#082adc2aebca6dcefa1fb383f5efb3ed081949d1"
-  integrity sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==
-  dependencies:
-    "@octokit/types" "^6.0.3"
-    is-plain-object "^5.0.0"
-    universal-user-agent "^6.0.0"
-
-"@octokit/graphql@^4.5.8":
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.0.tgz#f9abca55f82183964a33439d5264674c701c3327"
-  integrity sha512-CJ6n7izLFXLvPZaWzCQDjU/RP+vHiZmWdOunaCS87v+2jxMsW9FB5ktfIxybRBxZjxuJGRnxk7xJecWTVxFUYQ==
-  dependencies:
-    "@octokit/request" "^5.3.0"
-    "@octokit/types" "^6.0.3"
-    universal-user-agent "^6.0.0"
-
-"@octokit/openapi-types@^5.1.0":
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.1.1.tgz#d01ae6e2879c589edcea7800e3a427455ece619f"
-  integrity sha512-yMyaX9EDWCiyv7m85/K8L7bLFj1wrLdfDkKcZEZ6gNmepSW5mfSMFJnYwRINN7lF58wvevKPWvw0MYy6sxcFlQ==
-
-"@octokit/plugin-paginate-rest@^2.6.2":
-  version "2.10.0"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.10.0.tgz#5925156d809c94b7bfc47b28e17488415548fa67"
-  integrity sha512-71OsKBSMcQEu/6lfVbhv5C5ikU1rn10rKot/WiV7do7fyfElQ2eCUQFogHPbj0ci5lnKAjvahOiMAr6lcvL8Qw==
-  dependencies:
-    "@octokit/types" "^6.10.0"
-
-"@octokit/plugin-request-log@^1.0.2":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz#70a62be213e1edc04bb8897ee48c311482f9700d"
-  integrity sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ==
-
-"@octokit/plugin-rest-endpoint-methods@4.12.2":
-  version "4.12.2"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.12.2.tgz#d2bd0b794d6c11a13113db6199baf44a39b06f50"
-  integrity sha512-5+MmGusB7wPw7OholtcGaMyjfrsFSpFqtJW8VsrbfU/TuaiQepY4wgVkS7P3TAObX257jrTbbGo/sJLcoGf16g==
-  dependencies:
-    "@octokit/types" "^6.10.1"
-    deprecation "^2.3.1"
-
-"@octokit/request-error@^2.0.0":
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.5.tgz#72cc91edc870281ad583a42619256b380c600143"
-  integrity sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==
-  dependencies:
-    "@octokit/types" "^6.0.3"
-    deprecation "^2.0.0"
-    once "^1.4.0"
-
-"@octokit/request@^5.3.0", "@octokit/request@^5.4.12":
-  version "5.4.14"
-  resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.14.tgz#ec5f96f78333bb2af390afa5ff66f114b063bc96"
-  integrity sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA==
-  dependencies:
-    "@octokit/endpoint" "^6.0.1"
-    "@octokit/request-error" "^2.0.0"
-    "@octokit/types" "^6.7.1"
-    deprecation "^2.0.0"
-    is-plain-object "^5.0.0"
-    node-fetch "^2.6.1"
-    once "^1.4.0"
-    universal-user-agent "^6.0.0"
-
-"@octokit/rest@^18.2.1":
-  version "18.2.1"
-  resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.2.1.tgz#04835fe9ab0d90ca2a93898cde2aa944c78c70bc"
-  integrity sha512-DdQ1vps41JSyB2axyL1mBwJiXAPibgugIQPOmt0mL/yhwheQ6iuq2aKiJWgGWa9ldMfe3v9gIFYlrFgxQ5ThGQ==
-  dependencies:
-    "@octokit/core" "^3.2.3"
-    "@octokit/plugin-paginate-rest" "^2.6.2"
-    "@octokit/plugin-request-log" "^1.0.2"
-    "@octokit/plugin-rest-endpoint-methods" "4.12.2"
-
-"@octokit/types@^6.0.3", "@octokit/types@^6.10.0", "@octokit/types@^6.10.1", "@octokit/types@^6.7.1":
-  version "6.10.1"
-  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.10.1.tgz#5955dc0cf344bb82a46283a0c332651f5dd9f1ad"
-  integrity sha512-hgNC5jxKG8/RlqxU/6GThkGrvFpz25+cPzjQjyiXTNBvhyltn2Z4GhFY25+kbtXwZ4Co4zM0goW5jak1KLp1ug==
-  dependencies:
-    "@octokit/openapi-types" "^5.1.0"
-
-before-after-hook@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.1.tgz#99ae36992b5cfab4a83f6bee74ab27835f28f405"
-  integrity sha512-5ekuQOvO04MDj7kYZJaMab2S8SPjGJbotVNyv7QYFCOAwrGZs/YnoDNlh1U+m5hl7H2D/+n0taaAV/tfyd3KMA==
-
-deprecation@^2.0.0, deprecation@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
-  integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
-
-is-plain-object@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
-  integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
-
-lodash@^4.17.21:
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
-  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-node-fetch@^2.6.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
-  integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
-
-once@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
-  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
-  dependencies:
-    wrappy "1"
-
-universal-user-agent@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
-  integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
-
-wrappy@1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
-  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

+ 0 - 9
netlify.toml

@@ -2,12 +2,3 @@
 from = "/*"
 to = "/index.html"
 status = 200
-
-[[plugins]]
-package = "@netlify/plugin-local-install-core"
-
-[[plugins]]
-package = "./netlify-plugins/contextual-env"
-  [plugins.inputs]
-  production_branch = "master"
-  app_env_prefix = "REACT_APP_"

+ 55 - 42
package.json

@@ -16,11 +16,15 @@
     "url": "https://github.com/Joystream/joystream/issues"
   },
   "scripts": {
+    "bundle-stats": "webpack-bundle-analyzer dist/bundle-stats.json -m static -r dist/stats.html",
     "start": "react-app-rewired start",
     "dev": "yarn start",
-    "build": "react-app-rewired build",
+    "build": "react-app-rewired build --stats && yarn bundle-stats",
     "eject": "react-app-rewired eject",
-    "lint": "eslint --ext .js,.jsx,.ts,.tsx .",
+    "lint:ts": "eslint --ext .js,.jsx,.ts,.tsx .",
+    "lint:prettier": "prettier --check .",
+    "lint:css": "stylelint './src/**/*.{tsx,ts}'",
+    "lint": "yarn lint:css && yarn lint:ts && yarn lint:prettier",
     "storybook": "start-storybook -p 6006 -s public",
     "build-storybook": "build-storybook -s public",
     "mocking:videos": "node scripts/mocking/generateVideos.js",
@@ -41,18 +45,9 @@
   },
   "dependencies": {
     "@apollo/client": "^3.3.0",
-    "@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",
     "@joystream/types": "~0.16.1",
     "@loadable/component": "^5.14.1",
@@ -60,71 +55,52 @@
     "@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",
-    "@types/react-dom": "^16.9.0",
-    "@types/react-transition-group": "^4.4.0",
-    "@types/video.js": "^7.3.10",
     "apollo": "^2.30.2",
     "awesome-debounce-promise": "^2.1.0",
     "axios": "^0.21.1",
-    "bn.js": "~5.2.0",
+    "bn.js": "^4.12.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",
     "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",
-    "faker": "^5.1.0",
-    "fluent-ffmpeg": "^2.1.2",
     "glider-js": "^1.7.3",
     "graphql": "^15.3.0",
     "graphql-tag": "^2.11.0",
     "graphql-tools": "^7.0.2",
     "history": "^5.0.0",
-    "husky": "^4.2.5",
+    "immer": "^9.0.3",
     "ipfs-only-hash": "^2.1.0",
-    "lint-staged": "^10.2.7",
     "lodash": "^4.17.19",
     "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-hook-form": "^7.8.1",
     "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",
     "react-use-measure": "^2.0.4",
     "retry-axios": "^2.4.0",
     "subscriptions-transport-ws": "^0.9.18",
-    "ts-loader": "^6.2.1",
-    "typescript": "^4.2.3",
     "use-resize-observer": "^7.0.0",
     "video.js": "^7.8.3",
-    "webpack-merge": "^5.7.3"
+    "zustand": "^3.5.2"
   },
   "devDependencies": {
+    "@emotion/babel-plugin": "~11.0.0",
+    "@emotion/eslint-plugin": "^11.2.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",
     "@storybook/addon-actions": "^6.1.16",
     "@storybook/addon-essentials": "^6.1.16",
     "@storybook/addon-links": "^6.1.16",
@@ -134,7 +110,44 @@
     "@storybook/react": "^6.1.16",
     "@storybook/theming": "^6.1.16",
     "@svgr/cli": "^5.5.0",
-    "@trivago/prettier-plugin-sort-imports": "^2.0.2"
+    "@trivago/prettier-plugin-sort-imports": "^2.0.2",
+    "@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",
+    "@types/react-dom": "^16.9.0",
+    "@types/react-transition-group": "^4.4.0",
+    "@types/video.js": "^7.3.10",
+    "@typescript-eslint/eslint-plugin": "^4.27.0",
+    "@typescript-eslint/parser": "^4.27.0",
+    "eslint": "^7.28.0",
+    "eslint-config-prettier": "^8.3.0",
+    "eslint-plugin-react": "^7.24.0",
+    "eslint-plugin-react-hooks": "^4.2.0",
+    "faker": "^5.1.0",
+    "fluent-ffmpeg": "^2.1.2",
+    "husky": "^4.2.5",
+    "lint-staged": "^10.2.7",
+    "patch-package": "^6.2.2",
+    "postinstall-postinstall": "^2.1.0",
+    "react-app-rewired": "^2.1.6",
+    "react-scripts": "4.0.1",
+    "stylelint": "^13.13.1",
+    "stylelint-config-prettier": "^8.0.2",
+    "stylelint-config-standard": "^22.0.0",
+    "stylelint-webpack-plugin": "^2.1.1",
+    "ts-loader": "^6.2.1",
+    "typescript": "^4.2.3",
+    "webpack-bundle-analyzer": "^4.4.2",
+    "webpack-merge": "^5.7.3"
+  },
+  "resolutions": {
+    "postcss-safe-parser": "4.0.2",
+    "bn.js": "4.12.0"
   },
   "browserslist": {
     "production": [

File diff suppressed because it is too large
+ 0 - 6
public/fonts.css


+ 2 - 8
public/index.html

@@ -11,15 +11,9 @@
     <link rel="mask-icon" href="%PUBLIC_URL%/favicon/safari-pinned-tab.svg" color="#5bbad5" />
     <meta name="msapplication-TileColor" content="#da532c" />
     <meta name="theme-color" content="#4038FF" />
-    <link
-      rel="stylesheet"
-      href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/Optimo-PxGrotesk/PxGroteskRegular-Regular.css"
-    />
-    <link
-      rel="stylesheet"
-      href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/Optimo-PxGrotesk/PxGroteskBold-Regular.css"
-    />
     <title>Joystream</title>
+    <link rel="stylesheet" href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/PxGrotesk/PxGrotesk.css" />
+    <link rel="stylesheet" href="https://eu-central-1.linodeobjects.com/atlas-assets/fonts/Inter/inter.css" />
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 12 - 12
src/App.tsx

@@ -2,26 +2,26 @@ import { ApolloProvider } from '@apollo/client'
 import React from 'react'
 
 import { createApolloClient } from '@/api'
-import { ConnectionStatusProvider, OverlayManagerProvider, SnackbarProvider, StorageProvidersProvider } from '@/hooks'
 
-import MainLayout from './MainLayout'
+import { MainLayout } from './MainLayout'
+import { AssetsManager, DialogProvider, OverlayManagerProvider, Snackbars, StorageProvidersProvider } from './providers'
 
-export default function App() {
+export const App = () => {
   // create client on render so the mocking setup is done if needed
   // App doesn't accept props and doesn't contain state so should never rerender
   const apolloClient = createApolloClient()
 
   return (
     <ApolloProvider client={apolloClient}>
-      <SnackbarProvider>
-        <ConnectionStatusProvider>
-          <OverlayManagerProvider>
-            <StorageProvidersProvider>
-              <MainLayout />
-            </StorageProvidersProvider>
-          </OverlayManagerProvider>
-        </ConnectionStatusProvider>
-      </SnackbarProvider>
+      <OverlayManagerProvider>
+        <StorageProvidersProvider>
+          <DialogProvider>
+            <MainLayout />
+            <Snackbars />
+            <AssetsManager />
+          </DialogProvider>
+        </StorageProvidersProvider>
+      </OverlayManagerProvider>
     </ApolloProvider>
   )
 }

+ 24 - 6
src/MainLayout.tsx

@@ -1,12 +1,15 @@
 import loadable from '@loadable/component'
-import React from 'react'
-import { BrowserRouter, Routes, Route } from 'react-router-dom'
+import React, { useEffect } from 'react'
+import { BrowserRouter, Route, Routes } from 'react-router-dom'
 
-import { TopbarBase, StudioLoading } from '@/components'
+import { StudioLoading, TopbarBase } from '@/components'
 import { BASE_PATHS } from '@/config/routes'
 import { GlobalStyle } from '@/shared/components'
 import { routingTransitions } from '@/styles/routingTransitions'
+import { isBrowserOutdated } from '@/utils/browser'
 
+import { useDialog } from './providers'
+import { AdminView } from './views/admin'
 import { LegalLayout } from './views/legal'
 import { PlaygroundLayout } from './views/playground'
 import { ViewerLayout } from './views/viewer'
@@ -20,7 +23,23 @@ const LoadableStudioLayout = loadable(() => import('./views/studio/StudioLayout'
   ),
 })
 
-const MainLayout: React.FC = () => {
+export const MainLayout: React.FC = () => {
+  const [openDialog, closeDialog] = useDialog({
+    title: 'Outdated browser detected',
+    description:
+      'It seems the browser version you are using is not fully supported by Joystream. Some of the features may be broken or not accessible. For the best experience, please upgrade your browser to the latest version.',
+    variant: 'warning',
+    primaryButtonText: 'Click here to see instructions',
+    onPrimaryButtonClick: () => window.open('https://www.whatismybrowser.com/guides/how-to-update-your-browser/auto'),
+    onExitClick: () => closeDialog(),
+  })
+
+  useEffect(() => {
+    if (isBrowserOutdated) {
+      openDialog()
+    }
+  }, [openDialog])
+
   return (
     <>
       <GlobalStyle additionalStyles={[routingTransitions]} />
@@ -30,10 +49,9 @@ const MainLayout: React.FC = () => {
           <Route path={BASE_PATHS.legal + '/*'} element={<LegalLayout />} />
           <Route path={BASE_PATHS.studio + '/*'} element={<LoadableStudioLayout />} />
           <Route path={BASE_PATHS.playground + '/*'} element={<PlaygroundLayout />} />
+          <Route path={BASE_PATHS.admin + '/*'} element={<AdminView />} />
         </Routes>
       </BrowserRouter>
     </>
   )
 }
-
-export default MainLayout

+ 2 - 1
src/api/client/index.ts

@@ -6,7 +6,7 @@ import { delegateToSchema } from '@graphql-tools/delegate'
 import { CreateProxyingResolverFn } from '@graphql-tools/delegate/types'
 import { mergeSchemas } from '@graphql-tools/merge'
 import { wrapSchema } from '@graphql-tools/wrap'
-import { buildASTSchema, GraphQLFieldResolver } from 'graphql'
+import { GraphQLFieldResolver, buildASTSchema } from 'graphql'
 
 import { createExecutors } from '@/api/client/executors'
 import { QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL } from '@/config/urls'
@@ -62,6 +62,7 @@ const createApolloClient = () => {
     uri: QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL,
     options: {
       reconnect: true,
+      reconnectionAttempts: 5,
     },
   })
 

+ 44 - 10
src/api/client/resolvers.ts

@@ -1,15 +1,21 @@
-import { delegateToSchema, Transform } from '@graphql-tools/delegate'
+import { Transform, delegateToSchema } from '@graphql-tools/delegate'
 import type { IResolvers, ISchemaLevelResolver } from '@graphql-tools/utils'
 import { GraphQLSchema } from 'graphql'
 
+import { createLookup } from '@/utils/data'
+import { Logger } from '@/utils/logger'
+
 import {
-  TransformOrionViewsField,
-  ORION_VIEWS_QUERY_NAME,
-  RemoveQueryNodeViewsField,
-  RemoveQueryNodeFollowsField,
+  ORION_BATCHED_VIEWS_QUERY_NAME,
   ORION_FOLLOWS_QUERY_NAME,
+  RemoveQueryNodeFollowsField,
+  RemoveQueryNodeViewsField,
+  TransformBatchedOrionViewsField,
   TransformOrionFollowsField,
 } from './transforms'
+import { ORION_VIEWS_QUERY_NAME, TransformOrionViewsField } from './transforms/orionViews'
+
+import { VideoEdge } from '../queries'
 
 const createResolverWithTransforms = (
   schema: GraphQLSchema,
@@ -56,10 +62,10 @@ export const queryNodeStitchingResolvers = (
     ]),
   },
   Video: {
-    // TODO: Resolve the views count in parallel to the videosConnection query
-    // this can be done by writing a resolver for the query itself in which two requests in the same fashion as below would be made
-    // then the results could be combined
     views: async (parent, args, context, info) => {
+      if (parent.views != null) {
+        return parent.views
+      }
       try {
         return await delegateToSchema({
           schema: orionSchema,
@@ -75,11 +81,39 @@ export const queryNodeStitchingResolvers = (
           transforms: [TransformOrionViewsField],
         })
       } catch (error) {
-        console.warn('Failed to resolve views field', { error })
+        Logger.warn('Failed to resolve views field', { error })
         return null
       }
     },
   },
+  VideoConnection: {
+    edges: async (parent, args, context, info) => {
+      const batchedVideoViews = await delegateToSchema({
+        schema: orionSchema,
+        operation: 'query',
+        // operationName has to be manually kept in sync with the query name used
+        operationName: 'GetBatchedVideoViews',
+        fieldName: ORION_BATCHED_VIEWS_QUERY_NAME,
+        args: {
+          videoIdList: parent.edges.map((edge: VideoEdge) => edge.node.id),
+        },
+        context,
+        info,
+        transforms: [TransformBatchedOrionViewsField],
+      })
+
+      const viewsLookup = createLookup<{ id: string; views: number }>(batchedVideoViews || [])
+
+      return parent.edges.map((edge: VideoEdge) => ({
+        ...edge,
+        node: {
+          ...edge.node,
+          views: viewsLookup[edge.node.id]?.views || 0,
+        },
+      }))
+    },
+  },
+
   Channel: {
     follows: async (parent, args, context, info) => {
       try {
@@ -97,7 +131,7 @@ export const queryNodeStitchingResolvers = (
           transforms: [TransformOrionFollowsField],
         })
       } catch (error) {
-        console.warn('Failed to resolve follows field', { error })
+        Logger.warn('Failed to resolve follows field', { error })
         return null
       }
     },

+ 2 - 2
src/api/client/transforms/index.ts

@@ -1,4 +1,4 @@
-export { ORION_VIEWS_QUERY_NAME, TransformOrionViewsField } from './orionViews'
 export { ORION_FOLLOWS_QUERY_NAME, TransformOrionFollowsField } from './orionFollows'
-export { RemoveQueryNodeViewsField } from './queryNodeViews'
+export { ORION_BATCHED_VIEWS_QUERY_NAME, TransformBatchedOrionViewsField } from './orionViews'
 export { RemoveQueryNodeFollowsField } from './queryNodeFollows'
+export { RemoveQueryNodeViewsField } from './queryNodeViews'

+ 38 - 0
src/api/client/transforms/orionViews.ts

@@ -74,3 +74,41 @@ export const TransformOrionViewsField: Transform = {
     return { data }
   },
 }
+
+export const ORION_BATCHED_VIEWS_QUERY_NAME = 'batchedVideoViews'
+
+export const TransformBatchedOrionViewsField: Transform = {
+  transformRequest(request) {
+    request.document = {
+      ...request.document,
+      definitions: request.document.definitions.map((definition) => {
+        if (definition.kind === 'OperationDefinition') {
+          return {
+            ...definition,
+            selectionSet: {
+              ...definition.selectionSet,
+              selections: definition.selectionSet.selections.map((selection) => {
+                if (selection.kind === 'Field' && selection.name.value === ORION_BATCHED_VIEWS_QUERY_NAME) {
+                  return {
+                    ...selection,
+                    selectionSet: VIDEO_INFO_SELECTION_SET,
+                  }
+                }
+                return selection
+              }),
+            },
+          }
+        }
+        return definition
+      }),
+    }
+
+    return request
+  },
+  transformResult(result) {
+    if (result.errors) {
+      throw new OrionError(result.errors)
+    }
+    return result
+  },
+}

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

@@ -1,6 +1,6 @@
 import { QueryHookOptions } from '@apollo/client'
 
-import { useGetVideoCategoriesQuery, GetVideoCategoriesQuery, GetVideoCategoriesQueryVariables } from '@/api/queries'
+import { GetVideoCategoriesQuery, GetVideoCategoriesQueryVariables, useGetVideoCategoriesQuery } from '@/api/queries'
 
 type Opts = QueryHookOptions<GetVideoCategoriesQuery>
 const useCategories = (variables?: GetVideoCategoriesQueryVariables, opts?: Opts) => {

+ 2 - 2
src/api/hooks/membership.ts

@@ -2,10 +2,10 @@ import { QueryHookOptions } from '@apollo/client'
 
 import {
   GetMembershipQuery,
-  useGetMembershipQuery,
-  useGetMembershipsQuery,
   GetMembershipQueryVariables,
   GetMembershipsQueryVariables,
+  useGetMembershipQuery,
+  useGetMembershipsQuery,
 } from '@/api/queries'
 
 type MembershipOpts = QueryHookOptions<GetMembershipQuery>

+ 1 - 1
src/api/hooks/search.ts

@@ -1,6 +1,6 @@
 import { QueryHookOptions } from '@apollo/client'
 
-import { useSearchQuery, SearchQueryVariables, SearchQuery } from '@/api/queries'
+import { SearchQuery, SearchQueryVariables, useSearchQuery } from '@/api/queries'
 
 type Opts = QueryHookOptions<SearchQuery>
 const useSearch = (variables: SearchQueryVariables, opts?: Opts) => {

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

@@ -1,12 +1,12 @@
-import { QueryHookOptions, MutationHookOptions } from '@apollo/client'
+import { MutationHookOptions, QueryHookOptions } from '@apollo/client'
 
 import {
-  useGetVideoQuery,
-  useAddVideoViewMutation,
-  GetVideoQuery,
   AddVideoViewMutation,
+  GetVideoQuery,
   GetVideosQuery,
   GetVideosQueryVariables,
+  useAddVideoViewMutation,
+  useGetVideoQuery,
   useGetVideosQuery,
 } from '@/api/queries'
 

+ 2 - 1
src/api/queries/__generated__/channels.generated.tsx

@@ -2,7 +2,8 @@ import { gql } from '@apollo/client'
 import * as Apollo from '@apollo/client'
 
 import * as Types from './baseTypes.generated'
-import { DataObjectFieldsFragment, DataObjectFieldsFragmentDoc } from './shared.generated'
+import { DataObjectFieldsFragment } from './shared.generated'
+import { DataObjectFieldsFragmentDoc } from './shared.generated'
 
 export type BasicChannelFieldsFragment = {
   __typename?: 'Channel'

+ 2 - 1
src/api/queries/__generated__/memberships.generated.tsx

@@ -2,7 +2,8 @@ import { gql } from '@apollo/client'
 import * as Apollo from '@apollo/client'
 
 import * as Types from './baseTypes.generated'
-import { BasicChannelFieldsFragment, BasicChannelFieldsFragmentDoc } from './channels.generated'
+import { BasicChannelFieldsFragment } from './channels.generated'
+import { BasicChannelFieldsFragmentDoc } from './channels.generated'
 
 export type BasicMembershipFieldsFragment = {
   __typename?: 'Membership'

+ 4 - 2
src/api/queries/__generated__/search.generated.tsx

@@ -2,8 +2,10 @@ import { gql } from '@apollo/client'
 import * as Apollo from '@apollo/client'
 
 import * as Types from './baseTypes.generated'
-import { BasicChannelFieldsFragment, BasicChannelFieldsFragmentDoc } from './channels.generated'
-import { VideoFieldsFragment, VideoFieldsFragmentDoc } from './videos.generated'
+import { BasicChannelFieldsFragment } from './channels.generated'
+import { BasicChannelFieldsFragmentDoc } from './channels.generated'
+import { VideoFieldsFragment } from './videos.generated'
+import { VideoFieldsFragmentDoc } from './videos.generated'
 
 export type SearchQueryVariables = Types.Exact<{
   text: Types.Scalars['String']

+ 2 - 1
src/api/queries/__generated__/shared.generated.tsx

@@ -1,7 +1,8 @@
 import { gql } from '@apollo/client'
 
 import * as Types from './baseTypes.generated'
-import { BasicWorkerFieldsFragment, BasicWorkerFieldsFragmentDoc } from './workers.generated'
+import { BasicWorkerFieldsFragment } from './workers.generated'
+import { BasicWorkerFieldsFragmentDoc } from './workers.generated'
 
 export type DataObjectFieldsFragment = {
   __typename?: 'DataObject'

+ 60 - 2
src/api/queries/__generated__/videos.generated.tsx

@@ -2,8 +2,10 @@ import { gql } from '@apollo/client'
 import * as Apollo from '@apollo/client'
 
 import * as Types from './baseTypes.generated'
-import { BasicChannelFieldsFragment, BasicChannelFieldsFragmentDoc } from './channels.generated'
-import { DataObjectFieldsFragment, DataObjectFieldsFragmentDoc } from './shared.generated'
+import { BasicChannelFieldsFragment } from './channels.generated'
+import { BasicChannelFieldsFragmentDoc } from './channels.generated'
+import { DataObjectFieldsFragment } from './shared.generated'
+import { DataObjectFieldsFragmentDoc } from './shared.generated'
 
 export type VideoMediaMetadataFieldsFragment = {
   __typename?: 'VideoMediaMetadata'
@@ -94,6 +96,15 @@ export type GetVideoViewsQuery = {
   videoViews?: Types.Maybe<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>
 }
 
+export type GetBatchedVideoViewsQueryVariables = Types.Exact<{
+  videoIdList: Array<Types.Scalars['ID']> | Types.Scalars['ID']
+}>
+
+export type GetBatchedVideoViewsQuery = {
+  __typename?: 'Query'
+  batchedVideoViews: Array<Types.Maybe<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>>
+}
+
 export type AddVideoViewMutationVariables = Types.Exact<{
   videoId: Types.Scalars['ID']
   channelId: Types.Scalars['ID']
@@ -340,6 +351,53 @@ export function useGetVideoViewsLazyQuery(
 export type GetVideoViewsQueryHookResult = ReturnType<typeof useGetVideoViewsQuery>
 export type GetVideoViewsLazyQueryHookResult = ReturnType<typeof useGetVideoViewsLazyQuery>
 export type GetVideoViewsQueryResult = Apollo.QueryResult<GetVideoViewsQuery, GetVideoViewsQueryVariables>
+export const GetBatchedVideoViewsDocument = gql`
+  query GetBatchedVideoViews($videoIdList: [ID!]!) {
+    batchedVideoViews(videoIdList: $videoIdList) {
+      id
+      views
+    }
+  }
+`
+
+/**
+ * __useGetBatchedVideoViewsQuery__
+ *
+ * To run a query within a React component, call `useGetBatchedVideoViewsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetBatchedVideoViewsQuery` 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 } = useGetBatchedVideoViewsQuery({
+ *   variables: {
+ *      videoIdList: // value for 'videoIdList'
+ *   },
+ * });
+ */
+export function useGetBatchedVideoViewsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetBatchedVideoViewsQuery, GetBatchedVideoViewsQueryVariables>
+) {
+  return Apollo.useQuery<GetBatchedVideoViewsQuery, GetBatchedVideoViewsQueryVariables>(
+    GetBatchedVideoViewsDocument,
+    baseOptions
+  )
+}
+export function useGetBatchedVideoViewsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetBatchedVideoViewsQuery, GetBatchedVideoViewsQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetBatchedVideoViewsQuery, GetBatchedVideoViewsQueryVariables>(
+    GetBatchedVideoViewsDocument,
+    baseOptions
+  )
+}
+export type GetBatchedVideoViewsQueryHookResult = ReturnType<typeof useGetBatchedVideoViewsQuery>
+export type GetBatchedVideoViewsLazyQueryHookResult = ReturnType<typeof useGetBatchedVideoViewsLazyQuery>
+export type GetBatchedVideoViewsQueryResult = Apollo.QueryResult<
+  GetBatchedVideoViewsQuery,
+  GetBatchedVideoViewsQueryVariables
+>
 export const AddVideoViewDocument = gql`
   mutation AddVideoView($videoId: ID!, $channelId: ID!) {
     addVideoView(videoId: $videoId, channelId: $channelId) {

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

@@ -94,6 +94,14 @@ query GetVideoViews($videoId: ID!) {
   }
 }
 
+# modyfying this query name will need a sync-up in `src/api/client/resolvers.ts`
+query GetBatchedVideoViews($videoIdList: [ID!]!) {
+  batchedVideoViews(videoIdList: $videoIdList) {
+    id
+    views
+  }
+}
+
 mutation AddVideoView($videoId: ID!, $channelId: ID!) {
   addVideoView(videoId: $videoId, channelId: $channelId) {
     id

BIN
src/assets/fonts/Inter-Bold.ttf


BIN
src/assets/fonts/Inter-ExtraLight-BETA.ttf


BIN
src/assets/fonts/Inter-Regular.ttf


+ 2 - 4
src/components/BackgroundPattern.tsx

@@ -2,7 +2,7 @@ import styled from '@emotion/styled'
 import React from 'react'
 
 import { SvgBgPattern } from '@/shared/illustrations'
-import { zIndex, transitions, media } from '@/shared/theme'
+import { media, transitions, zIndex } from '@/shared/theme'
 
 const PATTERN_OFFSET = {
   SMALL: '-150px',
@@ -37,12 +37,10 @@ const StyledBackgroundPattern = styled(SvgBgPattern)`
   }
 `
 
-const BackgroundPattern: React.FC = () => {
+export const BackgroundPattern: React.FC = () => {
   return (
     <StyledBackgroundPatternContainer>
       <StyledBackgroundPattern />
     </StyledBackgroundPatternContainer>
   )
 }
-
-export default BackgroundPattern

+ 2 - 4
src/components/ChannelGallery.tsx

@@ -5,7 +5,7 @@ import { BasicChannelFieldsFragment } from '@/api/queries'
 import { Gallery } from '@/shared/components'
 import { sizes } from '@/shared/theme'
 
-import ChannelPreview from './ChannelPreview'
+import { ChannelPreview } from './ChannelPreview'
 
 type ChannelGalleryProps = {
   title?: string
@@ -16,7 +16,7 @@ type ChannelGalleryProps = {
 
 const PLACEHOLDERS_COUNT = 12
 
-const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, channels = [], loading, onChannelClick }) => {
+export const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, channels = [], loading, onChannelClick }) => {
   if (!loading && channels?.length === 0) {
     return null
   }
@@ -38,5 +38,3 @@ const StyledChannelPreview = styled(ChannelPreview)`
     margin-left: 16px;
   }
 `
-
-export default ChannelGallery

+ 2 - 3
src/components/ChannelGrid.tsx

@@ -4,7 +4,7 @@ import React from 'react'
 import { BasicChannelFieldsFragment } from '@/api/queries'
 import { Grid } from '@/shared/components'
 
-import ChannelPreview from './ChannelPreview'
+import { ChannelPreview } from './ChannelPreview'
 
 const StyledChannelPreview = styled(ChannelPreview)`
   margin: 0 auto;
@@ -15,7 +15,7 @@ type ChannelGridProps = {
   onChannelClick?: (id: string) => void
 } & React.ComponentProps<typeof Grid>
 
-const ChannelGrid: React.FC<ChannelGridProps> = ({ channels, onChannelClick, ...gridProps }) => {
+export const ChannelGrid: React.FC<ChannelGridProps> = ({ channels, onChannelClick, ...gridProps }) => {
   const handleClick = (id: string) => {
     if (onChannelClick) {
       onChannelClick(id)
@@ -30,4 +30,3 @@ const ChannelGrid: React.FC<ChannelGridProps> = ({ channels, onChannelClick, ...
     </Grid>
   )
 }
-export default ChannelGrid

+ 10 - 14
src/components/ChannelLink/ChannelLink.tsx

@@ -3,8 +3,9 @@ import React from 'react'
 import { useBasicChannel } from '@/api/hooks'
 import { BasicChannelFieldsFragment } from '@/api/queries'
 import { absoluteRoutes } from '@/config/routes'
-import { useAsset } from '@/hooks'
-import Avatar, { AvatarSize } from '@/shared/components/Avatar'
+import { AssetType, useAsset } from '@/providers'
+import { Avatar, AvatarSize } from '@/shared/components/Avatar'
+import { Logger } from '@/utils/logger'
 
 import { Container, Handle, HandlePlaceholder } from './ChannelLink.style'
 
@@ -20,7 +21,7 @@ type ChannelLinkProps = {
   onNotFound?: () => void
 }
 
-const ChannelLink: React.FC<ChannelLinkProps> = ({
+export const ChannelLink: React.FC<ChannelLinkProps> = ({
   id,
   hideHandle,
   hideAvatar,
@@ -33,21 +34,18 @@ const ChannelLink: React.FC<ChannelLinkProps> = ({
   const { channel } = useBasicChannel(id || '', {
     skip: !id,
     onCompleted: (data) => !data && onNotFound?.(),
-    onError: (error) => console.error('Failed to fetch channel', error),
+    onError: (error) => Logger.error('Failed to fetch channel', error),
+  })
+  const { url: avatarPhotoUrl } = useAsset({
+    entity: channel,
+    assetType: AssetType.AVATAR,
   })
-  const { getAssetUrl } = useAsset()
 
   const displayedChannel = overrideChannel || channel
 
-  const avatarPhotoUrl = getAssetUrl(
-    displayedChannel?.avatarPhotoAvailability,
-    displayedChannel?.avatarPhotoUrls,
-    displayedChannel?.avatarPhotoDataObject
-  )
-
   return (
     <Container to={absoluteRoutes.viewer.channel(id)} disabled={!id || noLink} className={className}>
-      {!hideAvatar && <Avatar imageUrl={avatarPhotoUrl} loading={!displayedChannel} size={avatarSize} />}
+      {!hideAvatar && <Avatar loading={!displayedChannel} size={avatarSize} assetUrl={avatarPhotoUrl} />}
       {!hideHandle &&
         (displayedChannel ? (
           <Handle withAvatar={!hideAvatar}>{displayedChannel.title}</Handle>
@@ -57,5 +55,3 @@ const ChannelLink: React.FC<ChannelLinkProps> = ({
     </Container>
   )
 }
-
-export default ChannelLink

+ 1 - 3
src/components/ChannelLink/index.ts

@@ -1,3 +1 @@
-import ChannelLink from './ChannelLink'
-
-export default ChannelLink
+export * from './ChannelLink'

+ 3 - 10
src/components/ChannelPreview.tsx

@@ -3,7 +3,7 @@ import React from 'react'
 import { useChannel } from '@/api/hooks'
 import { useChannelVideoCount } from '@/api/hooks/channel'
 import { absoluteRoutes } from '@/config/routes'
-import { useAsset } from '@/hooks'
+import { AssetType, useAsset } from '@/providers'
 import { ChannelPreviewBase } from '@/shared/components'
 
 type ChannelPreviewProps = {
@@ -14,29 +14,22 @@ type ChannelPreviewProps = {
 
 export const ChannelPreview: React.FC<ChannelPreviewProps> = ({ id, className, onClick }) => {
   const { channel, loading } = useChannel(id ?? '', { fetchPolicy: 'cache-first', skip: !id })
+  const { url } = useAsset({ entity: channel, assetType: AssetType.AVATAR })
   const { videoCount } = useChannelVideoCount(id ?? '', {
     fetchPolicy: 'cache-first',
     skip: !id,
   })
   const isLoading = loading || id === undefined
-  const { getAssetUrl } = useAsset()
 
-  const avatarPhotoUrl = getAssetUrl(
-    channel?.avatarPhotoAvailability,
-    channel?.avatarPhotoUrls,
-    channel?.avatarPhotoDataObject
-  )
   return (
     <ChannelPreviewBase
       className={className}
-      avatarUrl={avatarPhotoUrl}
       title={channel?.title}
       channelHref={id ? absoluteRoutes.viewer.channel(id) : undefined}
       videoCount={videoCount}
       loading={isLoading}
       onClick={onClick}
+      assetUrl={url}
     />
   )
 }
-
-export default ChannelPreview

+ 10 - 7
src/components/CoverVideo/CoverVideo.style.ts

@@ -3,9 +3,9 @@ import styled from '@emotion/styled'
 import { darken, fluidRange } from 'polished'
 
 import { Button, IconButton, Placeholder, Text } from '@/shared/components'
-import { breakpoints, colors, sizes, media } from '@/shared/theme'
+import { breakpoints, colors, media, sizes } from '@/shared/theme'
 
-import ChannelLink from '../ChannelLink'
+import { ChannelLink } from '../ChannelLink'
 
 const CONTENT_OVERLAP_MAP = {
   SMALL: 25,
@@ -22,8 +22,8 @@ const BUTTONS_HEIGHT_PX = '54px'
 export const Container = styled.section`
   position: relative;
 
-  // 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
+  /* 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.small} {
     margin-bottom: -${CONTENT_OVERLAP_MAP.SMALL}px;
   }
@@ -80,12 +80,14 @@ const pulse = keyframes`
 
 export const PlayerPlaceholder = styled.div`
   ${absoluteMediaCss};
+
   background-color: ${colors.gray[800]};
   animation: ${pulse} 0.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 `
 
 export const HorizontalGradientOverlay = styled.div`
   ${absoluteMediaCss};
+
   display: none;
   background: linear-gradient(90deg, rgba(0, 0, 0, 0.8) 11.76%, rgba(0, 0, 0, 0) 100%);
 
@@ -97,10 +99,9 @@ export const HorizontalGradientOverlay = styled.div`
 export const VerticalGradientOverlay = styled.div`
   ${absoluteMediaCss};
 
-  // 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
+  /* 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.small} {
     background: linear-gradient(
       0deg,
@@ -184,6 +185,7 @@ export const TitleContainer = styled.div`
   a {
     text-decoration: none;
   }
+
   margin-bottom: ${sizes(8)};
 
   ${media.medium} {
@@ -195,6 +197,7 @@ export const TitleContainer = styled.div`
     max-width: 40ch;
     ${fluidRange({ prop: 'fontSize', fromSize: '14px', toSize: '22px' }, breakpoints.base, breakpoints.xlarge)};
     ${fluidRange({ prop: 'lineHeight', fromSize: '20px', toSize: '26px' }, breakpoints.base, breakpoints.xlarge)};
+
     color: ${colors.white};
   }
 `

+ 11 - 16
src/components/CoverVideo/CoverVideo.tsx

@@ -3,40 +3,43 @@ import { Link } from 'react-router-dom'
 import { CSSTransition } from 'react-transition-group'
 
 import { absoluteRoutes } from '@/config/routes'
-import { useAsset } from '@/hooks'
+import { AssetType, useAsset } from '@/providers'
 import { Placeholder, VideoPlayer } from '@/shared/components'
 import { SvgPlayerPause, SvgPlayerPlay, SvgPlayerSoundOff, SvgPlayerSoundOn } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 
 import {
+  ButtonsContainer,
   Container,
+  ControlsContainer,
   HorizontalGradientOverlay,
   InfoContainer,
   Media,
   MediaWrapper,
   PlayButton,
   PlayerContainer,
+  PlayerPlaceholder,
   SoundButton,
   StyledChannelLink,
-  TitleContainer,
-  VerticalGradientOverlay,
   Title,
+  TitleContainer,
   TitlePlaceholder,
-  PlayerPlaceholder,
-  ControlsContainer,
-  ButtonsContainer,
+  VerticalGradientOverlay,
 } from './CoverVideo.style'
 import { useCoverVideo } from './coverVideoData'
 
 const VIDEO_PLAYBACK_DELAY = 1250
 
-const CoverVideo: React.FC = () => {
+export const CoverVideo: React.FC = () => {
   const coverVideo = useCoverVideo()
 
   const [videoPlaying, setVideoPlaying] = useState(false)
   const [displayControls, setDisplayControls] = useState(false)
   const [soundMuted, setSoundMuted] = useState(true)
-  const { getAssetUrl } = useAsset()
+  const { url: thumbnailPhotoUrl } = useAsset({
+    entity: coverVideo?.video,
+    assetType: AssetType.THUMBNAIL,
+  })
 
   const handlePlaybackDataLoaded = () => {
     setTimeout(() => {
@@ -61,12 +64,6 @@ const CoverVideo: React.FC = () => {
     setVideoPlaying(false)
   }
 
-  const thumbnailPhotoUrl = getAssetUrl(
-    coverVideo?.video?.thumbnailPhotoAvailability,
-    coverVideo?.video?.thumbnailPhotoUrls,
-    coverVideo?.video?.thumbnailPhotoDataObject
-  )
-
   return (
     <Container>
       <MediaWrapper>
@@ -141,5 +138,3 @@ const CoverVideo: React.FC = () => {
     </Container>
   )
 }
-
-export default CoverVideo

+ 2 - 1
src/components/CoverVideo/coverVideoData.ts

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
 import { useVideo } from '@/api/hooks/video'
 import { VideoFieldsFragment } from '@/api/queries'
 import { COVER_VIDEO_INFO_URL } from '@/config/urls'
+import { Logger } from '@/utils/logger'
 
 import backupCoverVideoInfo from './backupCoverVideoInfo.json'
 
@@ -34,7 +35,7 @@ export const useCoverVideo = (): CoverInfo => {
         const response = await axios.get<RawCoverInfo>(COVER_VIDEO_INFO_URL)
         setFetchedCoverInfo(response.data)
       } catch (e) {
-        console.error(`Failed to fetch cover info from ${COVER_VIDEO_INFO_URL}. Using backup`, e)
+        Logger.error(`Failed to fetch cover info from ${COVER_VIDEO_INFO_URL}. Using backup`, e)
         setFetchedCoverInfo(backupCoverVideoInfo)
       }
     }

+ 1 - 3
src/components/CoverVideo/index.ts

@@ -1,3 +1 @@
-import CoverVideo from './CoverVideo'
-
-export default CoverVideo
+export * from './CoverVideo'

+ 3 - 3
src/components/Dialogs/ActionDialog/ActionDialog.stories.tsx

@@ -1,10 +1,10 @@
-import { Story, Meta } from '@storybook/react'
+import { Meta, Story } from '@storybook/react'
 import React, { useState } from 'react'
 
-import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
 import { Button } from '@/shared/components'
 
-import ActionDialog, { ActionDialogProps } from './ActionDialog'
+import { ActionDialog, ActionDialogProps } from './ActionDialog'
 
 export default {
   title: 'General/ActionDialog',

+ 3 - 2
src/components/Dialogs/ActionDialog/ActionDialog.style.ts

@@ -2,7 +2,7 @@ import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
 import { Button } from '@/shared/components'
-import { media, sizes, colors, typography } from '@/shared/theme'
+import { media, sizes } from '@/shared/theme'
 
 type ButtonProps = {
   error?: boolean
@@ -35,7 +35,6 @@ export const ButtonsContainer = styled.div`
 export const ActionsContainer = styled.div`
   display: flex;
   flex-direction: column;
-
   padding-top: ${sizes(6)};
 
   ${media.small} {
@@ -78,12 +77,14 @@ const buttonColorsFromProps = ({ error, warning }: ButtonProps) => {
     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};

+ 3 - 5
src/components/Dialogs/ActionDialog/ActionDialog.tsx

@@ -4,12 +4,12 @@ import { Button } from '@/shared/components'
 
 import {
   ActionsContainer,
-  ButtonsContainer,
   AdditionalActionsContainer,
+  ButtonsContainer,
   StyledPrimaryButton,
 } from './ActionDialog.style'
 
-import BaseDialog, { BaseDialogProps } from '../BaseDialog'
+import { BaseDialog, BaseDialogProps } from '../BaseDialog'
 
 export type ActionDialogProps = {
   additionalActionsNode?: React.ReactNode
@@ -23,7 +23,7 @@ export type ActionDialogProps = {
   error?: boolean
 } & BaseDialogProps
 
-const ActionDialog: React.FC<ActionDialogProps> = ({
+export const ActionDialog: React.FC<ActionDialogProps> = ({
   additionalActionsNode,
   primaryButtonText,
   secondaryButtonText,
@@ -66,5 +66,3 @@ const ActionDialog: React.FC<ActionDialogProps> = ({
     </BaseDialog>
   )
 }
-
-export default ActionDialog

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

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

+ 3 - 3
src/components/Dialogs/BaseDialog/BaseDialog.stories.tsx

@@ -1,10 +1,10 @@
-import { Story, Meta } from '@storybook/react'
+import { Meta, Story } from '@storybook/react'
 import React, { useState } from 'react'
 
-import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
 import { Button } from '@/shared/components'
 
-import Dialog, { BaseDialogProps } from './BaseDialog'
+import { BaseDialogProps, BaseDialog as Dialog } from './BaseDialog'
 
 export default {
   title: 'General/BaseDialog',

+ 2 - 2
src/components/Dialogs/BaseDialog/BaseDialog.style.ts

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled'
 
 import { IconButton } from '@/shared/components'
-import { colors, sizes, media, zIndex } from '@/shared/theme'
+import { colors, media, sizes, zIndex } from '@/shared/theme'
 
 export const DialogBackDrop = styled.div`
   position: fixed;
@@ -19,8 +19,8 @@ export const StyledContainer = styled.div`
   ${media.small} {
     --dialog-padding: ${sizes(6)};
   }
-  z-index: ${zIndex.globalOverlay};
 
+  z-index: ${zIndex.globalOverlay};
   position: fixed;
   left: 50%;
   transform: translateX(-50%);

+ 10 - 5
src/components/Dialogs/BaseDialog/BaseDialog.tsx

@@ -1,8 +1,8 @@
 import React, { useEffect } from 'react'
 import { CSSTransition } from 'react-transition-group'
 
-import Portal from '@/components/Portal'
-import { useOverlayManager } from '@/hooks'
+import { Portal } from '@/components'
+import { useOverlayManager } from '@/providers'
 import { SvgGlyphClose } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 
@@ -15,7 +15,13 @@ export type BaseDialogProps = {
   className?: string
 }
 
-const BaseDialog: React.FC<BaseDialogProps> = ({ children, showDialog, exitButton = true, onExitClick, className }) => {
+export const BaseDialog: React.FC<BaseDialogProps> = ({
+  children,
+  showDialog,
+  exitButton = true,
+  onExitClick,
+  className,
+}) => {
   const { dialogContainerRef, incrementOverlaysOpenCount, decrementOverlaysOpenCount } = useOverlayManager()
 
   useEffect(() => {
@@ -35,6 +41,7 @@ const BaseDialog: React.FC<BaseDialogProps> = ({ children, showDialog, exitButto
         classNames={transitions.names.dialog}
         mountOnEnter
         unmountOnExit
+        appear
         onEnter={incrementOverlaysOpenCount}
         onExited={decrementOverlaysOpenCount}
       >
@@ -50,5 +57,3 @@ const BaseDialog: React.FC<BaseDialogProps> = ({ children, showDialog, exitButto
     </Portal>
   )
 }
-
-export default BaseDialog

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

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

+ 13 - 23
src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx

@@ -1,13 +1,12 @@
-import { css } from '@emotion/react'
 import styled from '@emotion/styled/'
-import { Story, Meta } from '@storybook/react'
-import React, { useState, useRef } from 'react'
+import { Meta, Story } from '@storybook/react'
+import React, { useRef, useState } from 'react'
 
-import { OverlayManagerProvider } from '@/hooks'
+import { OverlayManagerProvider } from '@/providers'
 import { Avatar, Placeholder } from '@/shared/components'
-import { ImageCropData, AssetDimensions } from '@/types/cropper'
+import { AssetDimensions, ImageCropData } from '@/types/cropper'
 
-import ImageCropDialog, { ImageCropDialogImperativeHandle, ImageCropDialogProps } from './ImageCropDialog'
+import { ImageCropDialog, ImageCropDialogImperativeHandle, ImageCropDialogProps } from './ImageCropDialog'
 
 export default {
   title: 'General/ImageCropDialog',
@@ -36,8 +35,8 @@ const RegularTemplate: Story<ImageCropDialogProps> = () => {
   const handleAvatarConfirm = (
     blob: Blob,
     url: string,
-    assetDimensions: AssetDimensions,
-    imageCropData: ImageCropData
+    _assetDimensions: AssetDimensions,
+    _imageCropData: ImageCropData
   ) => {
     setAvatarImageUrl(url)
   }
@@ -45,8 +44,8 @@ const RegularTemplate: Story<ImageCropDialogProps> = () => {
   const handleThumbnailConfirm = (
     blob: Blob,
     url: string,
-    assetDimensions: AssetDimensions,
-    imageCropData: ImageCropData
+    _assetDimensions: AssetDimensions,
+    _imageCropData: ImageCropData
   ) => {
     setThumbnailImageUrl(url)
   }
@@ -54,24 +53,15 @@ const RegularTemplate: Story<ImageCropDialogProps> = () => {
   const handleCoverConfirm = (
     blob: Blob,
     url: string,
-    assetDimensions: AssetDimensions,
-    imageCropData: ImageCropData
+    _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" />
+    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'start', gap: '24px' }}>
+      <Avatar assetUrl={avatarImageUrl} editable onEditClick={() => avatarDialogRef.current?.open()} size="cover" />
 
       {thumbnailImageUrl ? (
         <Image src={thumbnailImageUrl} onClick={() => thumbnailDialogRef.current?.open()} />

+ 3 - 4
src/components/Dialogs/ImageCropDialog/ImageCropDialog.style.ts

@@ -2,10 +2,10 @@ import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
 import { Placeholder, Text } from '@/shared/components'
-import Slider from '@/shared/components/Slider'
+import { Slider } from '@/shared/components/Slider'
 import { colors, sizes } from '@/shared/theme'
 
-import ActionDialog from '../ActionDialog'
+import { ActionDialog } from '../ActionDialog'
 
 export const StyledActionDialog = styled(ActionDialog)`
   max-width: 536px;
@@ -59,6 +59,7 @@ export const CropContainer = styled.div<{ rounded?: boolean; disabled?: boolean
   ${cropAreaSizeCss};
 
   ${({ rounded }) => rounded && roundedCropperCss};
+
   .cropper-view-box {
     outline: none;
 
@@ -70,7 +71,6 @@ export const CropContainer = styled.div<{ rounded?: boolean; disabled?: boolean
       height: 100%;
       top: 0;
       border-radius: ${({ rounded }) => (rounded ? '50%' : '0')};
-
       box-shadow: inset 0 0 0 2px ${colors.transparentWhite[32]};
     }
   }
@@ -84,7 +84,6 @@ export const CropContainer = styled.div<{ rounded?: boolean; disabled?: boolean
 
 export const StyledImage = styled.img`
   display: block;
-
   max-width: 100%;
 `
 

+ 6 - 7
src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx

@@ -2,8 +2,9 @@ import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState }
 
 import { IconButton } from '@/shared/components'
 import { SvgGlyphPan, SvgGlyphZoomIn, SvgGlyphZoomOut } from '@/shared/icons'
-import { ImageCropData, AssetDimensions } from '@/types/cropper'
+import { AssetDimensions, ImageCropData } from '@/types/cropper'
 import { validateImage } from '@/utils/image'
+import { Logger } from '@/utils/logger'
 
 import {
   AlignInfo,
@@ -39,7 +40,7 @@ export type ImageCropDialogImperativeHandle = {
 const ImageCropDialogComponent: React.ForwardRefRenderFunction<
   ImageCropDialogImperativeHandle,
   ImageCropDialogProps
-> = ({ imageType, onConfirm, onExitClick, onError }, ref) => {
+> = ({ imageType, onConfirm, onError }, ref) => {
   const [showDialog, setShowDialog] = useState(false)
   const inputRef = useRef<HTMLInputElement>(null)
   const [imageEl, setImageEl] = useState<HTMLImageElement | null>(null)
@@ -85,7 +86,7 @@ const ImageCropDialogComponent: React.ForwardRefRenderFunction<
   const handleFileChange = async () => {
     const files = inputRef.current?.files
     if (!files?.length) {
-      console.error('no files selected')
+      Logger.error('no files selected')
       return
     }
     try {
@@ -96,7 +97,7 @@ const ImageCropDialogComponent: React.ForwardRefRenderFunction<
       setShowDialog(true)
     } catch (error) {
       onError?.(error)
-      console.error(error)
+      Logger.error(error)
     }
   }
 
@@ -160,7 +161,5 @@ const ImageCropDialogComponent: React.ForwardRefRenderFunction<
   )
 }
 
-const ImageCropDialog = forwardRef(ImageCropDialogComponent)
+export const ImageCropDialog = forwardRef(ImageCropDialogComponent)
 ImageCropDialog.displayName = 'ImageCropDialog'
-
-export default ImageCropDialog

+ 2 - 1
src/components/Dialogs/ImageCropDialog/cropper.ts

@@ -3,6 +3,7 @@ import 'cropperjs/dist/cropper.min.css'
 import { useEffect, useState } from 'react'
 
 import { AssetDimensions, ImageCropData } from '@/types/cropper'
+import { Logger } from '@/utils/logger'
 
 const MAX_ZOOM = 3
 
@@ -165,7 +166,7 @@ export const useCropper = ({ imageEl, imageType, cropData }: UseCropperOpts) =>
       }
       canvas.toBlob((blob) => {
         if (!blob) {
-          console.error('Empty blob from cropped canvas', { blob })
+          Logger.error('Empty blob from cropped canvas', { blob })
           return
         }
         const url = URL.createObjectURL(blob)

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

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

+ 3 - 3
src/components/Dialogs/MessageDialog/MessageDialog.stories.tsx

@@ -1,9 +1,9 @@
-import { Story, Meta } from '@storybook/react'
+import { Meta, Story } from '@storybook/react'
 import React from 'react'
 
-import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
 
-import MessageDialog, { MessageDialogProps } from './MessageDialog'
+import { MessageDialog, MessageDialogProps } from './MessageDialog'
 
 export default {
   title: 'General/MessageDialog',

+ 3 - 5
src/components/Dialogs/MessageDialog/MessageDialog.tsx

@@ -2,9 +2,9 @@ import React, { ReactNode } from 'react'
 
 import { SvgOutlineError, SvgOutlineSuccess, SvgOutlineWarning } from '@/shared/icons'
 
-import { StyledTitleText, StyledDescriptionText, MessageIconWrapper } from './MessageDialog.style'
+import { MessageIconWrapper, StyledDescriptionText, StyledTitleText } from './MessageDialog.style'
 
-import ActionDialog, { ActionDialogProps } from '../ActionDialog/ActionDialog'
+import { ActionDialog, ActionDialogProps } from '../ActionDialog/ActionDialog'
 
 type DialogVariant = 'success' | 'warning' | 'error' | 'info'
 
@@ -22,7 +22,7 @@ const VARIANT_TO_ICON: Record<DialogVariant, ReactNode | null> = {
   info: null,
 }
 
-const MessageDialog: React.FC<MessageDialogProps> = ({
+export const MessageDialog: React.FC<MessageDialogProps> = ({
   title,
   description,
   variant = 'info',
@@ -39,5 +39,3 @@ const MessageDialog: React.FC<MessageDialogProps> = ({
     </ActionDialog>
   )
 }
-
-export default MessageDialog

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

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

+ 3 - 3
src/components/Dialogs/Multistepper/Multistepper.stories.tsx

@@ -1,10 +1,10 @@
-import { Story, Meta } from '@storybook/react'
+import { Meta, Story } from '@storybook/react'
 import React, { useState } from 'react'
 
-import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
 import { Button } from '@/shared/components'
 
-import Multistepper from './Multistepper'
+import { Multistepper } from './Multistepper'
 
 export default {
   title: 'General/Multistepper',

+ 5 - 7
src/components/Dialogs/Multistepper/Multistepper.style.ts

@@ -2,9 +2,9 @@ import styled from '@emotion/styled'
 
 import { Text } from '@/shared/components'
 import { SvgGlyphChevronRight } from '@/shared/icons'
-import { colors, sizes, media, typography } from '@/shared/theme'
+import { colors, media, sizes, typography } from '@/shared/theme'
 
-import BaseDialog from '../BaseDialog'
+import { BaseDialog } from '../BaseDialog'
 
 type CircleProps = {
   isFilled?: boolean
@@ -23,10 +23,10 @@ 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
+
+  /* account for close button */
   padding: 0 calc(var(--dialog-padding) + 40px) var(--dialog-padding) var(--dialog-padding);
 
   hr {
@@ -70,8 +70,7 @@ export const StyledCircle = styled.div<CircleProps>`
   width: 32px;
   height: 32px;
   border-radius: 100%;
-  background-color: ${colors.gray[400]};
-  background-color: ${({ isActive }) => isActive && colors.blue[500]};
+  background-color: ${({ isActive }) => (isActive ? colors.blue[500] : colors.gray[400])};
   color: ${colors.gray[50]};
 `
 export const StyledStepInfoText = styled.div<StyledStepInfoProps>`
@@ -90,7 +89,6 @@ export const StyledStepTitle = styled(Text)`
 export const StyledChevron = styled(SvgGlyphChevronRight)`
   margin: 0 ${sizes(1)};
   flex-shrink: 0;
-
   display: none;
   ${media.small} {
     display: block;

+ 4 - 6
src/components/Dialogs/Multistepper/Multistepper.tsx

@@ -4,14 +4,14 @@ import { Text } from '@/shared/components'
 import { SvgGlyphCheck } from '@/shared/icons'
 
 import {
+  StyledChevron,
+  StyledCircle,
   StyledDialog,
   StyledHeader,
-  StyledStepsInfoContainer,
   StyledStepInfo,
-  StyledCircle,
   StyledStepInfoText,
-  StyledChevron,
   StyledStepTitle,
+  StyledStepsInfoContainer,
 } from './Multistepper.style'
 
 import { BaseDialogProps } from '../BaseDialog'
@@ -26,7 +26,7 @@ type MultistepperProps = {
   currentStepIdx?: number
 } & BaseDialogProps
 
-const Multistepper: React.FC<MultistepperProps> = ({ steps, currentStepIdx = 0, ...dialogProps }) => {
+export const Multistepper: React.FC<MultistepperProps> = ({ steps, currentStepIdx = 0, ...dialogProps }) => {
   return (
     <StyledDialog {...dialogProps}>
       <StyledHeader>
@@ -59,5 +59,3 @@ const Multistepper: React.FC<MultistepperProps> = ({ steps, currentStepIdx = 0,
     </StyledDialog>
   )
 }
-
-export default Multistepper

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

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

+ 2 - 2
src/components/Dialogs/TransactionDialog/TransactionDialog.stories.tsx

@@ -1,10 +1,10 @@
 import { Meta, Story } from '@storybook/react'
 import React from 'react'
 
-import { OverlayManagerProvider } from '@/hooks/useOverlayManager'
 import { ExtrinsicStatus } from '@/joystream-lib'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
 
-import TransactionDialog, { TransactionDialogProps } from './TransactionDialog'
+import { TransactionDialog, TransactionDialogProps } from './TransactionDialog'
 
 export default {
   title: 'General/TransactionDialog',

+ 3 - 3
src/components/Dialogs/TransactionDialog/TransactionDialog.style.ts

@@ -1,9 +1,9 @@
 import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
-import Spinner from '@/shared/components/Spinner'
+import { Spinner } from '@/shared/components/Spinner'
 import { SvgTransactionIllustration } from '@/shared/illustrations'
-import { sizes, media, colors, transitions } from '@/shared/theme'
+import { colors, media, sizes, transitions } from '@/shared/theme'
 
 type StepProps = {
   isActive?: boolean
@@ -12,7 +12,6 @@ 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)};
@@ -24,6 +23,7 @@ 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 &&

+ 6 - 9
src/components/Dialogs/TransactionDialog/TransactionDialog.tsx

@@ -3,10 +3,10 @@ import React from 'react'
 import { ExtrinsicStatus } from '@/joystream-lib'
 import { Tooltip } from '@/shared/components'
 
-import { TextContainer, StyledTransactionIllustration, StyledSpinner, StepsBar, Step } from './TransactionDialog.style'
+import { Step, StepsBar, StyledSpinner, StyledTransactionIllustration, TextContainer } from './TransactionDialog.style'
 
-import ActionDialog, { ActionDialogProps } from '../ActionDialog/ActionDialog'
-import { StyledTitleText, StyledDescriptionText } from '../MessageDialog/MessageDialog.style'
+import { ActionDialog, ActionDialogProps } from '../ActionDialog/ActionDialog'
+import { StyledDescriptionText, StyledTitleText } from '../MessageDialog/MessageDialog.style'
 
 export type TransactionDialogProps = Pick<ActionDialogProps, 'className'> & {
   status: ExtrinsicStatus | null
@@ -39,18 +39,17 @@ const TRANSACTION_STEPS_DETAILS = {
   },
 }
 
-const TransactionDialog: React.FC<TransactionDialogProps> = ({ status, onClose, ...actionDialogProps }) => {
+export const TransactionDialog: React.FC<TransactionDialogProps> = ({ status, onClose, ...actionDialogProps }) => {
   const stepDetails =
     status != null && status !== ExtrinsicStatus.Error && status !== ExtrinsicStatus.Completed
       ? TRANSACTION_STEPS_DETAILS[status]
       : null
 
-  const canCancel = status === ExtrinsicStatus.ProcessingAssets || ExtrinsicStatus.Unsigned
+  const canCancel = status === ExtrinsicStatus.ProcessingAssets || status === ExtrinsicStatus.Unsigned
 
   const transactionStepsWithoutProcessingAssets = Object.values(TRANSACTION_STEPS_DETAILS).filter(
     (step) => step.title !== TRANSACTION_STEPS_DETAILS[ExtrinsicStatus.ProcessingAssets].title
   )
-
   return (
     <ActionDialog
       showDialog={!!stepDetails}
@@ -61,7 +60,7 @@ const TransactionDialog: React.FC<TransactionDialogProps> = ({ status, onClose,
       {...actionDialogProps}
     >
       <StepsBar>
-        {transactionStepsWithoutProcessingAssets.map(({ title, tooltip }, idx) => (
+        {transactionStepsWithoutProcessingAssets.map(({ tooltip }, idx) => (
           <Tooltip key={idx} text={tooltip} placement="top-end">
             <Step isActive={!!status && status > idx} />
           </Tooltip>
@@ -76,5 +75,3 @@ const TransactionDialog: React.FC<TransactionDialogProps> = ({ status, onClose,
     </ActionDialog>
   )
 }
-
-export default TransactionDialog

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

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

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

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

+ 4 - 6
src/components/ErrorFallback.tsx

@@ -3,7 +3,8 @@ import { FallbackRender } from '@sentry/react/dist/errorboundary'
 import React from 'react'
 
 import { Button, Text } from '@/shared/components'
-import { sizes, colors } from '@/shared/theme'
+import { colors, sizes } from '@/shared/theme'
+import { Logger } from '@/utils/logger'
 
 const Container = styled.div`
   padding: ${sizes(4)};
@@ -17,9 +18,8 @@ const StyledButton = styled(Button)`
 `
 type FallbackProps = Partial<Parameters<FallbackRender>[0]>
 
-const ErrorFallback: React.FC<FallbackProps> = ({ error, componentStack, resetError }) => {
-  console.error(`An error occurred in ${componentStack}`)
-  console.error(error)
+export const ErrorFallback: React.FC<FallbackProps> = ({ error, componentStack, resetError }) => {
+  Logger.error(`An error occurred in ${componentStack}`, error)
   return (
     <Container>
       <Text>Something went wrong...</Text>
@@ -29,5 +29,3 @@ const ErrorFallback: React.FC<FallbackProps> = ({ error, componentStack, resetEr
     </Container>
   )
 }
-
-export default ErrorFallback

+ 9 - 6
src/components/InfiniteGrids/InfiniteChannelGrid.tsx

@@ -6,13 +6,13 @@ import {
   GetChannelsConnectionDocument,
   GetChannelsConnectionQuery,
   GetChannelsConnectionQueryVariables,
-  AssetAvailability,
 } from '@/api/queries'
-import ChannelPreview from '@/components/ChannelPreview'
 import { Grid, Text } from '@/shared/components'
 import { sizes } from '@/shared/theme'
 
-import useInfiniteGrid from './useInfiniteGrid'
+import { useInfiniteGrid } from './useInfiniteGrid'
+
+import { ChannelPreview } from '../ChannelPreview'
 
 type InfiniteChannelGridProps = {
   title?: string
@@ -30,7 +30,12 @@ const QUERY_VARIABLES = {
   },
 }
 
-const InfiniteChannelGrid: React.FC<InfiniteChannelGridProps> = ({ title, skipCount = 0, ready = true, className }) => {
+export const InfiniteChannelGrid: React.FC<InfiniteChannelGridProps> = ({
+  title,
+  skipCount = 0,
+  ready = true,
+  className,
+}) => {
   const [channelsPerRow, setChannelsPerRow] = useState(INITIAL_CHANNELS_PER_ROW)
   const [targetRowsCount, setTargetRowsCount] = useState(INITIAL_ROWS)
 
@@ -92,5 +97,3 @@ const previewCss = css`
 const StyledChannelPreview = styled(ChannelPreview)`
   ${previewCss};
 `
-
-export default InfiniteChannelGrid

+ 20 - 8
src/components/InfiniteGrids/InfiniteVideoGrid.tsx

@@ -2,17 +2,18 @@ import styled from '@emotion/styled'
 import React, { useCallback, useEffect, useState } from 'react'
 
 import {
+  AssetAvailability,
   GetVideosConnectionDocument,
   GetVideosConnectionQuery,
   GetVideosConnectionQueryVariables,
   VideoWhereInput,
-  AssetAvailability,
 } from '@/api/queries'
-import VideoPreview from '@/components/VideoPreview'
-import { Grid, Text, Placeholder } from '@/shared/components'
+import { Grid, Placeholder, Text } from '@/shared/components'
 import { sizes } from '@/shared/theme'
 
-import useInfiniteGrid from './useInfiniteGrid'
+import { useInfiniteGrid } from './useInfiniteGrid'
+
+import { VideoPreview } from '../VideoPreview'
 
 type InfiniteVideoGridProps = {
   title?: string
@@ -28,12 +29,13 @@ type InfiniteVideoGridProps = {
   ready?: boolean
   showChannel?: boolean
   className?: string
+  currentlyWatchedVideoId?: string
 }
 
 const INITIAL_ROWS = 4
 const INITIAL_VIDEOS_PER_ROW = 4
 
-const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
+export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   title,
   categoryId = '',
   channelId = null,
@@ -47,6 +49,7 @@ const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   ready = true,
   showChannel = true,
   className,
+  currentlyWatchedVideoId,
 }) => {
   const [videosPerRow, setVideosPerRow] = useState(INITIAL_VIDEOS_PER_ROW)
   const queryVariables: { where: VideoWhereInput } = {
@@ -87,7 +90,18 @@ const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
     skipCount,
     queryVariables,
     targetRowsCount,
-    dataAccessor: (rawData) => rawData?.videosConnection,
+    dataAccessor: (rawData) => {
+      if (currentlyWatchedVideoId) {
+        return (
+          rawData?.videosConnection && {
+            ...rawData.videosConnection,
+            totalCount: rawData.videosConnection.totalCount - 1,
+            edges: rawData.videosConnection.edges.filter((edge) => edge.node.id !== currentlyWatchedVideoId),
+          }
+        )
+      }
+      return rawData?.videosConnection
+    },
     itemsPerRow: videosPerRow,
   })
 
@@ -153,5 +167,3 @@ const Title = styled(Text)`
 const StyledPlaceholder = styled(Placeholder)`
   margin-bottom: ${sizes(4)};
 `
-
-export default InfiniteVideoGrid

+ 2 - 4
src/components/InfiniteGrids/index.ts

@@ -1,4 +1,2 @@
-import InfiniteChannelGrid from './InfiniteChannelGrid'
-import InfiniteVideoGrid from './InfiniteVideoGrid'
-
-export { InfiniteVideoGrid, InfiniteChannelGrid }
+export * from './InfiniteChannelGrid'
+export * from './InfiniteVideoGrid'

+ 5 - 3
src/components/InfiniteGrids/useInfiniteGrid.ts

@@ -45,7 +45,11 @@ type UseInfiniteGridReturn<TPaginatedData extends PaginatedData<unknown>> = {
   error?: ApolloError
 }
 
-const useInfiniteGrid = <TRawData, TPaginatedData extends PaginatedData<unknown>, TArgs extends PaginatedDataArgs>({
+export const useInfiniteGrid = <
+  TRawData,
+  TPaginatedData extends PaginatedData<unknown>,
+  TArgs extends PaginatedDataArgs
+>({
   query,
   dataAccessor,
   isReady,
@@ -118,5 +122,3 @@ const useInfiniteGrid = <TRawData, TPaginatedData extends PaginatedData<unknown>
     error,
   }
 }
-
-export default useInfiniteGrid

+ 6 - 9
src/components/InterruptedVideosGallery.tsx

@@ -2,15 +2,14 @@ import { RouteComponentProps } from '@reach/router'
 import React from 'react'
 
 import { VideoGallery } from '@/components'
-import { usePersonalData } from '@/hooks'
+import { usePersonalDataStore } from '@/providers'
+import { Logger } from '@/utils/logger'
 
 const INTERRUPTED_VIDEOS_COUNT = 16
 
-const InterruptedVideosGallery: React.FC<RouteComponentProps> = () => {
-  const {
-    state: { watchedVideos },
-    updateWatchedVideos,
-  } = usePersonalData()
+export const InterruptedVideosGallery: React.FC<RouteComponentProps> = () => {
+  const watchedVideos = usePersonalDataStore((state) => state.watchedVideos)
+  const updateWatchedVideos = usePersonalDataStore((state) => state.actions.updateWatchedVideos)
 
   const interruptedVideosState = watchedVideos
     .filter((video) => video.__typename === 'INTERRUPTED')
@@ -25,7 +24,7 @@ const InterruptedVideosGallery: React.FC<RouteComponentProps> = () => {
   }
 
   const onVideoNotFound = (id: string) => {
-    console.warn(`Interrupted video not found, removing id: ${id}`)
+    Logger.warn(`Interrupted video not found, removing id: ${id}`)
     updateWatchedVideos('REMOVED', id)
   }
 
@@ -40,5 +39,3 @@ const InterruptedVideosGallery: React.FC<RouteComponentProps> = () => {
     />
   )
 }
-
-export default InterruptedVideosGallery

+ 1 - 1
src/components/Link/Link.style.ts

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
-import { typography, colors } from '@/shared/theme'
+import { colors, typography } from '@/shared/theme'
 
 export const StyledLink = styled(Link)`
   font-family: ${typography.fonts.base};

+ 1 - 3
src/components/Link/Link.tsx

@@ -12,7 +12,7 @@ export type LinkProps = {
   onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
 }
 
-const Link: React.FC<LinkProps> = ({ to = '', disabled = false, replace = false, ref, children, className }) => {
+export const Link: React.FC<LinkProps> = ({ to = '', disabled = false, replace = false, ref, children, className }) => {
   if (disabled) return <DisabledLabel>{children}</DisabledLabel>
   return (
     <StyledLink to={to} className={className} replace={replace} ref={ref}>
@@ -20,5 +20,3 @@ const Link: React.FC<LinkProps> = ({ to = '', disabled = false, replace = false,
     </StyledLink>
   )
 }
-
-export default Link

+ 1 - 3
src/components/Link/index.ts

@@ -1,3 +1 @@
-import Link from './Link'
-
-export default Link
+export * from './Link'

+ 8 - 8
src/components/NoConnectionIndicator/NoConnectionIndicator.stories.tsx

@@ -1,9 +1,9 @@
-import { Story, Meta } from '@storybook/react'
+import { Meta, Story } from '@storybook/react'
 import React from 'react'
 
-import { ConnectionStatusProvider, SnackbarProvider } from '@/hooks'
+import { ConnectionStatusManager, Snackbars } from '@/providers'
 
-import NoConnectionIndicator, { NoConnectionIndicatorProps } from './NoConnectionIndicator'
+import { NoConnectionIndicator, NoConnectionIndicatorProps } from './NoConnectionIndicator'
 
 export default {
   title: 'General/NoConnectionIndicator',
@@ -13,11 +13,11 @@ export default {
   },
   decorators: [
     (Story) => (
-      <SnackbarProvider>
-        <ConnectionStatusProvider>
-          <Story />
-        </ConnectionStatusProvider>
-      </SnackbarProvider>
+      <>
+        <Story />
+        <Snackbars />
+        <ConnectionStatusManager />
+      </>
     ),
   ],
 } as Meta

+ 1 - 1
src/components/NoConnectionIndicator/NoConnectionIndicator.style.ts

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled'
 
 import { TOP_NAVBAR_HEIGHT } from '@/components'
-import { colors, sizes, zIndex, media } from '@/shared/theme'
+import { colors, media, sizes, zIndex } from '@/shared/theme'
 
 export const TextWrapper = styled.div`
   width: 100%;

+ 3 - 5
src/components/NoConnectionIndicator/NoConnectionIndicator.tsx

@@ -1,19 +1,19 @@
 import React from 'react'
 import { CSSTransition } from 'react-transition-group'
 
-import { ConnectionStatus } from '@/hooks'
+import { ConnectionStatus } from '@/providers'
 import { Text } from '@/shared/components'
 import { SvgAlertWarning } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 
-import { IndicatorWrapper, TextWrapper, IconWrapper } from './NoConnectionIndicator.style'
+import { IconWrapper, IndicatorWrapper, TextWrapper } from './NoConnectionIndicator.style'
 
 export type NoConnectionIndicatorProps = {
   nodeConnectionStatus: ConnectionStatus
   isConnectedToInternet: boolean
 }
 
-const NoConnectionIndicator: React.FC<NoConnectionIndicatorProps> = ({
+export const NoConnectionIndicator: React.FC<NoConnectionIndicatorProps> = ({
   nodeConnectionStatus,
   isConnectedToInternet,
 }) => {
@@ -43,5 +43,3 @@ const NoConnectionIndicator: React.FC<NoConnectionIndicatorProps> = ({
     </CSSTransition>
   )
 }
-
-export default NoConnectionIndicator

+ 1 - 3
src/components/NoConnectionIndicator/index.ts

@@ -1,3 +1 @@
-import NoConnectionIndicator from './NoConnectionIndicator'
-
-export default NoConnectionIndicator
+export * from './NoConnectionIndicator'

+ 2 - 3
src/components/PlaceholderVideoGrid.tsx

@@ -2,12 +2,12 @@ import React from 'react'
 
 import { Grid } from '@/shared/components'
 
-import VideoPreview from './VideoPreview'
+import { VideoPreview } from './VideoPreview'
 
 type PlaceholderVideoGridProps = {
   videosCount?: number
 }
-const PlaceholderVideoGrid: React.FC<PlaceholderVideoGridProps> = ({ videosCount = 10 }) => {
+export const PlaceholderVideoGrid: React.FC<PlaceholderVideoGridProps> = ({ videosCount = 10 }) => {
   return (
     <Grid>
       {Array.from({ length: videosCount }).map((_, idx) => (
@@ -16,4 +16,3 @@ const PlaceholderVideoGrid: React.FC<PlaceholderVideoGridProps> = ({ videosCount
     </Grid>
   )
 }
-export default PlaceholderVideoGrid

+ 1 - 3
src/components/Portal.tsx

@@ -5,12 +5,10 @@ type PortalProps = {
   containerRef: React.RefObject<HTMLDivElement>
 }
 
-const Portal: React.FC<PortalProps> = ({ children, containerRef }) => {
+export const Portal: React.FC<PortalProps> = ({ children, containerRef }) => {
   const element = containerRef.current
   if (!element) {
     return null
   }
   return createPortal(children, element)
 }
-
-export default Portal

+ 1 - 1
src/components/PrivateRoute.tsx

@@ -1,5 +1,5 @@
 import React from 'react'
-import { Route, Navigate } from 'react-router'
+import { Navigate, Route } from 'react-router'
 
 type PrivateRouteProps = {
   element: React.ReactElement

+ 17 - 10
src/components/Sidenav/SidenavBase.style.ts

@@ -5,7 +5,7 @@ import { Link, LinkProps } from 'react-router-dom'
 import { Text } from '@/shared/components'
 import { badgeStyles } from '@/shared/components/Badge'
 import { SvgJoystreamFullLogo } from '@/shared/illustrations'
-import { colors, sizes, transitions, typography, zIndex, media } from '@/shared/theme'
+import { colors, media, sizes, transitions, typography, zIndex } from '@/shared/theme'
 
 export const EXPANDED_SIDENAVBAR_WIDTH = 360
 export const NAVBAR_LEFT_PADDING = 24
@@ -35,10 +35,8 @@ export const SidebarNav = styled.nav<SidebarNavProps>`
   z-index: ${zIndex.sideNav};
   width: ${({ expanded }) => (expanded ? `${EXPANDED_SIDENAVBAR_WIDTH}px` : 'var(--sidenav-collapsed-width)')};
   transition: width ${transitions.timings.regular} ${transitions.easing};
-
   display: grid;
   grid-template-rows: auto auto minmax(0, 1fr) auto;
-
   overflow: hidden;
   color: ${colors.white};
   background-color: ${({ isStudio }) => (isStudio ? colors.gray[800] : colors.gray[700])};
@@ -49,10 +47,6 @@ export const LogoLink = styled(Link)`
   margin-top: 24px;
   margin-left: 80px;
   text-decoration: none;
-
-  ${media.medium} {
-    margin-left: 86px;
-  }
 `
 
 export const Logo = styled(SvgJoystreamFullLogo)`
@@ -83,6 +77,7 @@ export const ButtonGroup = styled.div`
   flex-direction: column;
   justify-content: flex-end;
   margin-top: ${sizes(3)};
+
   > * + * {
     margin-top: ${sizes(4)};
   }
@@ -94,7 +89,8 @@ export const SidebarNavItem = styled.li<ExpandableElementProps>`
   display: flex;
   flex-direction: column;
   ${badgeStyles}
-  &[data-badge]:after {
+
+  &[data-badge]::after {
     left: ${sizes(12)};
     top: ${sizes(3)};
 
@@ -112,21 +108,26 @@ export const SidebarNavLink = styled(Link, { shouldForwardProp: isPropValid })<S
   display: flex;
   position: relative;
   align-items: center;
+
   &:hover {
     background-color: ${colors.transparentPrimary[10]};
   }
+
   &:focus {
     background-color: ${colors.transparentPrimary[10]};
   }
+
   &:active {
     background-color: ${colors.transparentPrimary[18]};
   }
+
   > svg {
     ${media.medium} {
       transform: translateY(${({ expanded }) => (expanded ? 0 : -8)}px);
       transition: transform ${transitions.timings.regular} ${transitions.easing};
     }
   }
+
   > span {
     margin-left: ${sizes(8)};
     font-weight: bold;
@@ -134,10 +135,12 @@ export const SidebarNavLink = styled(Link, { shouldForwardProp: isPropValid })<S
     font-size: ${typography.sizes.h5};
     line-height: 1;
   }
+
   &[data-active='true'] {
     background-color: ${colors.transparentPrimary[18]};
   }
-  :after {
+
+  ::after {
     ${media.medium} {
       content: ${({ content }) => `'${content}'`};
       position: absolute;
@@ -167,6 +170,7 @@ export const SubItemsWrapper = styled.div<SubItemProps>`
   transition: height ${transitions.timings.regular} ${transitions.easing};
   overflow: hidden;
   height: ${({ expanded, subitemsHeight }) => (expanded ? subitemsHeight || 0 : 0)}px;
+
   > ul {
     display: flex;
     flex-direction: column;
@@ -179,6 +183,7 @@ export const SubItem = styled.li`
   font-size: ${typography.sizes.body2};
   font-family: ${typography.fonts.base};
   margin-top: ${sizes(8)};
+
   :first-of-type {
     margin-top: ${sizes(6)};
   }
@@ -187,7 +192,6 @@ export const SubItem = styled.li`
 export const LegalLinksWrapper = styled.span`
   display: flex;
   align-items: center;
-
   margin-top: ${sizes(8)};
   padding: ${sizes(4)} 0 ${sizes(6)};
   border-top: 1px solid ${colors.gray[300]};
@@ -205,12 +209,15 @@ export const LegalLink = styled(Link)`
   text-decoration: none;
   font-family: ${typography.fonts.headers};
   font-size: ${typography.sizes.subtitle2};
+
   &:hover {
     color: ${colors.gray[400]};
   }
+
   &:focus {
     color: ${colors.gray[400]};
   }
+
   &:active {
     color: ${colors.gray[500]};
   }

+ 11 - 11
src/components/Sidenav/SidenavBase.tsx

@@ -4,24 +4,24 @@ import { CSSTransition } from 'react-transition-group'
 import useResizeObserver from 'use-resize-observer'
 
 import { absoluteRoutes } from '@/config/routes'
-import HamburgerButton from '@/shared/components/HamburgerButton'
+import { HamburgerButton } from '@/shared/components/HamburgerButton'
 import { transitions } from '@/shared/theme'
 
 import {
-  SidebarNav,
-  SidebarNavList,
-  SidebarNavItem,
-  SidebarNavLink,
+  ButtonGroup,
   DrawerOverlay,
-  SubItem,
-  SubItemsWrapper,
+  LegalLink,
+  LegalLinksWrapper,
   Logo,
   LogoLink,
-  ButtonGroup,
-  LegalLink,
+  SidebarNav,
   SidebarNavFooter,
+  SidebarNavItem,
+  SidebarNavLink,
+  SidebarNavList,
   StudioText,
-  LegalLinksWrapper,
+  SubItem,
+  SubItemsWrapper,
 } from './SidenavBase.style'
 
 type NavSubitem = {
@@ -151,5 +151,5 @@ const NavItem: React.FC<NavItemProps> = ({
   )
 }
 
-export { SidenavBase as default, NavItem }
+export { SidenavBase, NavItem }
 export type { NavItemType }

+ 15 - 21
src/components/Sidenav/StudioSidenav/StudioSidenav.tsx

@@ -1,13 +1,19 @@
 import React, { useState } from 'react'
-import { useNavigate } from 'react-router'
 import { CSSTransition } from 'react-transition-group'
 
-import SidenavBase, { NavItemType } from '@/components/Sidenav/SidenavBase'
+import { NavItemType, SidenavBase } from '@/components/Sidenav/SidenavBase'
 import { absoluteRoutes } from '@/config/routes'
-import { useDrafts, useAuthorizedUser, useEditVideoSheet, useUploadsManager, useDisplayDataLostWarning } from '@/hooks'
+import {
+  chanelUnseenDraftsSelector,
+  useAuthorizedUser,
+  useDraftStore,
+  useEditVideoSheet,
+  useUploadsStore,
+} from '@/providers'
 import { Button } from '@/shared/components'
 import { SvgGlyphAddVideo, SvgGlyphExternal, SvgNavChannel, SvgNavUpload, SvgNavVideos } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
+import { openInNewTab } from '@/utils/browser'
 
 const studioNavbarItems: NavItemType[] = [
   {
@@ -33,14 +39,12 @@ const studioNavbarItems: NavItemType[] = [
 export const StudioSidenav: React.FC = () => {
   const [expanded, setExpanded] = useState(false)
   const { activeChannelId } = useAuthorizedUser()
-  const { unseenDrafts } = useDrafts('video', activeChannelId)
-  const { uploadsState } = useUploadsManager()
-  const navigate = useNavigate()
-  const { sheetState } = useEditVideoSheet()
+  const unseenDrafts = useDraftStore(chanelUnseenDraftsSelector(activeChannelId))
 
-  const { openWarningDialog } = useDisplayDataLostWarning()
+  const { sheetState } = useEditVideoSheet()
+  const uploadsStatus = useUploadsStore((state) => state.uploadsStatus)
 
-  const assetsInProgress = uploadsState.flat().filter((asset) => asset.lastStatus === 'inProgress')
+  const assetsInProgress = Object.values(uploadsStatus).filter((asset) => asset?.lastStatus === 'inProgress')
 
   const studioNavbarItemsWithBadge = studioNavbarItems.map((item) => {
     if (item.to === absoluteRoutes.studio.videos()) {
@@ -51,20 +55,10 @@ export const StudioSidenav: React.FC = () => {
     }
     return item
   })
-  const { anyVideoTabsCachedAssets } = useEditVideoSheet()
 
   const handleClick = () => {
-    if (anyVideoTabsCachedAssets) {
-      openWarningDialog({
-        onConfirm: () => {
-          setExpanded(false)
-          navigate(absoluteRoutes.viewer.index())
-        },
-        onCancel: () => setExpanded(false),
-      })
-    } else {
-      navigate(absoluteRoutes.viewer.index())
-    }
+    setExpanded(false)
+    openInNewTab(absoluteRoutes.viewer.index(), true)
   }
 
   return (

+ 1 - 1
src/components/Sidenav/StudioSidenav/index.ts

@@ -1 +1 @@
-export { StudioSidenav } from './StudioSidenav'
+export * from './StudioSidenav'

+ 3 - 7
src/components/Sidenav/ViewerSidenav/FollowedChannels.style.ts

@@ -1,10 +1,10 @@
 import styled from '@emotion/styled'
 
+import { ChannelLink } from '@/components'
 import { Text } from '@/shared/components'
-import { sizes, colors, typography } from '@/shared/theme'
+import { colors, sizes, typography } from '@/shared/theme'
 
-import ChannelLink from '../../ChannelLink'
-import { NAVBAR_LEFT_PADDING, EXPANDED_SIDENAVBAR_WIDTH } from '../SidenavBase.style'
+import { EXPANDED_SIDENAVBAR_WIDTH, NAVBAR_LEFT_PADDING } from '../SidenavBase.style'
 
 export const FollowedChannelsWrapper = styled.div`
   display: grid;
@@ -16,9 +16,7 @@ export const ChannelsTitle = styled(Text)`
   margin-top: ${sizes(6)};
   margin-bottom: ${sizes(4)};
   padding-left: ${NAVBAR_LEFT_PADDING}px;
-
   width: ${EXPANDED_SIDENAVBAR_WIDTH - NAVBAR_LEFT_PADDING}px;
-
   color: ${colors.gray[300]};
 `
 export const ChannelsWrapper = styled.div`
@@ -49,11 +47,9 @@ export const ChannelsItem = styled.li`
 export const ShowMoreButton = styled.button`
   border: none;
   background: none;
-
   font-family: ${typography.fonts.base};
   font-size: 1rem;
   font-weight: bold;
-
   cursor: pointer;
   padding: ${sizes(5)} 0;
   display: flex;

+ 7 - 9
src/components/Sidenav/ViewerSidenav/FollowedChannels.tsx

@@ -1,19 +1,19 @@
 import React, { useState } from 'react'
 import { CSSTransition } from 'react-transition-group'
 
-import { FollowedChannel } from '@/hooks/usePersonalData/localStorageClient/types'
+import { FollowedChannel } from '@/providers/personalData/types'
 import { SvgGlyphChevronDown, SvgGlyphChevronUp } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 
 import {
-  ChannelsWrapper,
-  ChannelsTitle,
-  ChannelsList,
   ChannelsItem,
+  ChannelsList,
+  ChannelsTitle,
+  ChannelsWrapper,
+  FollowedChannelsWrapper,
   ShowMoreButton,
-  StyledChannelLink,
   ShowMoreIconWrapper,
-  FollowedChannelsWrapper,
+  StyledChannelLink,
 } from './FollowedChannels.style'
 
 const MAX_CHANNELS = 4
@@ -25,7 +25,7 @@ type FollowedChannelsProps = {
   onChannelNotFound?: (id: string) => void
 }
 
-const FollowedChannels: React.FC<FollowedChannelsProps> = ({
+export const FollowedChannels: React.FC<FollowedChannelsProps> = ({
   followedChannels,
   expanded,
   onClick,
@@ -66,5 +66,3 @@ const FollowedChannels: React.FC<FollowedChannelsProps> = ({
     </CSSTransition>
   )
 }
-
-export default FollowedChannels

+ 12 - 10
src/components/Sidenav/ViewerSidenav/ViewerSidenav.tsx

@@ -1,12 +1,14 @@
 import React, { useState } from 'react'
 
-import SidenavBase, { NavItemType } from '@/components/Sidenav/SidenavBase'
+import { NavItemType, SidenavBase } from '@/components/Sidenav/SidenavBase'
 import { absoluteRoutes } from '@/config/routes'
-import { usePersonalData } from '@/hooks'
+import { usePersonalDataStore } from '@/providers'
 import { Button } from '@/shared/components'
 import { SvgGlyphExternal, SvgNavChannels, SvgNavHome, SvgNavVideos } from '@/shared/icons'
+import { openInNewTab } from '@/utils/browser'
+import { Logger } from '@/utils/logger'
 
-import FollowedChannels from './FollowedChannels'
+import { FollowedChannels } from './FollowedChannels'
 
 const viewerSidenavItems: NavItemType[] = [
   {
@@ -28,13 +30,11 @@ const viewerSidenavItems: NavItemType[] = [
 
 export const ViewerSidenav: React.FC = () => {
   const [expanded, setExpanded] = useState(false)
-  const {
-    state: { followedChannels },
-    updateChannelFollowing,
-  } = usePersonalData()
+  const followedChannels = usePersonalDataStore((state) => state.followedChannels)
+  const updateChannelFollowing = usePersonalDataStore((state) => state.actions.updateChannelFollowing)
 
   const handleChannelNotFound = (id: string) => {
-    console.warn(`Followed channel not found, removing id: ${id}`)
+    Logger.warn(`Followed channel not found, removing id: ${id}`)
     updateChannelFollowing(id, false)
   }
 
@@ -55,9 +55,11 @@ export const ViewerSidenav: React.FC = () => {
         <>
           <Button
             variant="secondary"
-            onClick={() => setExpanded(false)}
+            onClick={() => {
+              setExpanded(false)
+              openInNewTab(absoluteRoutes.studio.index(), true)
+            }}
             icon={<SvgGlyphExternal />}
-            to={absoluteRoutes.studio.index()}
           >
             Joystream studio
           </Button>

+ 1 - 1
src/components/Sidenav/ViewerSidenav/index.ts

@@ -1 +1 @@
-export { ViewerSidenav } from './ViewerSidenav'
+export * from './ViewerSidenav'

+ 4 - 6
src/components/Sidenav/index.ts

@@ -1,7 +1,5 @@
-import SidenavBase, { NavItem, NavItemType } from './SidenavBase'
-import { EXPANDED_SIDENAVBAR_WIDTH } from './SidenavBase.style'
+export * from './SidenavBase'
+export * from './SidenavBase.style'
 
-export { SidenavBase as default, EXPANDED_SIDENAVBAR_WIDTH, NavItem }
-export type { NavItemType }
-export { StudioSidenav } from './StudioSidenav'
-export { ViewerSidenav } from './ViewerSidenav'
+export * from './StudioSidenav'
+export * from './ViewerSidenav'

+ 6 - 4
src/components/SignInSteps/AccountStep.style.ts

@@ -1,9 +1,9 @@
 import styled from '@emotion/styled'
 
-import { Button, Text, RadioButton } from '@/shared/components'
-import Spinner from '@/shared/components/Spinner'
+import { Button, RadioButton, Text } from '@/shared/components'
+import { Spinner } from '@/shared/components/Spinner'
 import { SvgAccountCreationIllustration } from '@/shared/illustrations'
-import { sizes, colors, typography, transitions, media } from '@/shared/theme'
+import { colors, media, sizes, transitions, typography } from '@/shared/theme'
 
 import { StepWrapper } from './SignInSteps.style'
 
@@ -22,6 +22,7 @@ export const StyledSpinner = styled(Spinner)`
 export const IconGroup = styled.div`
   display: flex;
   align-items: center;
+
   > * {
     margin: 0 ${sizes(2)};
   }
@@ -54,6 +55,7 @@ export const AccountWrapper = styled.label<AccountWrapperProps>`
   border: 1px solid ${({ isSelected }) => (isSelected ? colors.blue[500] : 'transparent')};
   transition: border ${transitions.timings.sharp} ${transitions.easing},
     background-color ${transitions.timings.sharp} ${transitions.easing};
+
   :hover {
     border: 1px solid ${({ isSelected }) => (isSelected ? colors.blue[500] : colors.gray[50])};
     background-color: ${colors.transparentPrimary[12]};
@@ -86,7 +88,6 @@ export const AccountAddress = styled(Text)`
 `
 
 export const StyledRadioButton = styled(RadioButton)`
-  display: block;
   align-self: center;
   margin: 0;
   margin-right: 20px;
@@ -118,6 +119,7 @@ export const OrderedStep = styled(Text)`
   align-items: center;
   max-width: 170px;
   flex-direction: column;
+
   ::before {
     margin-bottom: ${sizes(2)};
     content: '0' counter(ordered-list-counter);

+ 13 - 15
src/components/SignInSteps/AccountStep.tsx

@@ -2,42 +2,42 @@ import React, { FormEvent, useState } from 'react'
 import { useNavigate } from 'react-router'
 import { CSSTransition, SwitchTransition } from 'react-transition-group'
 
-import { useUser } from '@/hooks'
+import { useUser } from '@/providers'
 import { Text } from '@/shared/components'
 import { SvgGlyphChannel, SvgOutlineConnect } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 
 import {
-  StyledSpinner,
+  AccountAddress,
+  AccountInfo,
   AccountStepImg,
-  AccountsWrapper,
   AccountWrapper,
-  AccountInfo,
+  AccountsWrapper,
+  IconGroup,
   IconWrapper,
-  OrderedSteps,
   OrderedStep,
-  IconGroup,
-  AccountAddress,
-  StyledRadioButton,
+  OrderedSteps,
   StyledButton,
-  SubTitle,
+  StyledRadioButton,
+  StyledSpinner,
   StyledStepWrapper,
+  SubTitle,
 } from './AccountStep.style'
 import {
-  StepFooter,
   BottomBarIcon,
+  StepFooter,
   StepSubTitle,
   StepTitle,
   StepWrapper,
-  StyledPolkadotLogo,
   StyledJoystreamLogo,
+  StyledPolkadotLogo,
 } from './SignInSteps.style'
 
 type AccountStepProps = {
   nextStepPath: string
 }
 
-const AccountStep: React.FC<AccountStepProps> = ({ nextStepPath }) => {
+export const AccountStep: React.FC<AccountStepProps> = ({ nextStepPath }) => {
   const navigate = useNavigate()
   const { accounts, setActiveUser, memberships, membershipsLoading } = useUser()
   const [selectedAccountAddress, setSelectedAccountAddress] = useState<undefined | string>()
@@ -135,7 +135,7 @@ export type AccountBarProps = {
   selectedValue?: string
 }
 
-const AccountBar: React.FC<AccountBarProps> = ({ name, id, onSelect, selectedValue }) => {
+export const AccountBar: React.FC<AccountBarProps> = ({ name, id, onSelect, selectedValue }) => {
   return (
     <AccountWrapper isSelected={selectedValue === id}>
       <AccountInfo>
@@ -153,5 +153,3 @@ const AccountBar: React.FC<AccountBarProps> = ({ name, id, onSelect, selectedVal
     </AccountWrapper>
   )
 }
-
-export default AccountStep

+ 1 - 0
src/components/SignInSteps/ExtensionStep.style.ts

@@ -15,6 +15,7 @@ export const StyledStepFooter = styled(StepFooter)`
 
 export const StyledListItem = styled(Text)`
   text-align: left;
+
   & + & {
     margin-top: ${sizes(2)};
   }

Some files were not shown because too many files changed in this diff