Bläddra i källkod

merge dev - content pages fixes, image loaders, tweaks (#1289)

merge dev - content pages fixes, image loaders, tweaks
Klaudiusz Dembler 3 år sedan
förälder
incheckning
aa1b1d456f
100 ändrade filer med 806 tillägg och 484 borttagningar
  1. 1 1
      .env
  2. 1 1
      .storybook/preview.jsx
  3. 6 0
      config-overrides.js
  4. 1 0
      package.json
  5. 6 2
      src/App.tsx
  6. 4 3
      src/MainLayout.tsx
  7. 191 78
      src/api/client/resolvers.ts
  8. 4 18
      src/api/client/transforms/index.ts
  9. 2 2
      src/api/client/transforms/orionViews.ts
  10. 1 1
      src/api/client/transforms/queryNodeFollows.ts
  11. 1 1
      src/api/client/transforms/queryNodeViews.ts
  12. 28 0
      src/api/hooks/video.ts
  13. 5 5
      src/api/queries/__generated__/search.generated.tsx
  14. 1 1
      src/api/queries/search.graphql
  15. 4 3
      src/components/ChannelCard.tsx
  16. 3 1
      src/components/ChannelGallery.tsx
  17. 1 1
      src/components/ChannelGrid.tsx
  18. 2 1
      src/components/ChannelLink/ChannelLink.style.ts
  19. 1 1
      src/components/ChannelLink/ChannelLink.tsx
  20. 3 1
      src/components/ChannelWithVideos/ChannelWithVideos.style.ts
  21. 21 33
      src/components/ChannelWithVideos/ChannelWithVideos.tsx
  22. 1 1
      src/components/Dialogs/ActionDialog/ActionDialog.stories.tsx
  23. 1 1
      src/components/Dialogs/ActionDialog/ActionDialog.tsx
  24. 1 1
      src/components/Dialogs/BaseDialog/BaseDialog.stories.tsx
  25. 1 1
      src/components/Dialogs/BaseDialog/BaseDialog.style.ts
  26. 2 2
      src/components/Dialogs/BaseDialog/BaseDialog.tsx
  27. 3 2
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx
  28. 2 1
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.style.ts
  29. 1 1
      src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx
  30. 5 5
      src/components/Dialogs/ImageCropDialog/cropper.ts
  31. 1 1
      src/components/Dialogs/MessageDialog/MessageDialog.style.ts
  32. 1 1
      src/components/Dialogs/Multistepper/Multistepper.stories.tsx
  33. 1 43
      src/components/Dialogs/Multistepper/Multistepper.style.ts
  34. 4 24
      src/components/Dialogs/Multistepper/Multistepper.tsx
  35. 1 1
      src/components/Dialogs/TransactionDialog/TransactionDialog.tsx
  36. 2 1
      src/components/InfiniteGrids/InfiniteChannelGrid.tsx
  37. 6 9
      src/components/InfiniteGrids/InfiniteChannelWithVideosGrid.tsx
  38. 1 1
      src/components/InfiniteGrids/InfiniteGrid.style.ts
  39. 5 1
      src/components/InfiniteGrids/InfiniteVideoGrid.tsx
  40. 2 2
      src/components/InterruptedVideosGallery.tsx
  41. 2 1
      src/components/NoConnectionIndicator/NoConnectionIndicator.stories.tsx
  42. 2 1
      src/components/NoConnectionIndicator/NoConnectionIndicator.style.ts
  43. 2 2
      src/components/NoConnectionIndicator/NoConnectionIndicator.tsx
  44. 9 8
      src/components/OfficialJoystreamUpdate.tsx
  45. 1 1
      src/components/Sidenav/SidenavBase.style.ts
  46. 5 8
      src/components/Sidenav/StudioSidenav/StudioSidenav.tsx
  47. 2 2
      src/components/Sidenav/ViewerSidenav/FollowedChannels.style.ts
  48. 2 2
      src/components/Sidenav/ViewerSidenav/ViewerSidenav.tsx
  49. 3 1
      src/components/SignInSteps/AccountStep.style.ts
  50. 2 2
      src/components/SignInSteps/AccountStep.tsx
  51. 2 1
      src/components/SignInSteps/ExtensionStep.style.ts
  52. 4 2
      src/components/SignInSteps/ExtensionStep.tsx
  53. 1 1
      src/components/SignInSteps/SignInSteps.style.ts
  54. 2 1
      src/components/SignInSteps/TermsStep.style.tsx
  55. 1 1
      src/components/SkeletonLoaderVideoGrid.tsx
  56. 4 3
      src/components/StudioEntrypoint.tsx
  57. 2 1
      src/components/TermsOfService.tsx
  58. 1 1
      src/components/TopTenThisWeek.tsx
  59. 3 1
      src/components/Topbar/StudioTopbar/StudioTopbar.style.tsx
  60. 8 2
      src/components/Topbar/StudioTopbar/StudioTopbar.tsx
  61. 1 1
      src/components/Topbar/TopbarBase.style.tsx
  62. 2 3
      src/components/VideoGallery.tsx
  63. 1 1
      src/components/VideoGrid.tsx
  64. 14 5
      src/components/VideoHero/VideoHero.style.ts
  65. 15 20
      src/components/VideoHero/VideoHero.tsx
  66. 49 21
      src/components/VideoTile.tsx
  67. 3 1
      src/components/ViewErrorFallback.tsx
  68. 0 30
      src/components/index.ts
  69. 89 0
      src/components/templates/VideoContentTemplate.tsx
  70. 30 0
      src/config/availableNodes.ts
  71. 3 8
      src/config/envs.ts
  72. 5 1
      src/hooks/useDeleteVideo.tsx
  73. 1 1
      src/hooks/useHandleFollowChannel.tsx
  74. 2 1
      src/mocking/accessors/pagination.ts
  75. 21 18
      src/providers/assets/assetsManager.tsx
  76. 1 5
      src/providers/assets/useAsset.tsx
  77. 2 1
      src/providers/editVideoSheet/hooks.tsx
  78. 1 1
      src/providers/editVideoSheet/provider.tsx
  79. 1 1
      src/providers/editVideoSheet/types.ts
  80. 1 0
      src/providers/environment/index.ts
  81. 44 0
      src/providers/environment/store.ts
  82. 0 13
      src/providers/index.ts
  83. 6 3
      src/providers/joystream/provider.tsx
  84. 3 2
      src/providers/snackbars/snackbar.tsx
  85. 1 1
      src/providers/storageProviders.tsx
  86. 1 1
      src/providers/transactionManager/transactionManager.tsx
  87. 5 2
      src/providers/transactionManager/useTransaction.ts
  88. 2 1
      src/providers/uploadsManager/uploadsManager.tsx
  89. 17 1
      src/providers/uploadsManager/useStartFileUpload.tsx
  90. 7 2
      src/providers/user/user.tsx
  91. 3 4
      src/shared/components/Avatar/Avatar.tsx
  92. 3 1
      src/shared/components/Banner/Banner.style.ts
  93. 1 1
      src/shared/components/Banner/Banner.tsx
  94. 6 1
      src/shared/components/CallToActionButton/CallToActionButton.style.ts
  95. 1 1
      src/shared/components/CallToActionButton/CallToActionButton.tsx
  96. 7 3
      src/shared/components/Carousel/Carousel.style.ts
  97. 32 22
      src/shared/components/ChannelCardBase/ChannelCardBase.tsx
  98. 1 1
      src/shared/components/ChannelCover/ChannelCover.tsx
  99. 4 4
      src/shared/components/CircularProgress/CircularProgress.style.tsx
  100. 34 6
      src/shared/components/CircularProgress/CircularProgress.tsx

+ 1 - 1
.env

@@ -13,8 +13,8 @@ REACT_APP_DEVELOPMENT_OFFICIAL_JOYSTREAM_CHANNEL_ID=12
 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_ASSET_LOGS_URL=https://orion.joystream.org/logs
-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_PRODUCTION_OFFICIAL_JOYSTREAM_CHANNEL_ID=3
 

+ 1 - 1
.storybook/preview.jsx

@@ -2,7 +2,7 @@ import styled from '@emotion/styled'
 import React, { useRef } from 'react'
 import useResizeObserver from 'use-resize-observer'
 
-import { GlobalStyle } from '../src/shared/components'
+import { GlobalStyle } from '../src/shared/components/GlobalStyle'
 
 const Wrapper = styled.div`
   padding: 10px;

+ 6 - 0
config-overrides.js

@@ -1,12 +1,18 @@
 /* eslint-disable @typescript-eslint/no-var-requires */
 const path = require('path')
 const StylelintPlugin = require('stylelint-webpack-plugin')
+const CircularDependencyPlugin = require('circular-dependency-plugin')
 const { override, addBabelPlugin, addWebpackAlias, addWebpackModuleRule, addWebpackPlugin } = require('customize-cra')
 
 module.exports = {
   webpack: override(
     addBabelPlugin('@emotion/babel-plugin'),
     addWebpackPlugin(new StylelintPlugin({ files: './src/**/*.{tsx,ts}' })),
+    addWebpackPlugin(
+      new CircularDependencyPlugin({
+        exclude: /a\.js|node_modules/,
+      })
+    ),
     addWebpackAlias({
       '@': path.resolve(__dirname, 'src/'),
     }),

+ 1 - 0
package.json

@@ -124,6 +124,7 @@
     "@types/video.js": "^7.3.23",
     "@typescript-eslint/eslint-plugin": "^4.27.0",
     "@typescript-eslint/parser": "^4.27.0",
+    "circular-dependency-plugin": "^5.2.2",
     "eslint": "^7.28.0",
     "eslint-config-prettier": "^8.3.0",
     "eslint-plugin-react": "^7.24.0",

+ 6 - 2
src/App.tsx

@@ -3,11 +3,15 @@ import React from 'react'
 import { BrowserRouter } from 'react-router-dom'
 
 import { createApolloClient } from '@/api'
-import { GlobalStyle } from '@/shared/components'
+import { GlobalStyle } from '@/shared/components/GlobalStyle'
 import { routingTransitions } from '@/styles/routingTransitions'
 
 import { MainLayout } from './MainLayout'
-import { AssetsManager, DialogProvider, OverlayManagerProvider, Snackbars, StorageProvidersProvider } from './providers'
+import { AssetsManager } from './providers/assets'
+import { DialogProvider } from './providers/dialogs'
+import { OverlayManagerProvider } from './providers/overlayManager'
+import { Snackbars } from './providers/snackbars'
+import { StorageProvidersProvider } from './providers/storageProviders'
 
 export const App = () => {
   // create client on render so the mocking setup is done if needed

+ 4 - 3
src/MainLayout.tsx

@@ -2,15 +2,16 @@ import loadable from '@loadable/component'
 import React, { useEffect } from 'react'
 import { Route, Routes } from 'react-router-dom'
 
-import { StudioLoading, TopbarBase } from '@/components'
+import { StudioLoading } from '@/components/StudioEntrypoint'
+import { TopbarBase } from '@/components/Topbar'
 import { BASE_PATHS } from '@/config/routes'
 import { isBrowserOutdated } from '@/utils/browser'
 
-import { useDialog } from './providers'
+import { useDialog } from './providers/dialogs'
 import { AdminView } from './views/admin'
 import { LegalLayout } from './views/legal'
 import { PlaygroundLayout } from './views/playground'
-import { ViewerLayout } from './views/viewer'
+import { ViewerLayout } from './views/viewer/ViewerLayout'
 
 const LoadableStudioLayout = loadable(() => import('./views/studio/StudioLayout'), {
   fallback: (

+ 191 - 78
src/api/client/resolvers.ts

@@ -3,29 +3,27 @@ import type { IResolvers, ISchemaLevelResolver } from '@graphql-tools/utils'
 import { GraphQLSchema } from 'graphql'
 
 import { createLookup } from '@/utils/data'
-import { ConsoleLogger } from '@/utils/logs'
+import { SentryLogger } from '@/utils/logs'
 
 import {
+  ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
   ORION_BATCHED_FOLLOWS_QUERY_NAME,
   ORION_BATCHED_VIEWS_QUERY_NAME,
+  ORION_CHANNEL_VIEWS_QUERY_NAME,
   ORION_FOLLOWS_QUERY_NAME,
   ORION_VIEWS_QUERY_NAME,
-  RemoveQueryNodeFollowsField,
-  RemoveQueryNodeViewsField,
+  RemoveQueryNodeChannelFollowsField,
+  RemoveQueryNodeChannelViewsField,
+  RemoveQueryNodeVideoViewsField,
+  TransformBatchedChannelOrionViewsField,
   TransformBatchedOrionFollowsField,
-  TransformBatchedOrionViewsField,
+  TransformBatchedOrionVideoViewsField,
+  TransformOrionChannelViewsField,
   TransformOrionFollowsField,
-  TransformOrionViewsField,
+  TransformOrionVideoViewsField,
 } from './transforms'
-import {
-  ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
-  ORION_CHANNEL_VIEWS_QUERY_NAME,
-  TransformBatchedChannelOrionViewsField,
-  TransformOrionChannelViewsField,
-} from './transforms/orionViews'
-import { RemoveQueryNodeChannelViewsField } from './transforms/queryNodeViews'
 
-import { Channel, ChannelEdge, Video, VideoEdge } from '../queries'
+import { Channel, ChannelEdge, SearchFtsOutput, Video, VideoEdge } from '../queries'
 
 const BATCHED_VIDEO_VIEWS_QUERY_NAME = 'GetBatchedVideoViews'
 const BATCHED_CHANNEL_VIEWS_QUERY_NAME = 'GetBatchedChannelViews'
@@ -58,17 +56,16 @@ export const queryNodeStitchingResolvers = (
   Query: {
     // video queries
     videoByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'videoByUniqueInput', [
-      RemoveQueryNodeViewsField,
+      RemoveQueryNodeVideoViewsField,
     ]),
     videos: async (parent, args, context, info) => {
+      const videosResolver = createResolverWithTransforms(queryNodeSchema, 'videos', [RemoveQueryNodeVideoViewsField])
+      const videos = await videosResolver(parent, args, context, info)
       try {
-        const videosResolver = createResolverWithTransforms(queryNodeSchema, 'videos', [RemoveQueryNodeViewsField])
-        const videos = await videosResolver(parent, args, context, info)
-
         const batchedVideoViewsResolver = createResolverWithTransforms(
           orionSchema,
           ORION_BATCHED_VIEWS_QUERY_NAME,
-          [TransformBatchedOrionViewsField],
+          [TransformBatchedOrionVideoViewsField],
           // operationName has to be manually kept in sync with the query name used
           BATCHED_VIDEO_VIEWS_QUERY_NAME
         )
@@ -84,24 +81,25 @@ export const queryNodeStitchingResolvers = (
         const viewsLookup = createLookup<{ id: string; views: number }>(batchedVideoViews || [])
         return videos.map((video: Video) => ({ ...video, views: viewsLookup[video.id]?.views || 0 }))
       } catch (error) {
-        ConsoleLogger.warn('Failed to resolve videos field', { error })
-        return null
+        SentryLogger.error('Failed to resolve video views', 'videos resolver', error)
+        return videos
       }
     },
-    videosConnection: createResolverWithTransforms(queryNodeSchema, 'videosConnection', [RemoveQueryNodeViewsField]),
+    videosConnection: createResolverWithTransforms(queryNodeSchema, 'videosConnection', [
+      RemoveQueryNodeVideoViewsField,
+    ]),
     // channel queries
     channelByUniqueInput: createResolverWithTransforms(queryNodeSchema, 'channelByUniqueInput', [
-      RemoveQueryNodeFollowsField,
+      RemoveQueryNodeChannelFollowsField,
       RemoveQueryNodeChannelViewsField,
     ]),
     channels: async (parent, args, context, info) => {
+      const channelsResolver = createResolverWithTransforms(queryNodeSchema, 'channels', [
+        RemoveQueryNodeChannelFollowsField,
+        RemoveQueryNodeChannelViewsField,
+      ])
+      const channels = await channelsResolver(parent, args, context, info)
       try {
-        const channelsResolver = createResolverWithTransforms(queryNodeSchema, 'channels', [
-          RemoveQueryNodeFollowsField,
-          RemoveQueryNodeChannelViewsField,
-        ])
-        const channels = await channelsResolver(parent, args, context, info)
-
         const batchedChannelFollowsResolver = createResolverWithTransforms(
           orionSchema,
           ORION_BATCHED_FOLLOWS_QUERY_NAME,
@@ -109,7 +107,7 @@ export const queryNodeStitchingResolvers = (
           // operationName has to be manually kept in sync with the query name used
           BATCHED_FOLLOWS_VIEWS_QUERY_NAME
         )
-        const batchedChannelFollows = await batchedChannelFollowsResolver(
+        const batchedChannelFollowsPromise = batchedChannelFollowsResolver(
           parent,
           {
             channelIdList: channels.map((channel: Channel) => channel.id),
@@ -117,7 +115,6 @@ export const queryNodeStitchingResolvers = (
           context,
           info
         )
-        const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
 
         const batchedChannelViewsResolver = createResolverWithTransforms(
           orionSchema,
@@ -126,7 +123,7 @@ export const queryNodeStitchingResolvers = (
           // operationName has to be manually kept in sync with the query name used
           BATCHED_CHANNEL_VIEWS_QUERY_NAME
         )
-        const batchedChannelViews = await batchedChannelViewsResolver(
+        const batchedChannelViewsPromise = batchedChannelViewsResolver(
           parent,
           {
             channelIdList: channels.map((channel: Channel) => channel.id),
@@ -135,6 +132,12 @@ export const queryNodeStitchingResolvers = (
           info
         )
 
+        const [batchedChannelFollows, batchedChannelViews] = await Promise.all([
+          batchedChannelFollowsPromise,
+          batchedChannelViewsPromise,
+        ])
+
+        const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
         const viewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
 
         return channels.map((channel: Channel) => ({
@@ -143,19 +146,117 @@ export const queryNodeStitchingResolvers = (
           views: viewsLookup[channel.id]?.views || 0,
         }))
       } catch (error) {
-        ConsoleLogger.warn('Failed to resolve channels field', { error })
-        return null
+        SentryLogger.error('Failed to resolve channel views or follows', 'channels resolver', error)
+        return channels
       }
     },
     channelsConnection: createResolverWithTransforms(queryNodeSchema, 'channelsConnection', [
-      RemoveQueryNodeFollowsField,
+      RemoveQueryNodeChannelFollowsField,
       RemoveQueryNodeChannelViewsField,
     ]),
     // mixed queries
-    search: createResolverWithTransforms(queryNodeSchema, 'search', [
-      RemoveQueryNodeViewsField,
-      RemoveQueryNodeFollowsField,
-    ]),
+    search: async (parent, args, context, info) => {
+      const searchResolver = createResolverWithTransforms(queryNodeSchema, 'search', [
+        RemoveQueryNodeVideoViewsField,
+        RemoveQueryNodeChannelFollowsField,
+        RemoveQueryNodeChannelViewsField,
+      ])
+      const search = await searchResolver(parent, args, context, info)
+      try {
+        const channelIdList = search
+          .filter((result: SearchFtsOutput) => result.item.__typename === 'Channel')
+          .map((result: SearchFtsOutput) => result.item.id)
+
+        const batchedChannelFollowsResolver = createResolverWithTransforms(
+          orionSchema,
+          ORION_BATCHED_FOLLOWS_QUERY_NAME,
+          [TransformBatchedOrionFollowsField],
+          // operationName has to be manually kept in sync with the query name used
+          BATCHED_FOLLOWS_VIEWS_QUERY_NAME
+        )
+        const batchedChannelFollowsPromise = batchedChannelFollowsResolver(
+          parent,
+          {
+            channelIdList,
+          },
+          context,
+          info
+        )
+
+        const batchedChannelViewsResolver = createResolverWithTransforms(
+          orionSchema,
+          ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+          [TransformBatchedChannelOrionViewsField],
+          // operationName has to be manually kept in sync with the query name used
+          BATCHED_CHANNEL_VIEWS_QUERY_NAME
+        )
+        const batchedChannelViewsPromise = batchedChannelViewsResolver(
+          parent,
+          {
+            channelIdList,
+          },
+          context,
+          info
+        )
+
+        const videoIdList = search
+          .filter((result: SearchFtsOutput) => result.item.__typename === 'Video')
+          .map((result: SearchFtsOutput) => result.item.id)
+
+        const batchedVideoViewsResolver = createResolverWithTransforms(
+          orionSchema,
+          ORION_BATCHED_VIEWS_QUERY_NAME,
+          [TransformBatchedOrionVideoViewsField],
+          // operationName has to be manually kept in sync with the query name used
+          BATCHED_VIDEO_VIEWS_QUERY_NAME
+        )
+        const batchedVideoViewsPromise = batchedVideoViewsResolver(
+          parent,
+          {
+            videoIdList,
+          },
+          context,
+          info
+        )
+
+        const [batchedChannelFollows, batchedChannelViews, batchedVideoViews] = await Promise.all([
+          batchedChannelFollowsPromise,
+          batchedChannelViewsPromise,
+          batchedVideoViewsPromise,
+        ])
+
+        const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
+        const channelViewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
+        const viewsLookup = createLookup<{ id: string; views: number }>(batchedVideoViews || [])
+
+        const searchWithFollowsAndViews = search.map((searchOutput: SearchFtsOutput) => {
+          if (searchOutput.item.__typename === 'Channel') {
+            return {
+              ...searchOutput,
+              item: {
+                ...searchOutput.item,
+                follows: followsLookup[searchOutput.item.id]?.follows || 0,
+                views: channelViewsLookup[searchOutput.item.id]?.views || 0,
+              },
+            }
+          }
+          if (searchOutput.item.__typename === 'Video') {
+            return {
+              ...searchOutput,
+              item: {
+                ...searchOutput.item,
+                views: viewsLookup[searchOutput.item.id]?.views || 0,
+              },
+            }
+          }
+        })
+
+        return searchWithFollowsAndViews
+      } catch (error) {
+        SentryLogger.error('Failed to resolve channel views, channel follows or video views', 'search resolver', error)
+        return search
+      }
+    },
   },
   Video: {
     views: async (parent, args, context, info) => {
@@ -165,7 +266,7 @@ export const queryNodeStitchingResolvers = (
       const orionViewsResolver = createResolverWithTransforms(
         orionSchema,
         ORION_VIEWS_QUERY_NAME,
-        [TransformOrionViewsField],
+        [TransformOrionVideoViewsField],
         // operationName has to be manually kept in sync with the query name used
         'GetVideoViews'
       )
@@ -179,7 +280,7 @@ export const queryNodeStitchingResolvers = (
           info
         )
       } catch (error) {
-        ConsoleLogger.warn('Failed to resolve views field', { error })
+        SentryLogger.error('Failed to resolve video views', 'Video.views resolver', error)
         return null
       }
     },
@@ -189,28 +290,32 @@ export const queryNodeStitchingResolvers = (
       const batchedVideoViewsResolver = createResolverWithTransforms(
         orionSchema,
         ORION_BATCHED_VIEWS_QUERY_NAME,
-        [TransformBatchedOrionViewsField],
+        [TransformBatchedOrionVideoViewsField],
         // operationName has to be manually kept in sync with the query name used
         BATCHED_VIDEO_VIEWS_QUERY_NAME
       )
-      const batchedVideoViews = await batchedVideoViewsResolver(
-        parent,
-        {
-          videoIdList: parent.edges.map((edge: VideoEdge) => edge.node.id),
-        },
-        context,
-        info
-      )
 
-      const viewsLookup = createLookup<{ id: string; views: number }>(batchedVideoViews || [])
+      try {
+        const batchedVideoViews = await batchedVideoViewsResolver(
+          parent,
+          { videoIdList: parent.edges.map((edge: VideoEdge) => edge.node.id) },
+          context,
+          info
+        )
 
-      return parent.edges.map((edge: VideoEdge) => ({
-        ...edge,
-        node: {
-          ...edge.node,
-          views: viewsLookup[edge.node.id]?.views || 0,
-        },
-      }))
+        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,
+          },
+        }))
+      } catch (error) {
+        SentryLogger.error('Failed to resolve video views', 'VideoConnection.edges resolver', error)
+        return parent.edges
+      }
     },
   },
   Channel: {
@@ -235,7 +340,7 @@ export const queryNodeStitchingResolvers = (
           info
         )
       } catch (error) {
-        ConsoleLogger.warn('Failed to resolve views field', { error })
+        SentryLogger.error('Failed to resolve channel views', 'Channel.views resolver', error)
         return null
       }
     },
@@ -259,7 +364,7 @@ export const queryNodeStitchingResolvers = (
           info
         )
       } catch (error) {
-        ConsoleLogger.warn('Failed to resolve follows field', { error })
+        SentryLogger.error('Failed to resolve channel follows', 'Channel.follows resolver', error)
         return null
       }
     },
@@ -273,7 +378,7 @@ export const queryNodeStitchingResolvers = (
         // operationName has to be manually kept in sync with the query name used
         BATCHED_FOLLOWS_VIEWS_QUERY_NAME
       )
-      const batchedChannelFollows = await batchedChannelFollowsResolver(
+      const batchedChannelFollowsPromise = batchedChannelFollowsResolver(
         parent,
         {
           channelIdList: parent.edges.map((edge: ChannelEdge) => edge.node.id),
@@ -289,26 +394,34 @@ export const queryNodeStitchingResolvers = (
         // operationName has to be manually kept in sync with the query name used
         BATCHED_CHANNEL_VIEWS_QUERY_NAME
       )
-      const batchedChannelViews = await batchedChannelViewsResolver(
-        parent,
-        {
-          channelIdList: parent.edges.map((edge: ChannelEdge) => edge.node.id),
-        },
-        context,
-        info
-      )
+      try {
+        const batchedChannelViewsPromise = batchedChannelViewsResolver(
+          parent,
+          {
+            channelIdList: parent.edges.map((edge: ChannelEdge) => edge.node.id),
+          },
+          context,
+          info
+        )
 
-      const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
-      const viewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
+        const [batchedChannelFollows, batchedChannelViews] = await Promise.all([
+          batchedChannelFollowsPromise,
+          batchedChannelViewsPromise,
+        ])
 
-      return parent.edges.map((edge: ChannelEdge) => ({
-        ...edge,
-        node: {
-          ...edge.node,
-          follows: followsLookup[edge.node.id]?.follows || 0,
-          views: viewsLookup[edge.node.id]?.views || 0,
-        },
-      }))
+        const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
+        const viewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
+        return parent.edges.map((edge: ChannelEdge) => ({
+          ...edge,
+          node: {
+            ...edge.node,
+            follows: followsLookup[edge.node.id]?.follows || 0,
+            views: viewsLookup[edge.node.id]?.views || 0,
+          },
+        }))
+      } catch (error) {
+        SentryLogger.error('Failed to resolve channel views or follows', 'ChannelConnection.edges resolver', error)
+      }
     },
   },
 })

+ 4 - 18
src/api/client/transforms/index.ts

@@ -1,18 +1,4 @@
-export {
-  ORION_BATCHED_FOLLOWS_QUERY_NAME,
-  ORION_FOLLOWS_QUERY_NAME,
-  TransformBatchedOrionFollowsField,
-  TransformOrionFollowsField,
-} from './orionFollows'
-export {
-  ORION_BATCHED_VIEWS_QUERY_NAME,
-  ORION_VIEWS_QUERY_NAME,
-  TransformBatchedOrionViewsField,
-  TransformOrionViewsField,
-  ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
-  ORION_CHANNEL_VIEWS_QUERY_NAME,
-  TransformBatchedChannelOrionViewsField,
-  TransformOrionChannelViewsField,
-} from './orionViews'
-export { RemoveQueryNodeFollowsField } from './queryNodeFollows'
-export { RemoveQueryNodeViewsField } from './queryNodeViews'
+export * from './orionFollows'
+export * from './orionViews'
+export * from './queryNodeFollows'
+export * from './queryNodeViews'

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

@@ -33,7 +33,7 @@ const INFO_SELECTION_SET: SelectionSetNode = {
 export const ORION_VIEWS_QUERY_NAME = 'videoViews'
 
 // Transform a request to expect VideoViewsInfo return type instead of an Int
-export const TransformOrionViewsField: Transform = {
+export const TransformOrionVideoViewsField: Transform = {
   transformRequest(request) {
     request.document = {
       ...request.document,
@@ -77,7 +77,7 @@ export const TransformOrionViewsField: Transform = {
 
 export const ORION_BATCHED_VIEWS_QUERY_NAME = 'batchedVideoViews'
 
-export const TransformBatchedOrionViewsField: Transform = {
+export const TransformBatchedOrionVideoViewsField: Transform = {
   transformRequest(request) {
     request.document = {
       ...request.document,

+ 1 - 1
src/api/client/transforms/queryNodeFollows.ts

@@ -1,7 +1,7 @@
 import { Transform } from '@graphql-tools/delegate'
 
 // remove follows field from the query node channel request
-export const RemoveQueryNodeFollowsField: Transform = {
+export const RemoveQueryNodeChannelFollowsField: Transform = {
   transformRequest: (request) => {
     request.document = {
       ...request.document,

+ 1 - 1
src/api/client/transforms/queryNodeViews.ts

@@ -1,7 +1,7 @@
 import { Transform } from '@graphql-tools/delegate'
 
 // remove views field from the query node video request
-export const RemoveQueryNodeViewsField: Transform = {
+export const RemoveQueryNodeVideoViewsField: Transform = {
   transformRequest: (request) => {
     request.document = {
       ...request.document,

+ 28 - 0
src/api/hooks/video.ts

@@ -2,6 +2,7 @@ import { MutationHookOptions, QueryHookOptions } from '@apollo/client'
 
 import {
   AddVideoViewMutation,
+  AssetAvailability,
   GetBasicVideosQuery,
   GetBasicVideosQueryVariables,
   GetMostViewedVideosAllTimeQuery,
@@ -11,6 +12,7 @@ import {
   GetVideoQuery,
   GetVideosQuery,
   GetVideosQueryVariables,
+  VideoOrderByInput,
   useAddVideoViewMutation,
   useGetBasicVideosQuery,
   useGetMostViewedVideosAllTimeQuery,
@@ -40,6 +42,32 @@ export const useVideos = (variables?: GetVideosQueryVariables, opts?: VideosOpts
   }
 }
 
+export const useChannelPreviewVideos = (
+  channelId: string | null | undefined,
+  opts?: QueryHookOptions<GetVideosQuery>
+) => {
+  const { data, ...rest } = useGetVideosQuery({
+    ...opts,
+    variables: {
+      where: {
+        channelId_eq: channelId,
+        isPublic_eq: true,
+        isCensored_eq: false,
+        thumbnailPhotoAvailability_eq: AssetAvailability.Accepted,
+        mediaAvailability_eq: AssetAvailability.Accepted,
+      },
+      orderBy: VideoOrderByInput.CreatedAtDesc,
+      offset: 0,
+      limit: 10,
+    },
+    skip: !channelId || opts?.skip,
+  })
+  return {
+    videos: data?.videos,
+    ...rest,
+  }
+}
+
 type AddVideoViewOpts = Omit<MutationHookOptions<AddVideoViewMutation>, 'variables'>
 export const useAddVideoView = (opts?: AddVideoViewOpts) => {
   const [addVideoView, rest] = useAddVideoViewMutation({

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

@@ -2,8 +2,8 @@ import { gql } from '@apollo/client'
 import * as Apollo from '@apollo/client'
 
 import * as Types from './baseTypes.generated'
-import { BasicChannelFieldsFragment } from './channels.generated'
-import { BasicChannelFieldsFragmentDoc } from './channels.generated'
+import { AllChannelFieldsFragment } from './channels.generated'
+import { AllChannelFieldsFragmentDoc } from './channels.generated'
 import { VideoFieldsFragment } from './videos.generated'
 import { VideoFieldsFragmentDoc } from './videos.generated'
 
@@ -18,7 +18,7 @@ export type SearchQuery = {
   __typename?: 'Query'
   search: Array<{
     __typename?: 'SearchFTSOutput'
-    item: ({ __typename?: 'Video' } & VideoFieldsFragment) | ({ __typename?: 'Channel' } & BasicChannelFieldsFragment)
+    item: ({ __typename?: 'Video' } & VideoFieldsFragment) | ({ __typename?: 'Channel' } & AllChannelFieldsFragment)
   }>
 }
 
@@ -30,13 +30,13 @@ export const SearchDocument = gql`
           ...VideoFields
         }
         ... on Channel {
-          ...BasicChannelFields
+          ...AllChannelFields
         }
       }
     }
   }
   ${VideoFieldsFragmentDoc}
-  ${BasicChannelFieldsFragmentDoc}
+  ${AllChannelFieldsFragmentDoc}
 `
 
 /**

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

@@ -5,7 +5,7 @@ query Search($text: String!, $whereVideo: VideoWhereInput, $whereChannel: Channe
         ...VideoFields
       }
       ... on Channel {
-        ...BasicChannelFields
+        ...AllChannelFields
       }
     }
   }

+ 4 - 3
src/components/ChannelCard.tsx

@@ -2,8 +2,8 @@ import React from 'react'
 
 import { useChannel } from '@/api/hooks'
 import { useHandleFollowChannel } from '@/hooks'
-import { AssetType, useAsset } from '@/providers'
-import { ChannelCardBase } from '@/shared/components'
+import { AssetType, useAsset } from '@/providers/assets'
+import { ChannelCardBase } from '@/shared/components/ChannelCardBase'
 
 export type ChannelCardProps = {
   id?: string
@@ -13,7 +13,7 @@ export type ChannelCardProps = {
 
 export const ChannelCard: React.FC<ChannelCardProps> = ({ id, className }) => {
   const { channel, loading } = useChannel(id ?? '', { skip: !id })
-  const { url } = useAsset({ entity: channel, assetType: AssetType.AVATAR })
+  const { url, isLoadingAsset } = useAsset({ entity: channel, assetType: AssetType.AVATAR })
 
   const { toggleFollowing, isFollowing } = useHandleFollowChannel(id)
 
@@ -26,6 +26,7 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({ id, className }) => {
     <ChannelCardBase
       className={className}
       isLoading={loading || !channel}
+      isLoadingAvatar={isLoadingAsset}
       id={channel?.id}
       avatarUrl={url}
       follows={channel?.follows}

+ 3 - 1
src/components/ChannelGallery.tsx

@@ -2,7 +2,9 @@ import React, { useMemo } from 'react'
 
 import { BasicChannelFieldsFragment } from '@/api/queries'
 import { ChannelCard } from '@/components/ChannelCard'
-import { Gallery, RankingNumberTile, breakpointsOfGrid } from '@/shared/components'
+import { Gallery } from '@/shared/components/Gallery'
+import { breakpointsOfGrid } from '@/shared/components/Grid'
+import { RankingNumberTile } from '@/shared/components/RankingNumberTile'
 
 type ChannelGalleryProps = {
   title?: string

+ 1 - 1
src/components/ChannelGrid.tsx

@@ -2,7 +2,7 @@ import styled from '@emotion/styled'
 import React from 'react'
 
 import { BasicChannelFieldsFragment } from '@/api/queries'
-import { Grid } from '@/shared/components'
+import { Grid } from '@/shared/components/Grid'
 
 import { ChannelCard } from './ChannelCard'
 

+ 2 - 1
src/components/ChannelLink/ChannelLink.style.ts

@@ -1,7 +1,8 @@
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
-import { SkeletonLoader, Text } from '@/shared/components'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
+import { Text } from '@/shared/components/Text'
 import { sizes } from '@/shared/theme'
 
 type ContainerProps = {

+ 1 - 1
src/components/ChannelLink/ChannelLink.tsx

@@ -3,7 +3,7 @@ import React from 'react'
 import { useBasicChannel } from '@/api/hooks'
 import { BasicChannelFieldsFragment } from '@/api/queries'
 import { absoluteRoutes } from '@/config/routes'
-import { AssetType, useAsset } from '@/providers'
+import { AssetType, useAsset } from '@/providers/assets'
 import { Avatar, AvatarSize } from '@/shared/components/Avatar'
 import { SentryLogger } from '@/utils/logs'
 

+ 3 - 1
src/components/ChannelWithVideos/ChannelWithVideos.style.ts

@@ -1,7 +1,9 @@
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
-import { Avatar, Button, Text } from '@/shared/components'
+import { Avatar } from '@/shared/components/Avatar'
+import { Button } from '@/shared/components/Button'
+import { Text } from '@/shared/components/Text'
 import { sizes } from '@/shared/theme'
 
 export const ChannelCardAnchor = styled(Link)`

+ 21 - 33
src/components/ChannelWithVideos/ChannelWithVideos.tsx

@@ -1,13 +1,12 @@
 import React, { FC, useState } from 'react'
 
-import { useChannel } from '@/api/hooks'
-import { GetVideosConnectionDocument, GetVideosConnectionQuery, GetVideosConnectionQueryVariables } from '@/api/queries'
-import { useInfiniteGrid } from '@/components/InfiniteGrids/useInfiniteGrid'
+import { useChannel, useChannelPreviewVideos } from '@/api/hooks'
 import { VideoTile } from '@/components/VideoTile'
 import { absoluteRoutes } from '@/config/routes'
 import { useHandleFollowChannel } from '@/hooks'
-import { AssetType, useAsset } from '@/providers'
-import { Grid, SkeletonLoader } from '@/shared/components'
+import { AssetType, useAsset } from '@/providers/assets'
+import { Grid } from '@/shared/components/Grid'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
 import { SentryLogger } from '@/utils/logs'
 import { formatNumberShort } from '@/utils/number'
 
@@ -25,54 +24,43 @@ type ChannelWithVideosProps = {
 }
 
 const INITIAL_VIDEOS_PER_ROW = 4
-const INITAL_ROWS = 1
+const INITIAL_ROWS = 1
 
 export const ChannelWithVideos: FC<ChannelWithVideosProps> = ({ channelId }) => {
   const [videosPerRow, setVideosPerRow] = useState(INITIAL_VIDEOS_PER_ROW)
-  const { channel, loading } = useChannel(channelId || '')
-
-  const { url: avatarUrl } = useAsset({ entity: channel, assetType: AssetType.AVATAR })
-  const { toggleFollowing, isFollowing } = useHandleFollowChannel(channelId)
-  const { displayedItems, placeholdersCount, error } = useInfiniteGrid<
-    GetVideosConnectionQuery,
-    GetVideosConnectionQuery['videosConnection'],
-    GetVideosConnectionQueryVariables
-  >({
-    query: GetVideosConnectionDocument,
-    isReady: !!channelId,
-    skipCount: 0,
-    queryVariables: {
-      where: {
-        channelId_eq: channelId,
-        isPublic_eq: true,
-        isCensored_eq: false,
-      },
-    },
-    targetRowsCount: INITAL_ROWS,
-    dataAccessor: (rawData) => rawData?.videosConnection,
-    itemsPerRow: videosPerRow,
+  const { channel, loading: channelLoading, error: channelError } = useChannel(channelId || '', {
+    skip: !channelId,
+    onError: (error) => SentryLogger.error('Failed to fetch channel', 'ChannelWithVideos', error),
+  })
+  const { videos, loading: videosLoading, error: videosError } = useChannelPreviewVideos(channelId, {
     onError: (error) => SentryLogger.error('Failed to fetch videos', 'ChannelWithVideos', error),
   })
 
-  const placeholderItems = Array.from({ length: placeholdersCount }, () => ({ id: undefined }))
+  const { url: avatarUrl, isLoadingAsset: isLoadingAvatar } = useAsset({ entity: channel, assetType: AssetType.AVATAR })
+  const { toggleFollowing, isFollowing } = useHandleFollowChannel(channelId)
+
+  const targetItemsCount = videosPerRow * INITIAL_ROWS
+  const displayedVideos = (videos || []).slice(0, targetItemsCount)
+  const placeholderItems = videosLoading ? Array.from({ length: targetItemsCount }, () => ({ id: undefined })) : []
+
   const gridContent = (
     <>
-      {[...displayedItems, ...placeholderItems]?.map((video, idx) => (
+      {[...displayedVideos, ...placeholderItems].map((video, idx) => (
         <VideoTile id={video.id} key={`channels-with-videos-${idx}`} showChannel />
       ))}
     </>
   )
 
-  const isLoading = !channelId || loading
+  const isLoading = !channelId || channelLoading
 
-  if (error) {
+  if (channelError || videosError) {
     return null
   }
 
   return (
     <>
       <ChannelCardAnchor to={absoluteRoutes.viewer.channel(channelId)}>
-        <StyledAvatar size="channel" loading={isLoading} assetUrl={avatarUrl} />
+        <StyledAvatar size="channel" loading={isLoading || isLoadingAvatar} assetUrl={avatarUrl} />
         <InfoWrapper>
           {isLoading ? (
             <SkeletonLoader width="120px" height="20px" bottomSpace="4px" />

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

@@ -2,7 +2,7 @@ import { Meta, Story } from '@storybook/react'
 import React, { useState } from 'react'
 
 import { OverlayManagerProvider } from '@/providers/overlayManager'
-import { Button } from '@/shared/components'
+import { Button } from '@/shared/components/Button'
 
 import { ActionDialog, ActionDialogProps } from './ActionDialog'
 

+ 1 - 1
src/components/Dialogs/ActionDialog/ActionDialog.tsx

@@ -1,6 +1,6 @@
 import React from 'react'
 
-import { Button, ButtonProps } from '@/shared/components'
+import { Button, ButtonProps } from '@/shared/components/Button'
 
 import { ActionsContainer, AdditionalActionsContainer, ButtonsContainer } from './ActionDialog.style'
 

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

@@ -2,7 +2,7 @@ import { Meta, Story } from '@storybook/react'
 import React, { useState } from 'react'
 
 import { OverlayManagerProvider } from '@/providers/overlayManager'
-import { Button } from '@/shared/components'
+import { Button } from '@/shared/components/Button'
 
 import { BaseDialogProps, BaseDialog as Dialog } from './BaseDialog'
 

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

@@ -1,6 +1,6 @@
 import styled from '@emotion/styled'
 
-import { IconButton } from '@/shared/components'
+import { IconButton } from '@/shared/components/IconButton'
 import { colors, media, sizes, zIndex } from '@/shared/theme'
 
 export const DialogBackDrop = styled.div`

+ 2 - 2
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'
-import { useOverlayManager } from '@/providers'
+import { Portal } from '@/components/Portal'
+import { useOverlayManager } from '@/providers/overlayManager'
 import { SvgGlyphClose } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 

+ 3 - 2
src/components/Dialogs/ImageCropDialog/ImageCropDialog.stories.tsx

@@ -2,8 +2,9 @@ import styled from '@emotion/styled/'
 import { Meta, Story } from '@storybook/react'
 import React, { useRef, useState } from 'react'
 
-import { OverlayManagerProvider } from '@/providers'
-import { Avatar, SkeletonLoader } from '@/shared/components'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
+import { Avatar } from '@/shared/components/Avatar'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
 import { AssetDimensions, ImageCropData } from '@/types/cropper'
 
 import { ImageCropDialog, ImageCropDialogImperativeHandle, ImageCropDialogProps } from './ImageCropDialog'

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

@@ -1,8 +1,9 @@
 import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
-import { SkeletonLoader, Text } from '@/shared/components'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
 import { Slider } from '@/shared/components/Slider'
+import { Text } from '@/shared/components/Text'
 import { colors, sizes } from '@/shared/theme'
 
 import { ActionDialog } from '../ActionDialog'

+ 1 - 1
src/components/Dialogs/ImageCropDialog/ImageCropDialog.tsx

@@ -1,6 +1,6 @@
 import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'
 
-import { IconButton } from '@/shared/components'
+import { IconButton } from '@/shared/components/IconButton'
 import { SvgGlyphPan, SvgGlyphZoomIn, SvgGlyphZoomOut } from '@/shared/icons'
 import { AssetDimensions, ImageCropData } from '@/types/cropper'
 import { validateImage } from '@/utils/image'

+ 5 - 5
src/components/Dialogs/ImageCropDialog/cropper.ts

@@ -33,10 +33,10 @@ const CANVAS_OPTS_PER_TYPE: Record<CropperImageType, Cropper.GetCroppedCanvasOpt
   videoThumbnail: {
     minWidth: 1280,
     minHeight: 720,
-    width: 1920,
-    height: 1080,
-    maxWidth: 3840,
-    maxHeight: 2160,
+    width: 1280,
+    height: 720,
+    maxWidth: 1920,
+    maxHeight: 1080,
   },
   cover: {
     minWidth: 1920,
@@ -170,7 +170,7 @@ export const useCropper = ({ imageEl, imageType, cropData }: UseCropperOpts) =>
         }
         const url = URL.createObjectURL(blob)
         resolve([blob, url, assetDimensions, imageCropData])
-      }, 'image/jpeg')
+      }, 'image/webp')
     })
   }
 

+ 1 - 1
src/components/Dialogs/MessageDialog/MessageDialog.style.ts

@@ -1,6 +1,6 @@
 import styled from '@emotion/styled'
 
-import { Text } from '@/shared/components'
+import { Text } from '@/shared/components/Text'
 import { colors, sizes } from '@/shared/theme'
 
 export const MessageIconWrapper = styled.div`

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

@@ -2,7 +2,7 @@ import { Meta, Story } from '@storybook/react'
 import React, { useState } from 'react'
 
 import { OverlayManagerProvider } from '@/providers/overlayManager'
-import { Button } from '@/shared/components'
+import { Button } from '@/shared/components/Button'
 
 import { Multistepper } from './Multistepper'
 

+ 1 - 43
src/components/Dialogs/Multistepper/Multistepper.style.ts

@@ -1,20 +1,10 @@
 import styled from '@emotion/styled'
 
-import { Text } from '@/shared/components'
 import { SvgGlyphChevronRight } from '@/shared/icons'
-import { colors, media, sizes, typography } from '@/shared/theme'
+import { colors, media, sizes } from '@/shared/theme'
 
 import { BaseDialog } from '../BaseDialog'
 
-type CircleProps = {
-  isFilled?: boolean
-  isActive?: boolean
-}
-
-type StyledStepInfoProps = {
-  isActive?: boolean
-}
-
 export const StyledDialog = styled(BaseDialog)`
   max-width: 740px;
 `
@@ -51,40 +41,8 @@ export const StyledStepsInfoContainer = styled.div`
     width: 100%;
     grid-template-columns: repeat(6, auto);
     align-items: center;
-    grid-column-gap: ${sizes(4)};
   }
 `
-export const StyledStepInfo = styled.div<StyledStepInfoProps>`
-  display: ${({ isActive }) => (isActive ? 'flex' : 'none')};
-  align-items: center;
-
-  ${media.small} {
-    display: flex;
-  }
-`
-export const StyledCircle = styled.div<CircleProps>`
-  display: flex;
-  flex-shrink: 0;
-  justify-content: center;
-  align-items: center;
-  width: 32px;
-  height: 32px;
-  border-radius: 100%;
-  background-color: ${({ isActive }) => (isActive ? colors.blue[500] : colors.gray[400])};
-  color: ${colors.gray[50]};
-`
-export const StyledStepInfoText = styled.div<StyledStepInfoProps>`
-  display: flex;
-  flex-direction: column;
-  flex-grow: 1;
-  justify-content: center;
-  font-weight: ${typography.weights.semibold};
-  margin-left: ${sizes(2)};
-`
-
-export const StyledStepTitle = styled(Text)`
-  margin-top: ${sizes(1)};
-`
 
 export const StyledChevron = styled(SvgGlyphChevronRight)`
   margin: 0 ${sizes(1)};

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

@@ -1,18 +1,8 @@
 import React, { Fragment } from 'react'
 
-import { Text } from '@/shared/components'
-import { SvgGlyphCheck } from '@/shared/icons'
+import { Step } from '@/shared/components/Step'
 
-import {
-  StyledChevron,
-  StyledCircle,
-  StyledDialog,
-  StyledHeader,
-  StyledStepInfo,
-  StyledStepInfoText,
-  StyledStepTitle,
-  StyledStepsInfoContainer,
-} from './Multistepper.style'
+import { StyledChevron, StyledDialog, StyledHeader, StyledStepsInfoContainer } from './Multistepper.style'
 
 import { BaseDialogProps } from '../BaseDialog'
 
@@ -38,18 +28,8 @@ export const Multistepper: React.FC<MultistepperProps> = ({ steps, currentStepId
 
             return (
               <Fragment key={idx}>
-                <StyledStepInfo isActive={isActive}>
-                  <StyledCircle isFilled={isActive || isCompleted} isActive={isActive}>
-                    {isCompleted ? <SvgGlyphCheck /> : idx + 1}
-                  </StyledCircle>
-                  <StyledStepInfoText isActive={isActive}>
-                    <Text variant="caption" secondary>
-                      Step {idx + 1}
-                    </Text>
-                    <StyledStepTitle variant="overhead">{step.title}</StyledStepTitle>
-                  </StyledStepInfoText>
-                </StyledStepInfo>
-                {isLast ? null : <StyledChevron />}
+                <Step title={step.title} number={idx + 1} active={isActive} completed={isCompleted} />
+                {!isLast && <StyledChevron />}
               </Fragment>
             )
           })}

+ 1 - 1
src/components/Dialogs/TransactionDialog/TransactionDialog.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 
 import { ExtrinsicStatus } from '@/joystream-lib'
-import { Tooltip } from '@/shared/components'
+import { Tooltip } from '@/shared/components/Tooltip'
 
 import { Step, StepsBar, StyledSpinner, StyledTransactionIllustration, TextContainer } from './TransactionDialog.style'
 

+ 2 - 1
src/components/InfiniteGrids/InfiniteChannelGrid.tsx

@@ -7,7 +7,8 @@ import {
   GetChannelsConnectionQuery,
   GetChannelsConnectionQueryVariables,
 } from '@/api/queries'
-import { Grid, Text } from '@/shared/components'
+import { Grid } from '@/shared/components/Grid'
+import { Text } from '@/shared/components/Text'
 import { sizes } from '@/shared/theme'
 import { SentryLogger } from '@/utils/logs'
 

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

@@ -11,15 +11,12 @@ import {
 import { ChannelWithVideos } from '@/components/ChannelWithVideos'
 import { useInfiniteGrid } from '@/components/InfiniteGrids/useInfiniteGrid'
 import { languages } from '@/config/languages'
-import {
-  EmptyFallback,
-  GridHeadingContainer,
-  LoadMoreButton,
-  Select,
-  SkeletonLoader,
-  Text,
-  TitleContainer,
-} from '@/shared/components'
+import { EmptyFallback } from '@/shared/components/EmptyFallback'
+import { GridHeadingContainer, TitleContainer } from '@/shared/components/GridHeading'
+import { LoadMoreButton } from '@/shared/components/LoadMoreButton'
+import { Select } from '@/shared/components/Select'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
+import { Text } from '@/shared/components/Text'
 import { SvgGlyphChevronRight } from '@/shared/icons'
 import { SentryLogger } from '@/utils/logs'
 

+ 1 - 1
src/components/InfiniteGrids/InfiniteGrid.style.ts

@@ -1,6 +1,6 @@
 import styled from '@emotion/styled'
 
-import { Button } from '@/shared/components'
+import { Button } from '@/shared/components/Button'
 import { colors, sizes } from '@/shared/theme'
 
 export const LoadMoreButtonWrapper = styled.div`

+ 5 - 1
src/components/InfiniteGrids/InfiniteVideoGrid.tsx

@@ -7,7 +7,11 @@ import {
   GetVideosConnectionQueryVariables,
   VideoWhereInput,
 } from '@/api/queries'
-import { Grid, GridHeadingContainer, LoadMoreButton, SkeletonLoader, Text, TitleContainer } from '@/shared/components'
+import { Grid } from '@/shared/components/Grid'
+import { GridHeadingContainer, TitleContainer } from '@/shared/components/GridHeading'
+import { LoadMoreButton } from '@/shared/components/LoadMoreButton'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
+import { Text } from '@/shared/components/Text'
 import { SvgGlyphChevronRight } from '@/shared/icons'
 import { SentryLogger } from '@/utils/logs'
 

+ 2 - 2
src/components/InterruptedVideosGallery.tsx

@@ -1,8 +1,8 @@
 import { RouteComponentProps } from '@reach/router'
 import React from 'react'
 
-import { VideoGallery } from '@/components'
-import { usePersonalDataStore } from '@/providers'
+import { VideoGallery } from '@/components/VideoGallery'
+import { usePersonalDataStore } from '@/providers/personalData'
 import { ConsoleLogger } from '@/utils/logs'
 
 const INTERRUPTED_VIDEOS_COUNT = 16

+ 2 - 1
src/components/NoConnectionIndicator/NoConnectionIndicator.stories.tsx

@@ -1,7 +1,8 @@
 import { Meta, Story } from '@storybook/react'
 import React from 'react'
 
-import { ConnectionStatusManager, Snackbars } from '@/providers'
+import { ConnectionStatusManager } from '@/providers/connectionStatus'
+import { Snackbars } from '@/providers/snackbars'
 
 import { NoConnectionIndicator, NoConnectionIndicatorProps } from './NoConnectionIndicator'
 

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

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

+ 2 - 2
src/components/NoConnectionIndicator/NoConnectionIndicator.tsx

@@ -1,8 +1,8 @@
 import React from 'react'
 import { CSSTransition } from 'react-transition-group'
 
-import { ConnectionStatus } from '@/providers'
-import { Text } from '@/shared/components'
+import { ConnectionStatus } from '@/providers/connectionStatus'
+import { Text } from '@/shared/components/Text'
 import { SvgAlertWarning } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 

+ 9 - 8
src/components/OfficialJoystreamUpdate.tsx

@@ -1,20 +1,21 @@
 import React from 'react'
 
-import { useVideos } from '@/api/hooks'
-import { VideoGallery } from '@/components'
+import { useChannelPreviewVideos } from '@/api/hooks'
+import { VideoGallery } from '@/components/VideoGallery'
 import { readEnv } from '@/config/envs'
+import { SentryLogger } from '@/utils/logs'
 
 const channelId = readEnv('OFFICIAL_JOYSTREAM_CHANNEL_ID')
-const MAX_VIDEOS = 10
 
 export const OfficialJoystreamUpdate = () => {
-  const { videos, loading } = useVideos({
-    where: {
-      channelId_eq: channelId,
-    },
-    limit: MAX_VIDEOS,
+  const { videos, loading, error } = useChannelPreviewVideos(channelId, {
+    onError: (error) => SentryLogger.error('Failed to fetch videos', 'OfficialJoystreamUpdate', error),
   })
 
+  if (error) {
+    return null
+  }
+
   return (
     <section>
       <VideoGallery title="Official Joystream updates" videos={videos || []} loading={loading} />

+ 1 - 1
src/components/Sidenav/SidenavBase.style.ts

@@ -2,8 +2,8 @@ import isPropValid from '@emotion/is-prop-valid'
 import styled from '@emotion/styled'
 import { Link, LinkProps } from 'react-router-dom'
 
-import { Text } from '@/shared/components'
 import { badgeStyles } from '@/shared/components/Badge'
+import { Text } from '@/shared/components/Text'
 import { SvgJoystreamFullLogo } from '@/shared/illustrations'
 import { colors, media, sizes, transitions, typography, zIndex } from '@/shared/theme'
 

+ 5 - 8
src/components/Sidenav/StudioSidenav/StudioSidenav.tsx

@@ -3,14 +3,11 @@ import { CSSTransition } from 'react-transition-group'
 
 import { NavItemType, SidenavBase } from '@/components/Sidenav/SidenavBase'
 import { absoluteRoutes } from '@/config/routes'
-import {
-  chanelUnseenDraftsSelector,
-  useAuthorizedUser,
-  useDraftStore,
-  useEditVideoSheet,
-  useUploadsStore,
-} from '@/providers'
-import { Button } from '@/shared/components'
+import { chanelUnseenDraftsSelector, useDraftStore } from '@/providers/drafts'
+import { useEditVideoSheet } from '@/providers/editVideoSheet'
+import { useUploadsStore } from '@/providers/uploadsManager'
+import { useAuthorizedUser } from '@/providers/user'
+import { Button } from '@/shared/components/Button'
 import { SvgGlyphAddVideo, SvgGlyphExternal, SvgNavChannel, SvgNavUpload, SvgNavVideos } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 import { openInNewTab } from '@/utils/browser'

+ 2 - 2
src/components/Sidenav/ViewerSidenav/FollowedChannels.style.ts

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled'
 
-import { ChannelLink } from '@/components'
-import { Text } from '@/shared/components'
+import { ChannelLink } from '@/components/ChannelLink'
+import { Text } from '@/shared/components/Text'
 import { colors, sizes, typography } from '@/shared/theme'
 
 import { EXPANDED_SIDENAVBAR_WIDTH, NAVBAR_LEFT_PADDING } from '../SidenavBase.style'

+ 2 - 2
src/components/Sidenav/ViewerSidenav/ViewerSidenav.tsx

@@ -2,8 +2,8 @@ import React, { useState } from 'react'
 
 import { NavItemType, SidenavBase } from '@/components/Sidenav/SidenavBase'
 import { absoluteRoutes } from '@/config/routes'
-import { usePersonalDataStore } from '@/providers'
-import { Button } from '@/shared/components'
+import { usePersonalDataStore } from '@/providers/personalData'
+import { Button } from '@/shared/components/Button'
 import { SvgGlyphExternal, SvgNavChannels, SvgNavHome, SvgNavNew, SvgNavPopular } from '@/shared/icons'
 import { openInNewTab } from '@/utils/browser'
 import { ConsoleLogger } from '@/utils/logs'

+ 3 - 1
src/components/SignInSteps/AccountStep.style.ts

@@ -1,7 +1,9 @@
 import styled from '@emotion/styled'
 
-import { Button, RadioButton, Text } from '@/shared/components'
+import { Button } from '@/shared/components/Button'
+import { RadioButton } from '@/shared/components/RadioButton'
 import { Spinner } from '@/shared/components/Spinner'
+import { Text } from '@/shared/components/Text'
 import { SvgAccountCreationIllustration } from '@/shared/illustrations'
 import { colors, media, sizes, transitions, typography } from '@/shared/theme'
 

+ 2 - 2
src/components/SignInSteps/AccountStep.tsx

@@ -2,8 +2,8 @@ import React, { FormEvent, useState } from 'react'
 import { useNavigate } from 'react-router'
 import { CSSTransition, SwitchTransition } from 'react-transition-group'
 
-import { useUser } from '@/providers'
-import { Text } from '@/shared/components'
+import { useUser } from '@/providers/user'
+import { Text } from '@/shared/components/Text'
 import { SvgGlyphChannel, SvgOutlineConnect } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 

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

@@ -1,6 +1,7 @@
 import styled from '@emotion/styled'
 
-import { Button, Text } from '@/shared/components'
+import { Button } from '@/shared/components/Button'
+import { Text } from '@/shared/components/Text'
 import { sizes } from '@/shared/theme'
 
 import { StepFooter } from './SignInSteps.style'

+ 4 - 2
src/components/SignInSteps/ExtensionStep.tsx

@@ -2,8 +2,10 @@ import React, { useEffect } from 'react'
 import { useNavigate } from 'react-router'
 
 import { useRouterQuery } from '@/hooks'
-import { useDialog, useUser } from '@/providers'
-import { Button, Text } from '@/shared/components'
+import { useDialog } from '@/providers/dialogs'
+import { useUser } from '@/providers/user'
+import { Button } from '@/shared/components/Button'
+import { Text } from '@/shared/components/Text'
 import { SvgGlyphExternal } from '@/shared/icons'
 
 import { PolkadotExtensionRejectedWrapper, StyledButton, StyledListItem, StyledStepFooter } from './ExtensionStep.style'

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

@@ -1,6 +1,6 @@
 import styled from '@emotion/styled'
 
-import { Text } from '@/shared/components'
+import { Text } from '@/shared/components/Text'
 import { SvgGlyphWarning } from '@/shared/icons/GlyphWarning'
 import { SvgJoystreamLogo, SvgPolkadotLogo } from '@/shared/illustrations'
 import { colors, sizes } from '@/shared/theme'

+ 2 - 1
src/components/SignInSteps/TermsStep.style.tsx

@@ -1,6 +1,7 @@
 import styled from '@emotion/styled'
 
-import { Button, IconButton } from '@/shared/components'
+import { Button } from '@/shared/components/Button'
+import { IconButton } from '@/shared/components/IconButton'
 import { colors, sizes } from '@/shared/theme'
 
 export const TermsBox = styled.div`

+ 1 - 1
src/components/SkeletonLoaderVideoGrid.tsx

@@ -1,6 +1,6 @@
 import React from 'react'
 
-import { Grid } from '@/shared/components'
+import { Grid } from '@/shared/components/Grid'
 
 import { VideoTile } from './VideoTile'
 

+ 4 - 3
src/components/StudioEntrypoint.tsx

@@ -2,10 +2,11 @@ import styled from '@emotion/styled'
 import React from 'react'
 import { Navigate } from 'react-router-dom'
 
-import { TOP_NAVBAR_HEIGHT } from '@/components'
+import { TOP_NAVBAR_HEIGHT } from '@/components/Topbar'
 import { absoluteRoutes } from '@/config/routes'
-import { useUser } from '@/providers'
-import { Spinner, Text } from '@/shared/components'
+import { useUser } from '@/providers/user'
+import { Spinner } from '@/shared/components/Spinner'
+import { Text } from '@/shared/components/Text'
 
 const DEFAULT_ROUTE = absoluteRoutes.studio.videos()
 

+ 2 - 1
src/components/TermsOfService.tsx

@@ -1,6 +1,7 @@
 import React from 'react'
 
-import { LegalLastUpdateText, LegalListItem, LegalParagraph, Text } from '@/shared/components'
+import { LegalLastUpdateText, LegalListItem, LegalParagraph } from '@/shared/components/LegalText'
+import { Text } from '@/shared/components/Text'
 
 export const TermsOfService: React.FC = () => {
   return (

+ 1 - 1
src/components/TopTenThisWeek.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 
 import { useMostViewedVideos } from '@/api/hooks'
-import { VideoGallery } from '@/components'
+import { VideoGallery } from '@/components/VideoGallery'
 
 export const TopTenThisWeek = () => {
   const { videos, loading } = useMostViewedVideos({ limit: 10, timePeriodDays: 7 })

+ 3 - 1
src/components/Topbar/StudioTopbar/StudioTopbar.style.tsx

@@ -1,7 +1,9 @@
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
-import { Avatar, SkeletonLoader, Text } from '@/shared/components'
+import { Avatar } from '@/shared/components/Avatar'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
+import { Text } from '@/shared/components/Text'
 import { colors, media, sizes, transitions, typography, zIndex } from '@/shared/theme'
 
 import { TopbarBase } from '../TopbarBase'

+ 8 - 2
src/components/Topbar/StudioTopbar/StudioTopbar.tsx

@@ -5,8 +5,14 @@ import { CSSTransition } from 'react-transition-group'
 import { BasicChannelFieldsFragment } from '@/api/queries'
 import { absoluteRoutes } from '@/config/routes'
 import { useDisplayDataLostWarning } from '@/hooks'
-import { AssetType, useAsset, useEditVideoSheet, useUser } from '@/providers'
-import { Button, ExpandButton, IconButton, SkeletonLoader, Text } from '@/shared/components'
+import { AssetType, useAsset } from '@/providers/assets'
+import { useEditVideoSheet } from '@/providers/editVideoSheet'
+import { useUser } from '@/providers/user'
+import { Button } from '@/shared/components/Button'
+import { ExpandButton } from '@/shared/components/ExpandButton'
+import { IconButton } from '@/shared/components/IconButton'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
+import { Text } from '@/shared/components/Text'
 import { SvgGlyphAddVideo, SvgGlyphCheck, SvgGlyphLogOut, SvgGlyphNewChannel } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 

+ 1 - 1
src/components/Topbar/TopbarBase.style.tsx

@@ -2,7 +2,7 @@ import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
 import { StyledSearchbar } from '@/components/Topbar/ViewerTopbar/ViewerTopbar.style'
-import { Text } from '@/shared/components'
+import { Text } from '@/shared/components/Text'
 import { SvgJoystreamFullLogo, SvgJoystreamOneLetterLogo } from '@/shared/illustrations'
 import { colors, media, sizes, transitions, typography, zIndex } from '@/shared/theme'
 

+ 2 - 3
src/components/VideoGallery.tsx

@@ -2,8 +2,9 @@ import styled from '@emotion/styled'
 import React, { useMemo } from 'react'
 
 import { VideoFieldsFragment } from '@/api/queries'
-import { Gallery, RankingNumberTile } from '@/shared/components'
+import { Gallery } from '@/shared/components/Gallery'
 import { breakpointsOfGrid } from '@/shared/components/Grid'
+import { RankingNumberTile } from '@/shared/components/RankingNumberTile'
 import { AvatarContainer } from '@/shared/components/VideoTileBase/VideoTileBase.styles'
 import { media } from '@/shared/theme'
 
@@ -126,8 +127,6 @@ export const VideoGallery: React.FC<VideoGalleryProps> = ({
 }
 
 const StyledVideoTile = styled(VideoTile)`
-  flex-shrink: 0;
-
   ${AvatarContainer} {
     display: none;
 

+ 1 - 1
src/components/VideoGrid.tsx

@@ -2,7 +2,7 @@ import styled from '@emotion/styled'
 import React from 'react'
 
 import { VideoFieldsFragment } from '@/api/queries'
-import { Grid } from '@/shared/components'
+import { Grid } from '@/shared/components/Grid'
 
 import { VideoTile } from './VideoTile'
 

+ 14 - 5
src/components/VideoHero/VideoHero.style.ts

@@ -1,11 +1,13 @@
 import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 
-import { IconButton, SkeletonLoader, Text } from '@/shared/components'
+import { IconButton } from '@/shared/components/IconButton'
+import { SkeletonLoader } from '@/shared/components/SkeletonLoader'
+import { Text } from '@/shared/components/Text'
 import { colors, media, sizes, typography } from '@/shared/theme'
 
-import { TOP_NAVBAR_HEIGHT } from '..'
 import { ChannelLink } from '../ChannelLink'
+import { TOP_NAVBAR_HEIGHT } from '../Topbar'
 
 const BUTTONS_HEIGHT = 48
 
@@ -57,7 +59,7 @@ export const GradientOverlay = styled.div`
     ${colors.transparentBlack[54]};
 `
 
-export const InfoContainer = styled.div<{ isLoading: boolean }>`
+export const InfoContainer = styled.div`
   position: relative;
   padding-bottom: ${sizes(16)};
   width: 100%;
@@ -87,6 +89,8 @@ export const StyledChannelLink = styled(ChannelLink)`
 `
 
 export const TitleContainer = styled.div`
+  min-height: 80px;
+
   a {
     text-decoration: none;
   }
@@ -109,10 +113,15 @@ export const Title = styled(Text)`
 `
 
 export const TitleSkeletonLoader = styled(SkeletonLoader)`
-  margin-bottom: ${sizes(4)};
+  width: 90%;
+  margin-bottom: ${sizes(2)};
+
+  ${media.compact} {
+    width: 380px;
+  }
 
   ${media.medium} {
-    margin-bottom: ${sizes(5)};
+    margin-bottom: ${sizes(3)};
   }
 `
 

+ 15 - 20
src/components/VideoHero/VideoHero.tsx

@@ -1,14 +1,14 @@
 import React, { useState } from 'react'
 import { Link } from 'react-router-dom'
-import { CSSTransition } from 'react-transition-group'
 
 import { absoluteRoutes } from '@/config/routes'
-import { AssetType, useAsset } from '@/providers'
-import { Button, GridItem, LayoutGrid, SkeletonLoader, VideoPlayer } from '@/shared/components'
+import { AssetType, useAsset } from '@/providers/assets'
+import { Button } from '@/shared/components/Button'
+import { GridItem, LayoutGrid } from '@/shared/components/LayoutGrid'
+import { VideoPlayer } from '@/shared/components/VideoPlayer'
 import { SvgActionPlay } from '@/shared/icons/ActionPlay'
 import { SvgActionSoundOff } from '@/shared/icons/ActionSoundOff'
 import { SvgActionSoundOn } from '@/shared/icons/ActionSoundOn'
-import { transitions } from '@/shared/theme'
 
 import {
   ButtonsContainer,
@@ -33,7 +33,6 @@ export const VideoHero: React.FC = () => {
   const coverVideo = useVideoHero()
 
   const [videoPlaying, setVideoPlaying] = useState(false)
-  const [displayControls, setDisplayControls] = useState(false)
   const [soundMuted, setSoundMuted] = useState(true)
   const { url: thumbnailPhotoUrl } = useAsset({
     entity: coverVideo?.video,
@@ -42,7 +41,6 @@ export const VideoHero: React.FC = () => {
 
   const handlePlaybackDataLoaded = () => {
     setTimeout(() => {
-      setDisplayControls(true)
       setVideoPlaying(true)
     }, VIDEO_PLAYBACK_DELAY)
   }
@@ -75,7 +73,7 @@ export const VideoHero: React.FC = () => {
           <GradientOverlay />
         </Media>
       </MediaWrapper>
-      <InfoContainer isLoading={!coverVideo}>
+      <InfoContainer>
         <StyledChannelLink
           variant="secondary"
           id={coverVideo?.video.channel.id}
@@ -93,31 +91,28 @@ export const VideoHero: React.FC = () => {
                 </>
               ) : (
                 <>
-                  <TitleSkeletonLoader width={380} height={60} />
-                  <SkeletonLoader width={300} height={20} bottomSpace={4} />
-                  <SkeletonLoader width={200} height={20} />
+                  <TitleSkeletonLoader height={34} />
+                  <TitleSkeletonLoader height={34} />
                 </>
               )}
             </TitleContainer>
           </GridItem>
         </LayoutGrid>
         <ButtonsSpaceKeeper>
-          <CSSTransition
-            in={displayControls}
-            timeout={parseInt(transitions.timings.loading)}
-            classNames={transitions.names.fade}
-            unmountOnExit
-            appear
-          >
+          {coverVideo && (
             <ButtonsContainer>
-              <Button to={absoluteRoutes.viewer.video(coverVideo ? coverVideo.video.id : '')} icon={<SvgActionPlay />}>
+              <Button
+                size="large"
+                to={absoluteRoutes.viewer.video(coverVideo ? coverVideo.video.id : '')}
+                icon={<SvgActionPlay />}
+              >
                 Play
               </Button>
-              <SoundButton variant="secondary" onClick={handleSoundToggleClick}>
+              <SoundButton size="large" variant="secondary" onClick={handleSoundToggleClick}>
                 {!soundMuted ? <SvgActionSoundOn /> : <SvgActionSoundOff />}
               </SoundButton>
             </ButtonsContainer>
-          </CSSTransition>
+          )}
         </ButtonsSpaceKeeper>
       </InfoContainer>
     </Container>

+ 49 - 21
src/components/VideoTile.tsx

@@ -3,7 +3,8 @@ import React from 'react'
 import { useVideo } from '@/api/hooks'
 import { AssetAvailability } from '@/api/queries'
 import { absoluteRoutes } from '@/config/routes'
-import { AssetType, singleDraftSelector, useAsset, useDraftStore } from '@/providers'
+import { AssetType, useAsset } from '@/providers/assets'
+import { singleDraftSelector, useDraftStore } from '@/providers/drafts'
 import {
   VideoTileBase,
   VideoTileBaseMetaProps,
@@ -20,18 +21,24 @@ export type VideoTileProps = {
   Pick<VideoTileBaseProps, 'progress' | 'className'>
 
 export const VideoTile: React.FC<VideoTileProps> = ({ id, onNotFound, ...metaProps }) => {
-  const { video, loading, videoHref } = useVideoSharedLogic({ id, isDraft: false, onNotFound })
-  const { url: thumbnailPhotoUrl } = useAsset({
-    entity: video,
-    assetType: AssetType.THUMBNAIL,
-  })
-  const { url: avatarPhotoUrl } = useAsset({
-    entity: video?.channel,
-    assetType: AssetType.AVATAR,
+  const {
+    video,
+    loading,
+    videoHref,
+    thumbnailPhotoUrl,
+    avatarPhotoUrl,
+    isLoadingThumbnail,
+    isLoadingAvatar,
+  } = useVideoSharedLogic({
+    id,
+    isDraft: false,
+    onNotFound,
   })
 
   return (
     <VideoTileBase
+      isLoadingThumbnail={isLoadingThumbnail}
+      isLoadingAvatar={isLoadingAvatar}
       publisherMode={false}
       title={video?.title}
       channelTitle={video?.channel.title}
@@ -44,7 +51,6 @@ export const VideoTile: React.FC<VideoTileProps> = ({ id, onNotFound, ...metaPro
       onCopyVideoURLClick={() => copyToClipboard(videoHref ? location.origin + videoHref : '')}
       thumbnailUrl={thumbnailPhotoUrl}
       isLoading={loading}
-      contentKey={id}
       {...metaProps}
     />
   )
@@ -53,21 +59,27 @@ export const VideoTile: React.FC<VideoTileProps> = ({ id, onNotFound, ...metaPro
 export type VideoTileWPublisherProps = VideoTileProps &
   Omit<VideoTilePublisherProps, 'publisherMode' | 'videoPublishState'>
 export const VideoTilePublisher: React.FC<VideoTileWPublisherProps> = ({ id, isDraft, onNotFound, ...metaProps }) => {
-  const { video, loading, videoHref } = useVideoSharedLogic({ id, isDraft, onNotFound })
-  const draft = useDraftStore(singleDraftSelector(id ?? ''))
-  const { url: thumbnailPhotoUrl } = useAsset({
-    entity: video,
-    assetType: AssetType.THUMBNAIL,
-  })
-  const { url: avatarPhotoUrl } = useAsset({
-    entity: video?.channel,
-    assetType: AssetType.AVATAR,
+  const {
+    video,
+    loading,
+    videoHref,
+    thumbnailPhotoUrl,
+    avatarPhotoUrl,
+    isLoadingThumbnail,
+    isLoadingAvatar,
+  } = useVideoSharedLogic({
+    id,
+    isDraft,
+    onNotFound,
   })
+  const draft = useDraftStore(singleDraftSelector(id ?? ''))
 
   const hasThumbnailUploadFailed = video?.thumbnailPhotoAvailability === AssetAvailability.Pending
 
   return (
     <VideoTileBase
+      isLoadingThumbnail={isLoadingThumbnail}
+      isLoadingAvatar={isLoadingAvatar}
       publisherMode
       title={isDraft ? draft?.title : video?.title}
       channelTitle={video?.channel.title}
@@ -83,7 +95,6 @@ export const VideoTilePublisher: React.FC<VideoTileWPublisherProps> = ({ id, isD
       onCopyVideoURLClick={isDraft ? undefined : () => copyToClipboard(videoHref ? location.origin + videoHref : '')}
       videoPublishState={video?.isPublic || video?.isPublic === undefined ? 'default' : 'unlisted'}
       isDraft={isDraft}
-      contentKey={id}
       {...metaProps}
     />
   )
@@ -100,7 +111,24 @@ const useVideoSharedLogic = ({ id, isDraft, onNotFound }: UseVideoSharedLogicOpt
     onCompleted: (data) => !data && onNotFound?.(),
     onError: (error) => SentryLogger.error('Failed to fetch video', 'VideoTile', error, { video: { id } }),
   })
+  const { url: thumbnailPhotoUrl, isLoadingAsset: isLoadingThumbnail } = useAsset({
+    entity: video,
+    assetType: AssetType.THUMBNAIL,
+  })
+  const { url: avatarPhotoUrl, isLoadingAsset: isLoadingAvatar } = useAsset({
+    entity: video?.channel,
+    assetType: AssetType.AVATAR,
+  })
+
   const internalIsLoadingState = loading || !id
   const videoHref = id ? absoluteRoutes.viewer.video(id) : undefined
-  return { video, loading: internalIsLoadingState, videoHref }
+  return {
+    video,
+    loading: internalIsLoadingState,
+    isLoadingThumbnail,
+    isLoadingAvatar,
+    thumbnailPhotoUrl,
+    avatarPhotoUrl,
+    videoHref,
+  }
 }

+ 3 - 1
src/components/ViewErrorFallback.tsx

@@ -5,7 +5,9 @@ import { useNavigate } from 'react-router-dom'
 
 import { absoluteRoutes } from '@/config/routes'
 import { JOYSTREAM_DISCORD_URL } from '@/config/urls'
-import { AnimatedError, Button, Text } from '@/shared/components'
+import { AnimatedError } from '@/shared/components/AnimatedError'
+import { Button } from '@/shared/components/Button'
+import { Text } from '@/shared/components/Text'
 import { media, sizes } from '@/shared/theme'
 import { SentryLogger } from '@/utils/logs'
 

+ 0 - 30
src/components/index.ts

@@ -1,30 +0,0 @@
-export * from './VideoGallery'
-export * from './VideoHero'
-export * from './ChannelGallery'
-export * from './Topbar/ViewerTopbar'
-export * from './Topbar/StudioTopbar'
-export * from './VideoGrid'
-export * from './SkeletonLoaderVideoGrid'
-export * from './VideoTile'
-export * from './ChannelCard'
-export * from './ChannelGrid'
-export * from './ViewErrorFallback'
-export * from './ChannelLink'
-export * from './BackgroundPattern'
-export * from './InfiniteGrids'
-export * from './Sidenav'
-export * from './InterruptedVideosGallery'
-export * from './ViewWrapper'
-export * from './Portal'
-export * from './Dialogs'
-export * from './LimitedWidthContainer'
-export * from './Topbar'
-export * from './NoConnectionIndicator'
-export * from './SignInSteps'
-export * from './StudioEntrypoint'
-export * from './PrivateRoute'
-export * from './OfficialJoystreamUpdate'
-export * from './TopTenThisWeek'
-export * from './TopTenChannels'
-export * from './ChannelWithVideos'
-export * from './DiscoverChannels'

+ 89 - 0
src/components/templates/VideoContentTemplate.tsx

@@ -0,0 +1,89 @@
+import styled from '@emotion/styled'
+import React from 'react'
+
+import { LimitedWidthContainer } from '@/components/LimitedWidthContainer'
+import { absoluteRoutes } from '@/config/routes'
+import {
+  CallToActionButton,
+  CallToActionButtonProps,
+  CallToActionWrapper,
+} from '@/shared/components/CallToActionButton'
+import { Text } from '@/shared/components/Text'
+import { SvgNavChannels, SvgNavHome, SvgNavNew, SvgNavPopular } from '@/shared/icons'
+import { media, sizes, typography } from '@/shared/theme'
+
+type CtaData = 'home' | 'new' | 'channels' | 'popular'
+
+type VideoContentTemplateProps = {
+  title?: string
+  cta?: CtaData[]
+}
+
+const CTA_MAP: Record<string, CallToActionButtonProps> = {
+  home: {
+    label: 'Home',
+    to: absoluteRoutes.viewer.index(),
+    colorVariant: 'yellow',
+    icon: <SvgNavHome />,
+  },
+  new: {
+    label: 'New & Noteworthy',
+    to: absoluteRoutes.viewer.new(),
+    colorVariant: 'green',
+    icon: <SvgNavNew />,
+  },
+  channels: {
+    label: 'Browse channels',
+    to: absoluteRoutes.viewer.channels(),
+    colorVariant: 'blue',
+    icon: <SvgNavChannels />,
+  },
+  popular: {
+    label: 'Popular on Joystream',
+    to: absoluteRoutes.viewer.popular(),
+    colorVariant: 'red',
+    icon: <SvgNavPopular />,
+  },
+}
+
+export const VideoContentTemplate: React.FC<VideoContentTemplateProps> = ({ children, title, cta }) => {
+  const ctaContent =
+    cta &&
+    cta.map((item, idx) => (
+      <CallToActionButton
+        key={`cta-${idx}`}
+        label={CTA_MAP[item].label}
+        to={CTA_MAP[item].to}
+        colorVariant={CTA_MAP[item].colorVariant}
+        icon={CTA_MAP[item].icon}
+      />
+    ))
+
+  return (
+    <StyledViewWrapper big>
+      {title && <Header variant="h3">{title}</Header>}
+      {children}
+      {cta && <CallToActionWrapper>{ctaContent}</CallToActionWrapper>}
+    </StyledViewWrapper>
+  )
+}
+
+const Header = styled(Text)`
+  margin: ${sizes(16)} 0;
+  font-size: ${typography.sizes.h3};
+
+  ${media.large} {
+    font-size: ${typography.sizes.h2};
+    line-height: ${typography.lineHeights.h2};
+  }
+`
+
+const StyledViewWrapper = styled(LimitedWidthContainer)`
+  padding-bottom: ${sizes(16)};
+
+  > section {
+    :not(:first-of-type) {
+      margin-top: ${sizes(32)};
+    }
+  }
+`

+ 30 - 0
src/config/availableNodes.ts

@@ -0,0 +1,30 @@
+export const availableNodes = [
+  {
+    name: 'Joystream (Europe/Germany - High Availabitliy)',
+    value: 'wss://rome-rpc-endpoint.joystream.org:9944',
+  },
+  {
+    name: 'Joystream (JoystreamStats.Live)',
+    value: 'wss://joystreamstats.live:9945',
+  },
+  {
+    name: 'Joystream (Europe/UK)',
+    value: 'wss://testnet-rpc-3-uk.joystream.org',
+  },
+  {
+    name: 'Joystream (US/East)',
+    value: 'wss://testnet-rpc-1-us.joystream.org',
+  },
+  {
+    name: 'Joystream (Singapore)',
+    value: 'wss://testnet-rpc-2-singapore.joystream.org',
+  },
+  {
+    name: 'Sumer Dev',
+    value: 'wss://sumer-dev-2.joystream.app/rpc',
+  },
+  {
+    name: 'Local node',
+    value: 'ws://127.0.0.1:9944',
+  },
+]

+ 3 - 8
src/config/envs.ts

@@ -1,16 +1,11 @@
+import { useEnvironmentStore } from '@/providers/environment/store'
+
 type BuildEnv = 'production' | 'development'
 
 export const BUILD_ENV = (process.env.REACT_APP_ENV || 'production') as BuildEnv
-const target_env = window.localStorage.getItem('target_env')
-export const TARGET_DEV_ENV = target_env ? JSON.parse(target_env) : 'development'
+export const TARGET_DEV_ENV = useEnvironmentStore.getState().targetEnv
 export const ENV_PREFIX = 'REACT_APP'
 
-export const setEnvInLocalStorage = (value: string) => {
-  if (availableEnvs().includes(value)) {
-    window.localStorage.setItem('target_env', JSON.stringify(value))
-  }
-}
-
 export const availableEnvs = () => {
   return Array.from(
     new Set(

+ 5 - 1
src/hooks/useDeleteVideo.tsx

@@ -1,6 +1,10 @@
 import { useApolloClient } from '@apollo/client'
 
-import { useAuthorizedUser, useDialog, useJoystream, useTransaction, useUploadsStore } from '@/providers'
+import { useDialog } from '@/providers/dialogs'
+import { useJoystream } from '@/providers/joystream'
+import { useTransaction } from '@/providers/transactionManager'
+import { useUploadsStore } from '@/providers/uploadsManager'
+import { useAuthorizedUser } from '@/providers/user'
 import { removeVideoFromCache } from '@/utils/cachingAssets'
 
 export const useDeleteVideo = () => {

+ 1 - 1
src/hooks/useHandleFollowChannel.tsx

@@ -1,5 +1,5 @@
 import { useFollowChannel, useUnfollowChannel } from '@/api/hooks'
-import { usePersonalDataStore } from '@/providers'
+import { usePersonalDataStore } from '@/providers/personalData'
 import { SentryLogger } from '@/utils/logs'
 
 export const useHandleFollowChannel = (id?: string) => {

+ 2 - 1
src/mocking/accessors/pagination.ts

@@ -1,4 +1,5 @@
-import { filterAndSortGenericData } from '.'
+import { filterAndSortGenericData } from './filtering'
+
 import { MockVideo } from '../data/mockVideos'
 import { CountData, FilteringArgs, GenericData } from '../types'
 

+ 21 - 18
src/providers/assets/assetsManager.tsx

@@ -2,13 +2,13 @@ import { shuffle } from 'lodash'
 import React, { useEffect } from 'react'
 
 import { ASSET_RESPONSE_TIMEOUT } from '@/config/assets'
-import { AssetType } from '@/providers'
 import { ResolvedAssetDetails } from '@/types/assets'
 import { AssetLogger, ConsoleLogger, SentryLogger } from '@/utils/logs'
 import { TimeoutError, withTimeout } from '@/utils/misc'
 
 import { getAssetUrl, testAssetDownload } from './helpers'
 import { useAssetStore } from './store'
+import { AssetType } from './types'
 
 import { useStorageProviders } from '../storageProviders'
 
@@ -42,6 +42,8 @@ export const AssetsManager: React.FC = () => {
         if (!assetUrl) {
           ConsoleLogger.warn('Unable to create asset url', resolutionData)
           addAsset(contentId, {})
+          removePendingAsset(contentId)
+          removeAssetBeingResolved(contentId)
           return
         }
 
@@ -55,24 +57,25 @@ export const AssetsManager: React.FC = () => {
           assetUrl,
         }
 
-        assetTestPromise.then((responseTime) => {
-          if (resolutionData.assetType === AssetType.MEDIA) {
-            // we're currently skipping monitoring video files as it's hard to measure their performance
-            // image assets are easy to measure but videos vary in length and size
-            // we will be able to handle that once we can access more detailed response timing
-            return
-          }
+        assetTestPromise
+          .then((responseTime) => {
+            if (resolutionData.assetType === AssetType.MEDIA) {
+              // we're currently skipping monitoring video files as it's hard to measure their performance
+              // image assets are easy to measure but videos vary in length and size
+              // we will be able to handle that once we can access more detailed response timing
+              return
+            }
 
-          // if response takes <20ms assume it's coming from cache
-          // we shouldn't need that once we can do detailed timing, then we can check directly
-          if (responseTime > 20) {
-            AssetLogger.assetResponseMetric(assetDetails, responseTime)
-          }
-        })
-        assetTestPromise.catch(() => {
-          AssetLogger.assetError(assetDetails)
-          ConsoleLogger.error('Failed to load asset', assetDetails)
-        })
+            // if response takes <20ms assume it's coming from cache
+            // we shouldn't need that once we can do detailed timing, then we can check directly
+            if (responseTime > 20) {
+              AssetLogger.assetResponseMetric(assetDetails, responseTime)
+            }
+          })
+          .catch(() => {
+            AssetLogger.assetError(assetDetails)
+            ConsoleLogger.error('Failed to load asset', assetDetails)
+          })
         const assetTestPromiseWithTimeout = withTimeout(assetTestPromise, ASSET_RESPONSE_TIMEOUT)
 
         try {

+ 1 - 5
src/providers/assets/useAsset.tsx

@@ -17,11 +17,7 @@ export const useAsset = ({ entity, assetType }: UseAssetDataArgs) => {
     addPendingAsset(contentId, assetData)
   }, [addPendingAsset, asset, assetData, contentId, pendingAsset])
 
-  if (asset) {
-    return { url: asset.url }
-  }
-
-  return { url: null }
+  return { url: asset?.url, isLoadingAsset: !assetData || (!!contentId && !asset) }
 }
 
 export const useRawAsset = (contentId: string | null) => {

+ 2 - 1
src/providers/editVideoSheet/hooks.tsx

@@ -12,7 +12,8 @@ import { SentryLogger } from '@/utils/logs'
 import { EditVideoSheetContext } from './provider'
 import { EditVideoAssets, EditVideoFormFields, EditVideoSheetState, EditVideoSheetTab } from './types'
 
-import { channelDraftsSelector, useAuthorizedUser, useDraftStore } from '..'
+import { channelDraftsSelector, useDraftStore } from '../drafts'
+import { useAuthorizedUser } from '../user'
 
 export const useEditVideoSheet = () => {
   const ctx = useContext(EditVideoSheetContext)

+ 1 - 1
src/providers/editVideoSheet/provider.tsx

@@ -12,7 +12,7 @@ import {
   EditVideoTabCachedDirtyFormData,
 } from './types'
 
-import { useOverlayManager } from '..'
+import { useOverlayManager } from '../overlayManager'
 
 export const EditVideoSheetContext = React.createContext<ContextValue | undefined>(undefined)
 EditVideoSheetContext.displayName = 'EditVideoSheetContext'

+ 1 - 1
src/providers/editVideoSheet/types.ts

@@ -1,4 +1,4 @@
-import { ImageInputMetadata, VideoInputMetadata } from '@/shared/components'
+import { ImageInputMetadata, VideoInputMetadata } from '@/shared/components/MultiFileSelect'
 
 export type EditVideoAssets = {
   video: {

+ 1 - 0
src/providers/environment/index.ts

@@ -0,0 +1 @@
+export type { EnvironmentState } from './store'

+ 44 - 0
src/providers/environment/store.ts

@@ -0,0 +1,44 @@
+import { createStore } from '@/store'
+
+const LOCAL_STORAGE_KEY = 'environment'
+
+export type EnvironmentState = {
+  selectedNode: string | null
+  targetEnv: string
+}
+
+const INITIAL_STATE: EnvironmentState = {
+  targetEnv: 'development',
+  selectedNode: null,
+}
+
+export type EnvironmentStoreActions = {
+  setSelectedNode: (node: string) => void
+  setTargetEnv: (env: string) => void
+}
+
+export const useEnvironmentStore = createStore<EnvironmentState, EnvironmentStoreActions>(
+  {
+    state: INITIAL_STATE,
+    actionsFactory: (set) => ({
+      setSelectedNode: (node) => {
+        set((state) => {
+          state.selectedNode = node || state.selectedNode
+        })
+      },
+      setTargetEnv: (env) => {
+        set((state) => {
+          state.targetEnv = env || state.targetEnv
+        })
+      },
+    }),
+  },
+  {
+    persist: {
+      key: LOCAL_STORAGE_KEY,
+      version: 0,
+      whitelist: ['selectedNode', 'targetEnv'],
+      migrate: () => null,
+    },
+  }
+)

+ 0 - 13
src/providers/index.ts

@@ -1,13 +0,0 @@
-export * from './personalData'
-export * from './snackbars'
-export * from './overlayManager'
-export * from './drafts'
-export * from './user'
-export * from './joystream'
-export * from './connectionStatus'
-export * from './editVideoSheet'
-export * from './uploadsManager'
-export * from './transactionManager'
-export * from './dialogs'
-export * from './storageProviders'
-export * from './assets'

+ 6 - 3
src/providers/joystream/provider.tsx

@@ -3,9 +3,11 @@ import React, { useCallback, useEffect, useState } from 'react'
 
 import { NODE_URL } from '@/config/urls'
 import { JoystreamJs } from '@/joystream-lib'
+import { useEnvironmentStore } from '@/providers/environment/store'
 import { SentryLogger } from '@/utils/logs'
 
-import { useConnectionStatusStore, useUser } from '..'
+import { useConnectionStatusStore } from '../connectionStatus'
+import { useUser } from '../user'
 
 type JoystreamContextValue = {
   joystream: JoystreamJs | null
@@ -15,6 +17,7 @@ JoystreamContext.displayName = 'JoystreamContext'
 
 export const JoystreamProvider: React.FC = ({ children }) => {
   const { activeAccountId, accounts } = useUser()
+  const { selectedNode } = useEnvironmentStore((state) => state)
   const setNodeConnection = useConnectionStatusStore((state) => state.actions.setNodeConnection)
 
   const [joystream, setJoystream] = useState<JoystreamJs | null>(null)
@@ -32,7 +35,7 @@ export const JoystreamProvider: React.FC = ({ children }) => {
     const init = async () => {
       try {
         setNodeConnection('connecting')
-        joystream = new JoystreamJs(NODE_URL)
+        joystream = new JoystreamJs(selectedNode || NODE_URL)
         setJoystream(joystream)
 
         joystream.onNodeConnectionUpdate = handleNodeConnectionUpdate
@@ -47,7 +50,7 @@ export const JoystreamProvider: React.FC = ({ children }) => {
     return () => {
       joystream?.destroy()
     }
-  }, [handleNodeConnectionUpdate, setNodeConnection])
+  }, [handleNodeConnectionUpdate, selectedNode, setNodeConnection])
 
   useEffect(() => {
     if (!joystream || !activeAccountId || !accounts) {

+ 3 - 2
src/providers/snackbars/snackbar.tsx

@@ -2,9 +2,9 @@ import styled from '@emotion/styled'
 import React, { ReactNode, useEffect } from 'react'
 import { CSSTransition, TransitionGroup } from 'react-transition-group'
 
-import { Snackbar } from '@/shared/components'
+import { Snackbar } from '@/shared/components/Snackbar'
 import { SvgAlertError, SvgAlertInfo, SvgAlertSuccess, SvgAlertWarning } from '@/shared/icons'
-import { sizes, transitions } from '@/shared/theme'
+import { sizes, transitions, zIndex } from '@/shared/theme'
 
 import { useSnackbarStore } from './store'
 
@@ -65,4 +65,5 @@ const SnackbarsContainer = styled.div`
   max-width: 360px;
   width: 100%;
   display: grid;
+  z-index: ${zIndex.nearSheetOverlay};
 `

+ 1 - 1
src/providers/storageProviders.tsx

@@ -9,7 +9,7 @@ import {
   GetWorkersQuery,
   GetWorkersQueryVariables,
 } from '@/api/queries/__generated__/workers.generated'
-import { ViewErrorFallback } from '@/components'
+import { ViewErrorFallback } from '@/components/ViewErrorFallback'
 import { SentryLogger } from '@/utils/logs'
 import { getRandomIntInclusive } from '@/utils/number'
 

+ 1 - 1
src/providers/transactionManager/transactionManager.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 
 import { useQueryNodeStateSubscription } from '@/api/hooks'
-import { TransactionDialog } from '@/components'
+import { TransactionDialog } from '@/components/Dialogs'
 import { SentryLogger } from '@/utils/logs'
 
 import { useTransactionManagerStore } from './store'

+ 5 - 2
src/providers/transactionManager/useTransaction.ts

@@ -1,8 +1,11 @@
 import { ExtrinsicFailedError, ExtrinsicResult, ExtrinsicSignCancelledError, ExtrinsicStatus } from '@/joystream-lib'
-import { TransactionDialogStep, useConnectionStatusStore, useDialog, useSnackbar } from '@/providers'
 import { ConsoleLogger, SentryLogger } from '@/utils/logs'
 
-import { useTransactionManagerStore } from './store'
+import { TransactionDialogStep, useTransactionManagerStore } from './store'
+
+import { useConnectionStatusStore } from '../connectionStatus'
+import { useDialog } from '../dialogs'
+import { useSnackbar } from '../snackbars'
 
 type UpdateStatusFn = (status: TransactionDialogStep) => void
 type SuccessMessage = {

+ 2 - 1
src/providers/uploadsManager/uploadsManager.tsx

@@ -8,7 +8,8 @@ import { fetchMissingAssets } from '@/providers/uploadsManager/utils'
 
 import { useUploadsStore } from './store'
 
-import { useSnackbar, useUser } from '..'
+import { useSnackbar } from '../snackbars'
+import { useUser } from '../user'
 
 export const UploadsManager: React.FC = () => {
   const navigate = useNavigate()

+ 17 - 1
src/providers/uploadsManager/useStartFileUpload.tsx

@@ -5,8 +5,9 @@ import { useNavigate } from 'react-router'
 import * as rax from 'retry-axios'
 
 import { absoluteRoutes } from '@/config/routes'
+import { ResolvedAssetDetails } from '@/types/assets'
 import { createStorageNodeUrl } from '@/utils/asset'
-import { ConsoleLogger, SentryLogger } from '@/utils/logs'
+import { AssetLogger, ConsoleLogger, SentryLogger } from '@/utils/logs'
 
 import { useUploadsStore } from './store'
 import { InputAssetUpload, StartFileUploadOptions, UploadStatus } from './types'
@@ -111,6 +112,14 @@ export const useStartFileUpload = () => {
       const assetKey = `${asset.parentObject.type}-${asset.parentObject.id}`
       const assetUrl = createStorageNodeUrl(asset.contentId, storageUrl)
 
+      const assetDetails: ResolvedAssetDetails = {
+        contentId: asset.contentId,
+        assetType: asset.type,
+        assetUrl,
+        storageProviderId,
+        storageProviderUrl: storageUrl,
+      }
+
       try {
         if (!fileInState && !file) {
           throw Error('File was not provided nor found')
@@ -166,10 +175,17 @@ export const useStartFileUpload = () => {
         assetsNotificationsCount.current.uploaded[assetKey] =
           (assetsNotificationsCount.current.uploaded[assetKey] || 0) + 1
         displayUploadedNotification.current(assetKey)
+
+        const performanceEntries = performance.getEntriesByName(assetUrl)
+        if (performanceEntries.length === 1) {
+          AssetLogger.uploadRequestMetric(assetDetails, performanceEntries[0].duration, file?.size || 0)
+        }
       } catch (e) {
         SentryLogger.error('Failed to upload asset', 'UploadsManager', e, {
           asset: { contentId: asset.contentId, storageProviderId, storageProviderUrl: storageUrl, assetUrl },
         })
+        AssetLogger.uploadError(assetDetails)
+
         setAssetStatus({ lastStatus: 'error', progress: 0 })
 
         const axiosError = e as AxiosError

+ 7 - 2
src/providers/user/user.tsx

@@ -3,10 +3,10 @@ import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'
 import React, { useContext, useEffect, useState } from 'react'
 
 import { useMembership, useMemberships } from '@/api/hooks'
-import { ViewErrorFallback } from '@/components'
+import { ViewErrorFallback } from '@/components/ViewErrorFallback'
 import { WEB3_APP_NAME } from '@/config/urls'
 import { AccountId } from '@/joystream-lib'
-import { ConsoleLogger, SentryLogger } from '@/utils/logs'
+import { AssetLogger, ConsoleLogger, SentryLogger } from '@/utils/logs'
 
 import { ActiveUserState, ActiveUserStoreActions, useActiveUserStore } from './store'
 
@@ -38,6 +38,11 @@ export const ActiveUserProvider: React.FC = ({ children }) => {
   const activeUserState = useActiveUserStore(({ actions, ...activeUser }) => ({ ...activeUser }))
   const { setActiveUser, resetActiveUser } = useActiveUserStore((state) => state.actions)
 
+  useEffect(() => {
+    SentryLogger.setUser(activeUserState)
+    AssetLogger.setUser(activeUserState)
+  }, [activeUserState])
+
   const [accounts, setAccounts] = useState<Account[] | null>(null)
   const [extensionConnected, setExtensionConnected] = useState<boolean | null>(null)
 

+ 3 - 4
src/shared/components/Avatar/Avatar.tsx

@@ -1,5 +1,5 @@
 import React from 'react'
-import { CSSTransition } from 'react-transition-group'
+import { CSSTransition, SwitchTransition } from 'react-transition-group'
 
 import { SvgGlyphImage, SvgGlyphNewChannel, SvgLargeUploadFailed } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
@@ -12,7 +12,6 @@ import {
   SilhouetteAvatar,
   StyledImage,
   StyledSkeletonLoader,
-  StyledTransitionGroup,
 } from './Avatar.style'
 
 export type AvatarProps = {
@@ -60,7 +59,7 @@ export const Avatar: React.FC<AvatarProps> = ({
           <SvgGlyphNewChannel />
         </NewChannelAvatar>
       ) : (
-        <StyledTransitionGroup>
+        <SwitchTransition>
           <CSSTransition
             key={loading ? 'placeholder' : 'content'}
             timeout={parseInt(transitions.timings.loading)}
@@ -78,7 +77,7 @@ export const Avatar: React.FC<AvatarProps> = ({
               <SilhouetteAvatar />
             )}
           </CSSTransition>
-        </StyledTransitionGroup>
+        </SwitchTransition>
       )}
     </Container>
   )

+ 3 - 1
src/shared/components/Banner/Banner.style.ts

@@ -1,10 +1,12 @@
 import styled from '@emotion/styled'
 
-import { Button, Text } from '@/shared/components'
 import { colors, sizes } from '@/shared/theme'
 
 import { BannerVariant } from './Banner'
 
+import { Button } from '../Button'
+import { Text } from '../Text'
+
 type BannerProps = {
   variant: BannerVariant
 }

+ 1 - 1
src/shared/components/Banner/Banner.tsx

@@ -1,6 +1,6 @@
 import React, { ReactNode } from 'react'
 
-import { IconButton } from '@/shared/components'
+import { IconButton } from '@/shared/components/IconButton'
 import { SvgAlertError, SvgAlertInfo, SvgAlertSuccess, SvgAlertWarning, SvgGlyphClose } from '@/shared/icons'
 
 import {

+ 6 - 1
src/shared/components/CallToActionButton/CallToActionButton.style.ts

@@ -1,7 +1,7 @@
 import isPropValid from '@emotion/is-prop-valid'
 import styled from '@emotion/styled'
 
-import { colors, media, sizes, transitions } from '@/shared/theme'
+import { colors, media, sizes, transitions, typography } from '@/shared/theme'
 
 import { ColorVariants } from './CallToActionButton'
 
@@ -41,6 +41,11 @@ export const BodyWrapper = styled(Text)`
   display: flex;
   align-items: center;
   justify-content: space-between;
+
+  ${media.compact} {
+    font-size: ${typography.sizes.h5};
+    line-height: ${typography.lineHeights.h5};
+  }
 `
 
 export const ContentWrapper = styled.div`

+ 1 - 1
src/shared/components/CallToActionButton/CallToActionButton.tsx

@@ -29,7 +29,7 @@ export const CallToActionButton: FC<CallToActionButtonProps> = ({
     <StyledContainer {...linkProps} onClick={onClick} colorVariant={colorVariant}>
       <ContentWrapper>
         <IconWrapper colorVariant={colorVariant === 'blue' ? 'lightBlue' : colorVariant}>{icon}</IconWrapper>
-        <BodyWrapper variant="h5">
+        <BodyWrapper variant="h6">
           {label}
           <SvgGlyphChevronRight />
         </BodyWrapper>

+ 7 - 3
src/shared/components/Carousel/Carousel.style.ts

@@ -44,6 +44,9 @@ export const Arrow = styled(IconButton)`
 export const GliderContainer = styled.div`
   padding-left: ${sizes(2)};
   padding-top: ${sizes(2)};
+
+  /* hides scrollbar on firefox */
+  scrollbar-width: none;
 `
 
 export const Track = styled.div`
@@ -56,7 +59,7 @@ export const Track = styled.div`
 `
 
 export const Dots = styled.div`
-  padding: ${sizes(5.5)} 0;
+  padding: ${sizes(3.5)} 0;
   margin-top: ${sizes(12)};
   display: none;
 
@@ -65,10 +68,11 @@ export const Dots = styled.div`
   }
 
   .glider-dot {
+    width: 36px;
+    height: 20px;
     background-color: transparent;
-    width: ${sizes(10)};
     border-radius: 0;
-    padding: ${sizes(1)};
+    padding: ${sizes(2)} ${sizes(0.5)};
     margin: 0;
 
     &::after {

+ 32 - 22
src/shared/components/ChannelCardBase/ChannelCardBase.tsx

@@ -1,6 +1,8 @@
 import React from 'react'
+import { CSSTransition, SwitchTransition } from 'react-transition-group'
 
 import { absoluteRoutes } from '@/config/routes'
+import { transitions } from '@/shared/theme'
 import { formatNumberShort } from '@/utils/number'
 
 import {
@@ -21,6 +23,7 @@ export type ChannelCardBaseProps = {
   title?: string | null
   follows?: number | null
   avatarUrl?: string | null
+  isLoadingAvatar?: boolean
   isFollowing?: boolean
   onFollow?: (event: React.MouseEvent) => void
   className?: string
@@ -33,6 +36,7 @@ export const ChannelCardBase: React.FC<ChannelCardBaseProps> = ({
   title,
   follows,
   avatarUrl,
+  isLoadingAvatar,
   isFollowing,
   onFollow,
   className,
@@ -41,28 +45,34 @@ export const ChannelCardBase: React.FC<ChannelCardBaseProps> = ({
   return (
     <ChannelCardArticle className={className}>
       <ChannelCardAnchor onClick={onClick} to={absoluteRoutes.viewer.channel(id || '')}>
-        <StyledAvatar size="channel-card" loading={isLoading} assetUrl={avatarUrl} />
-        <InfoWrapper>
-          {isLoading ? (
-            <SkeletonLoader width="100px" height="20px" bottomSpace="4px" />
-          ) : (
-            <ChannelTitle variant="h6">{title}</ChannelTitle>
-          )}
-          {isLoading ? (
-            <SkeletonLoader width="70px" height="20px" bottomSpace="16px" />
-          ) : (
-            <ChannelFollows variant="body2" secondary>
-              {formatNumberShort(follows || 0)} followers
-            </ChannelFollows>
-          )}
-          {isLoading ? (
-            <SkeletonLoader width="60px" height="30px" />
-          ) : (
-            <FollowButton variant="secondary" size="small" onClick={onFollow}>
-              {isFollowing ? 'Unfollow' : 'Follow'}
-            </FollowButton>
-          )}
-        </InfoWrapper>
+        <StyledAvatar size="channel-card" loading={isLoadingAvatar} assetUrl={avatarUrl} />
+        <SwitchTransition>
+          <CSSTransition
+            key={isLoading ? 'placeholder' : 'content'}
+            timeout={parseInt(transitions.timings.sharp)}
+            classNames={transitions.names.fade}
+          >
+            <InfoWrapper>
+              {isLoading ? (
+                <>
+                  <SkeletonLoader width="100px" height="20px" bottomSpace="4px" />
+                  <SkeletonLoader width="70px" height="20px" bottomSpace="16px" />
+                  <SkeletonLoader width="60px" height="30px" />
+                </>
+              ) : (
+                <>
+                  <ChannelTitle variant="h6">{title}</ChannelTitle>
+                  <ChannelFollows variant="body2" secondary>
+                    {formatNumberShort(follows || 0)} followers
+                  </ChannelFollows>
+                  <FollowButton variant="secondary" size="small" onClick={onFollow}>
+                    {isFollowing ? 'Unfollow' : 'Follow'}
+                  </FollowButton>
+                </>
+              )}
+            </InfoWrapper>
+          </CSSTransition>
+        </SwitchTransition>
       </ChannelCardAnchor>
     </ChannelCardArticle>
   )

+ 1 - 1
src/shared/components/ChannelCover/ChannelCover.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 import { CSSTransition, TransitionGroup } from 'react-transition-group'
 
-import { BackgroundPattern } from '@/components'
+import { BackgroundPattern } from '@/components/BackgroundPattern'
 import { SvgGlyphFileImage, SvgGlyphImage, SvgLargeUploadFailed } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 

+ 4 - 4
src/shared/components/CircularProgress/CircularProgress.style.tsx

@@ -2,8 +2,6 @@ import styled from '@emotion/styled'
 
 import { colors } from '@/shared/theme'
 
-import { Path } from './Path'
-
 export type TrailVariant = 'default' | 'player'
 
 type TrailProps = {
@@ -22,13 +20,15 @@ const getStrokeColor = (variant?: TrailVariant) => {
 }
 
 export const SVG = styled.svg`
+  fill: none;
+
   /* needed when parent container has display: flex */
   width: 100%;
 `
-export const Trail = styled(Path)<TrailProps>`
+export const Trail = styled.path<TrailProps>`
   stroke: ${({ variant }) => getStrokeColor(variant)};
 `
-export const StyledPath = styled(Path)`
+export const StyledPath = styled.path`
   stroke: ${colors.blue[500]};
   transition: stroke-dashoffset 0.5s ease 0s;
 `

+ 34 - 6
src/shared/components/CircularProgress/CircularProgress.tsx

@@ -52,20 +52,48 @@ export const CircularProgress: React.FC<CircularProgressProps> = ({
         {background ? <Background cx={VIEWBOX_CENTER_X} cy={VIEWBOX_CENTER_Y} r={VIEWBOX_HEIGHT_HALF} /> : null}
         {!noTrail && (
           <Trail
-            counterClockwise={counterClockwise}
-            dashRatio={circleRatio}
-            pathRadius={pathRadius}
+            style={Object.assign({}, getDashStyle(counterClockwise, circleRatio, pathRadius))}
+            d={getPathDescription(pathRadius, counterClockwise)}
             variant={variant}
             strokeWidth={strokeWidth}
           />
         )}
         <StyledPath
-          counterClockwise={counterClockwise}
-          dashRatio={pathRatio * circleRatio}
-          pathRadius={pathRadius}
           strokeWidth={strokeWidth}
+          className={className}
+          style={Object.assign({}, getDashStyle(counterClockwise, pathRatio * circleRatio, pathRadius))}
+          d={getPathDescription(pathRadius, counterClockwise)}
+          fillOpacity={0}
         />
       </SVG>
     </>
   )
 }
+
+// SVG path description specifies how the path should be drawn
+const getPathDescription = (pathRadius: number, counterClockwise: boolean) => {
+  const radius = pathRadius
+  const rotation = counterClockwise ? 1 : 0
+
+  // Move to center of canvas
+  // Relative move to top canvas
+  // Relative arc to bottom of canvas
+  // Relative arc to top of canvas
+  return `
+      M ${VIEWBOX_CENTER_X},${VIEWBOX_CENTER_Y}
+      m 0,-${radius}
+      a ${radius},${radius} ${rotation} 1 1 0,${2 * radius}
+      a ${radius},${radius} ${rotation} 1 1 0,-${2 * radius}
+    `
+}
+
+const getDashStyle = (counterClockwise: boolean, dashRatio: number, pathRadius: number) => {
+  const diameter = Math.PI * 2 * pathRadius
+  const gapLength = (1 - dashRatio) * diameter
+  return {
+    // Have dash be full diameter, and gap be full diameter
+    strokeDasharray: `${diameter}px ${diameter}px`,
+    // Shift dash backward by gapLength, so gap starts appearing at correct distance
+    strokeDashoffset: `${counterClockwise ? -gapLength : gapLength}px`,
+  }
+}

Vissa filer visades inte eftersom för många filer har ändrats