Explorar o código

uploads improvements (#766)

* improve uploads and storage providers handling

* fix video sorting

* improve assets data flow

* improve fetching data about uploads

* introduce RETRY_DELAY const

* handle errors in transaction callbacks

* improve error on transaction

* ignore completed assets

* further improvements to error handling
Klaudiusz Dembler %!s(int64=3) %!d(string=hai) anos
pai
achega
5d2c34e605

+ 10 - 9
.env

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

+ 4 - 2
src/App.tsx

@@ -2,7 +2,7 @@ import { ApolloProvider } from '@apollo/client'
 import React from 'react'
 
 import { createApolloClient } from '@/api'
-import { ConnectionStatusProvider, OverlayManagerProvider, SnackbarProvider } from '@/hooks'
+import { ConnectionStatusProvider, OverlayManagerProvider, SnackbarProvider, StorageProvidersProvider } from '@/hooks'
 
 import MainLayout from './MainLayout'
 
@@ -16,7 +16,9 @@ export default function App() {
       <SnackbarProvider>
         <ConnectionStatusProvider>
           <OverlayManagerProvider>
-            <MainLayout />
+            <StorageProvidersProvider>
+              <MainLayout />
+            </StorageProvidersProvider>
           </OverlayManagerProvider>
         </ConnectionStatusProvider>
       </SnackbarProvider>

+ 3 - 3
src/api/client/cache.ts

@@ -97,13 +97,13 @@ const queryCacheFields: CachePolicyFields<keyof Query> = {
         existing?.edges.filter((edge) => readField('isPublic', edge.node) === isPublic || isPublic === undefined) ?? []
 
       const sortingASC = args?.orderBy === VideoOrderByInput.CreatedAtAsc
-      const preSortedASC = filteredEdges
-        ?.slice()
+      const preSortedDESC = (filteredEdges || [])
+        .slice()
         .sort(
           (a, b) =>
             (readField('createdAt', b.node) as Date).getTime() - (readField('createdAt', a.node) as Date).getTime()
         )
-      const sortedEdges = sortingASC ? preSortedASC : preSortedASC.reverse()
+      const sortedEdges = sortingASC ? preSortedDESC.reverse() : preSortedDESC
 
       return (
         existing && {

+ 1 - 19
src/api/hooks/workers.ts

@@ -1,5 +1,4 @@
 import { QueryHookOptions } from '@apollo/client'
-import { useCallback } from 'react'
 
 import { WorkerType } from '@/api/queries'
 import {
@@ -9,7 +8,6 @@ import {
   useGetWorkerQuery,
   useGetWorkersQuery,
 } from '@/api/queries/__generated__/workers.generated'
-import { getRandomIntInclusive } from '@/utils/number'
 
 type WorkerOpts = QueryHookOptions<GetWorkerQuery>
 export const useWorker = (id: string, opts?: WorkerOpts) => {
@@ -24,7 +22,7 @@ export const useWorker = (id: string, opts?: WorkerOpts) => {
 }
 
 type WorkersOpts = QueryHookOptions<GetWorkersQuery>
-export const useStorageProviders = (variables: GetWorkersQueryVariables, opts?: WorkersOpts) => {
+export const useStorageWorkers = (variables: GetWorkersQueryVariables, opts?: WorkersOpts) => {
   const { data, loading, ...rest } = useGetWorkersQuery({
     ...opts,
     variables: {
@@ -43,19 +41,3 @@ export const useStorageProviders = (variables: GetWorkersQueryVariables, opts?:
     ...rest,
   }
 }
-
-export const useRandomStorageProviderUrl = () => {
-  const { storageProviders, loading } = useStorageProviders({ limit: 100 }, { fetchPolicy: 'network-only' })
-
-  const getRandomStorageProviderUrl = useCallback(() => {
-    if (storageProviders?.length && !loading) {
-      const randomStorageIdx = getRandomIntInclusive(0, storageProviders.length - 1)
-      return storageProviders[randomStorageIdx].metadata
-    } else if (!loading) {
-      console.error('No active storage provider available')
-    }
-    return null
-  }, [loading, storageProviders])
-
-  return { getRandomStorageProviderUrl }
-}

+ 1 - 1
src/components/Sidenav/StudioSidenav/StudioSidenav.tsx

@@ -34,7 +34,7 @@ export const StudioSidenav: React.FC = () => {
   const [expanded, setExpanded] = useState(false)
   const { activeChannelId } = useAuthorizedUser()
   const { unseenDrafts } = useDrafts('video', activeChannelId)
-  const { uploadsState } = useUploadsManager(activeChannelId)
+  const { uploadsState } = useUploadsManager()
   const navigate = useNavigate()
   const { sheetState } = useEditVideoSheet()
 

+ 1 - 0
src/hooks/index.ts

@@ -15,3 +15,4 @@ export * from './useDisplayDataLostWarning'
 export * from './useTransactionManager'
 export * from './useAsset'
 export * from './useDialog'
+export * from './useStorageProviders'

+ 6 - 6
src/hooks/useAsset.tsx

@@ -1,11 +1,11 @@
 import { useCallback } from 'react'
 
-import { useRandomStorageProviderUrl } from '@/api/hooks'
 import { AssetAvailability, DataObject } from '@/api/queries'
+import { useStorageProviders } from '@/hooks'
 import { createStorageNodeUrl } from '@/utils/asset'
 
 export const useAsset = () => {
-  const { getRandomStorageProviderUrl } = useRandomStorageProviderUrl()
+  const { getStorageProvider } = useStorageProviders()
 
   const getAssetUrl = useCallback(
     (availability?: AssetAvailability, assetUrls?: string[], dataObject?: DataObject | null) => {
@@ -22,12 +22,12 @@ export const useAsset = () => {
         return createStorageNodeUrl(dataObject.joystreamContentId, dataObject?.liaison?.metadata)
       }
 
-      const randomStorageUrl = getRandomStorageProviderUrl()
-      if (randomStorageUrl) {
-        return createStorageNodeUrl(dataObject.joystreamContentId, randomStorageUrl)
+      const storageProvider = getStorageProvider()
+      if (storageProvider?.url) {
+        return createStorageNodeUrl(dataObject.joystreamContentId, storageProvider.url)
       }
     },
-    [getRandomStorageProviderUrl]
+    [getStorageProvider]
   )
 
   return { getAssetUrl }

+ 1 - 1
src/hooks/useConnectionStatus.tsx

@@ -28,7 +28,7 @@ export const ConnectionStatusProvider: React.FC = ({ children }) => {
   const checkConnection = useCallback(async () => {
     try {
       const res = await withTimeout(
-        fetch('https://google.com', {
+        fetch('https://www.google.com', {
           method: 'HEAD',
           mode: 'no-cors',
         }),

+ 1 - 0
src/hooks/useStorageProviders/index.ts

@@ -0,0 +1 @@
+export * from './useStorageProviders'

+ 105 - 0
src/hooks/useStorageProviders/useStorageProviders.tsx

@@ -0,0 +1,105 @@
+import React, { SetStateAction, useCallback, useContext, useState } from 'react'
+
+import { useStorageWorkers as useStorageProvidersData } from '@/api/hooks'
+import { BasicWorkerFieldsFragment } from '@/api/queries/__generated__/workers.generated'
+import { getRandomIntInclusive } from '@/utils/number'
+
+type StorageProvidersContextValue = {
+  storageProviders: BasicWorkerFieldsFragment[]
+  storageProvidersLoading: boolean
+  notWorkingStorageProvidersIds: string[]
+  setNotWorkingStorageProvidersIds: React.Dispatch<SetStateAction<string[]>>
+}
+const StorageProvidersContext = React.createContext<StorageProvidersContextValue | undefined>(undefined)
+StorageProvidersContext.displayName = 'StorageProvidersContext'
+
+class NoStorageProviderError extends Error {
+  storageProviders: string[]
+  notWorkingStorageProviders: string[]
+
+  constructor(message: string, storageProviders: string[], notWorkingStorageProviders: string[]) {
+    super(message)
+
+    this.storageProviders = storageProviders
+    this.notWorkingStorageProviders = notWorkingStorageProviders
+  }
+}
+
+// ¯\_(ツ)_/¯ for the name
+export const StorageProvidersProvider: React.FC = ({ children }) => {
+  const [notWorkingStorageProvidersIds, setNotWorkingStorageProvidersIds] = useState<string[]>([])
+
+  const { storageProviders, loading } = useStorageProvidersData(
+    { limit: 100 },
+    {
+      fetchPolicy: 'network-only',
+      onError: (error) => console.error('Failed to fetch storage providers list', error),
+    }
+  )
+
+  return (
+    <StorageProvidersContext.Provider
+      value={{
+        storageProvidersLoading: loading,
+        storageProviders: storageProviders || [],
+        notWorkingStorageProvidersIds,
+        setNotWorkingStorageProvidersIds,
+      }}
+    >
+      {children}
+    </StorageProvidersContext.Provider>
+  )
+}
+
+export const useStorageProviders = () => {
+  const ctx = useContext(StorageProvidersContext)
+
+  if (!ctx) {
+    throw new Error('useStorageProviders must be used within StorageProvidersProvider')
+  }
+
+  const {
+    storageProvidersLoading,
+    storageProviders,
+    notWorkingStorageProvidersIds,
+    setNotWorkingStorageProvidersIds,
+  } = ctx
+
+  const getStorageProvider = useCallback(() => {
+    // make sure we finished fetching providers list
+    if (storageProvidersLoading) {
+      // TODO: we need to handle that somehow, possibly make it async and block until ready
+      console.error('Trying to use storage providers while still loading')
+      return null
+    }
+
+    const workingStorageProviders = storageProviders.filter(
+      ({ workerId }) => !notWorkingStorageProvidersIds.includes(workerId)
+    )
+
+    if (!workingStorageProviders.length) {
+      throw new NoStorageProviderError(
+        'No storage provider available',
+        storageProviders.map(({ workerId }) => workerId),
+        notWorkingStorageProvidersIds
+      )
+    }
+
+    const randomStorageProviderIdx = getRandomIntInclusive(0, workingStorageProviders.length - 1)
+    const randomStorageProvider = workingStorageProviders[randomStorageProviderIdx]
+
+    return {
+      id: randomStorageProvider.workerId,
+      url: randomStorageProvider.metadata as string,
+    }
+  }, [notWorkingStorageProvidersIds, storageProvidersLoading, storageProviders])
+
+  const markStorageProviderNotWorking = useCallback(
+    (workerId: string) => {
+      setNotWorkingStorageProvidersIds((state) => [...state, workerId])
+    },
+    [setNotWorkingStorageProvidersIds]
+  )
+
+  return { getStorageProvider, markStorageProviderNotWorking }
+}

+ 31 - 14
src/hooks/useTransactionManager.tsx

@@ -1,7 +1,7 @@
 import React, { useCallback, useContext, useEffect, useState } from 'react'
 
 import { useQueryNodeStateSubscription } from '@/api/hooks'
-import { ActionDialog, TransactionDialog } from '@/components'
+import { TransactionDialog } from '@/components'
 import { useSnackbar } from '@/hooks/useSnackbar'
 import { ExtrinsicResult, ExtrinsicSignCancelledError, ExtrinsicStatus } from '@/joystream-lib'
 
@@ -15,10 +15,10 @@ type SuccessMessage = {
 }
 type HandleTransactionOpts<T> = {
   txFactory: (updateStatus: UpdateStatusFn) => Promise<ExtrinsicResult<T>>
-  preProcess?: () => Promise<void>
-  onTxFinalize?: (data: T) => void
-  onTxSync?: (data: T) => void
-  onTxClose?: (completed: boolean) => void
+  preProcess?: () => void | Promise<void>
+  onTxFinalize?: (data: T) => void | Promise<unknown>
+  onTxSync?: (data: T) => void | Promise<unknown>
+  onTxClose?: (completed: boolean) => void | Promise<unknown>
   successMessage: SuccessMessage
 }
 
@@ -35,8 +35,8 @@ TransactionManagerContext.displayName = 'TransactionManagerContext'
 export const TransactionManagerProvider: React.FC = ({ children }) => {
   const [status, setStatus] = useState<ExtrinsicStatus | null>(null)
   const [finalizationBlock, setFinalizationBlock] = useState<number | null>(null)
-  const [syncCallback, setSyncCallback] = useState<(() => void) | null>(null)
-  const [dialogCloseCallback, setDialogCloseCallback] = useState<(() => void) | null>(null)
+  const [syncCallback, setSyncCallback] = useState<(() => void | Promise<unknown>) | null>(null)
+  const [dialogCloseCallback, setDialogCloseCallback] = useState<(() => void | Promise<unknown>) | null>(null)
   const [successMessage, setSuccessMessage] = useState<SuccessMessage>({ title: '', description: '' })
 
   // Keep persistent subscription to the query node. If this proves problematic for some reason we can skip until in Syncing
@@ -75,8 +75,14 @@ export const TransactionManagerProvider: React.FC = ({ children }) => {
 
   const { displaySnackbar } = useSnackbar()
 
-  const handleDialogClose = useCallback(() => {
-    dialogCloseCallback?.()
+  const handleDialogClose = useCallback(async () => {
+    try {
+      if (dialogCloseCallback) {
+        await dialogCloseCallback()
+      }
+    } catch (e) {
+      console.error('Transaction dialog close callback failed', e)
+    }
     reset()
   }, [dialogCloseCallback])
 
@@ -86,7 +92,16 @@ export const TransactionManagerProvider: React.FC = ({ children }) => {
     }
     if (queryNodeState.indexerHead >= finalizationBlock) {
       setStatus(ExtrinsicStatus.Completed)
-      syncCallback?.()
+      const runCallback = async () => {
+        try {
+          if (syncCallback) {
+            await syncCallback()
+          }
+        } catch (e) {
+          console.error('Transaction sync callback failed', e)
+        }
+      }
+      runCallback()
 
       openCompletedDialog()
     }
@@ -114,7 +129,7 @@ export const TransactionManagerProvider: React.FC = ({ children }) => {
       }
       setSuccessMessage(successMessage)
       // set up fallback dialog close callback
-      setDialogCloseCallback(() => () => onTxClose?.(false))
+      setDialogCloseCallback(() => async () => await onTxClose?.(false))
 
       // if provided, do any preprocessing
       if (preProcess) {
@@ -129,13 +144,15 @@ export const TransactionManagerProvider: React.FC = ({ children }) => {
       // set up query node sync
       setStatus(ExtrinsicStatus.Syncing)
       setFinalizationBlock(block)
-      setSyncCallback(() => () => onTxSync?.(data))
+      setSyncCallback(() => async () => await onTxSync?.(data))
 
       // set up dialog close callback
-      setDialogCloseCallback(() => () => onTxClose?.(true))
+      setDialogCloseCallback(() => async () => await onTxClose?.(true))
 
       // call tx callback
-      onTxFinalize?.(data)
+      if (onTxFinalize) {
+        await onTxFinalize(data)
+      }
     } catch (e) {
       if (e instanceof ExtrinsicSignCancelledError) {
         console.warn('Sign cancelled')

+ 1 - 1
src/hooks/useUploadsManager/store.ts

@@ -7,7 +7,7 @@ const LOCAL_STORAGE_KEY = 'uploads'
 const uploadsManagerReducer = (state: UploadsManagerState, action: UploadsManagerStateAction): UploadsManagerState => {
   switch (action.type) {
     case 'ADD_ASSET':
-      return [...state, action.asset]
+      return [action.asset, ...state]
     case 'UPDATE_ASSET':
       return state.map((asset) => {
         if (asset.contentId !== action.contentId) {

+ 3 - 11
src/hooks/useUploadsManager/types.ts

@@ -1,14 +1,14 @@
-import { LiaisonJudgement } from '@/api/queries'
 import { ChannelId, VideoId } from '@/joystream-lib'
 import { AssetDimensions, ImageCropData } from '@/types/cropper'
 
 type AssetType = 'video' | 'thumbnail' | 'cover' | 'avatar'
 type AssetParent = 'video' | 'channel'
 
-export type AssetUploadStatus = 'completed' | 'inProgress' | 'error' | 'reconnecting' | 'reconnectionError'
+export type AssetUploadStatus = 'completed' | 'inProgress' | 'error' | 'reconnecting' | 'missing'
 
 export type AssetUpload = {
   contentId: string
+  ipfsContentId?: string
   parentObject: {
     type: AssetParent
     id: ChannelId | VideoId
@@ -16,14 +16,11 @@ export type AssetUpload = {
   owner: ChannelId
   type: AssetType
   lastStatus: AssetUploadStatus
-  liaisonJudgement?: LiaisonJudgement
-  ipfsContentId?: string
   // size in bytes
   size: number
   dimensions?: AssetDimensions
   imageCropData?: ImageCropData
   metadata?: string
-  title?: string | null
 }
 export type AssetUploadWithProgress = AssetUpload & {
   // progress of upload - 0...1
@@ -40,12 +37,7 @@ export type StartFileUploadOptions = {
 
 export type UploadManagerValue = {
   uploadsState: AssetUploadWithProgress[][]
-  startFileUpload: (
-    file: File | Blob | null,
-    asset: InputAssetUpload,
-    storageMetadata: string,
-    opts?: StartFileUploadOptions
-  ) => void
+  startFileUpload: (file: File | Blob | null, asset: InputAssetUpload, opts?: StartFileUploadOptions) => Promise<void>
   isLoading: boolean
 }
 export type UploadsProgressRecord = Record<string, number>

+ 93 - 115
src/hooks/useUploadsManager/useUploadsManager.tsx

@@ -5,10 +5,8 @@ import { useNavigate } from 'react-router'
 import * as rax from 'retry-axios'
 
 import { useChannel, useVideos } from '@/api/hooks'
-import { LiaisonJudgement } from '@/api/queries'
 import { absoluteRoutes } from '@/config/routes'
-import { useSnackbar, useUser } from '@/hooks'
-import { ChannelId } from '@/joystream-lib'
+import { useSnackbar, useUser, useStorageProviders } from '@/hooks'
 import { createStorageNodeUrl } from '@/utils/asset'
 
 import { useUploadsManagerStore } from './store'
@@ -20,7 +18,8 @@ import {
   StartFileUploadOptions,
 } from './types'
 
-const RETRIES_COUNT = 5
+const RETRIES_COUNT = 3
+const RETRY_DELAY = 1000
 const UPLOADING_SNACKBAR_TIMEOUT = 8000
 const UPLOADED_SNACKBAR_TIMEOUT = 13000
 
@@ -33,27 +32,21 @@ type AssetFile = {
   blob: File | Blob
 }
 
-class ReconnectFailedError extends Error {
-  reason: AxiosError
-
-  constructor(reason: AxiosError) {
-    super()
-    this.reason = reason
-  }
-}
-
 const UploadManagerContext = React.createContext<UploadManagerValue | undefined>(undefined)
 UploadManagerContext.displayName = 'UploadManagerContext'
 
 export const UploadManagerProvider: React.FC = ({ children }) => {
   const navigate = useNavigate()
   const { uploadsState, addAsset, updateAsset } = useUploadsManagerStore()
+  const { getStorageProvider, markStorageProviderNotWorking } = useStorageProviders()
   const { displaySnackbar } = useSnackbar()
   const [uploadsProgress, setUploadsProgress] = useState<UploadsProgressRecord>({})
+  // \/ workaround for now to not show completed uploads but not delete them since we may want to show history of uploads in the future
+  const [ignoredAssetsIds, setIgnoredAssetsIds] = useState<string[]>([])
   const [assetsFiles, setAssetsFiles] = useState<AssetFile[]>([])
   const { activeChannelId } = useUser()
-  const { channel, loading: channelLoading } = useChannel(activeChannelId ?? '')
-  const { videos, loading: videosLoading } = useVideos(
+  const { loading: channelLoading } = useChannel(activeChannelId ?? '')
+  const { loading: videosLoading } = useVideos(
     {
       where: {
         id_in: uploadsState.filter((item) => item.parentObject.type === 'video').map((item) => item.parentObject.id),
@@ -63,92 +56,57 @@ export const UploadManagerProvider: React.FC = ({ children }) => {
   )
   const pendingNotificationsCounts = useRef({ uploading: 0, uploaded: 0 })
 
-  const uploadsStateWithProgress: AssetUploadWithProgress[] = uploadsState.map((asset) => ({
-    ...asset,
-    progress: uploadsProgress[asset.contentId] ?? 0,
-  }))
-
-  const channelDataObjects = [channel?.avatarPhotoDataObject, channel?.coverPhotoDataObject]
-  const videosDataObjects = videos?.flatMap((video) => [video.mediaDataObject, video.thumbnailPhotoDataObject]) || []
-  const allDataObjects = [...channelDataObjects, ...videosDataObjects]
-
-  // Enriching data with pending/accepted/rejected status
-  const uploadsStateWithLiaisonJudgement = uploadsStateWithProgress
-    .map((asset) => {
-      const dataObject = allDataObjects.find((dataObject) => dataObject?.joystreamContentId === asset.contentId)
-      if (!dataObject && !channelLoading && !videosLoading) {
-        return null
-      }
-
-      return { ...asset, liaisonJudgement: dataObject?.liaisonJudgement, ipfsContentId: dataObject?.ipfsContentId }
-    })
-    .filter((asset) => asset !== null)
-
-  const lostConnectionAssets = uploadsStateWithLiaisonJudgement.filter(
-    (asset) => asset?.liaisonJudgement === LiaisonJudgement.Pending && asset.lastStatus === 'error'
-  )
-
+  // Will set all incomplete assets' status to missing on initial mount
+  const isInitialMount = useRef(true)
   useEffect(() => {
-    if (!lostConnectionAssets.length) {
+    if (!isInitialMount.current) {
       return
     }
-    displaySnackbar({
-      title: `(${lostConnectionAssets.length}) Asset${
-        lostConnectionAssets.length > 1 ? 's' : ''
-      } waiting to resume upload`,
-      description: 'Reconnect files to fix the issue',
-      actionText: 'See',
-      onActionClick: () => navigate(absoluteRoutes.studio.uploads()),
-      iconType: 'warning',
+    isInitialMount.current = false
+
+    let missingAssetsCount = 0
+    uploadsState.forEach((asset) => {
+      if (asset.lastStatus !== 'completed') {
+        updateAsset(asset.contentId, 'missing')
+        missingAssetsCount++
+      } else {
+        setIgnoredAssetsIds((ignored) => [...ignored, asset.contentId])
+      }
     })
-  }, [displaySnackbar, lostConnectionAssets.length, navigate])
 
-  // Enriching video type assets with video title
-  const uploadsStateWithVideoTitles = uploadsStateWithLiaisonJudgement.map((asset) => {
-    if (asset?.type === 'video') {
-      const video = videos?.find((video) => video.mediaDataObject?.joystreamContentId === asset.contentId)
-      const title = video?.title ?? null
-      return { ...asset, title }
+    if (missingAssetsCount > 0) {
+      displaySnackbar({
+        title: `(${missingAssetsCount}) Asset${missingAssetsCount > 1 ? 's' : ''} waiting to resume upload`,
+        description: 'Reconnect files to fix the issue',
+        actionText: 'See',
+        onActionClick: () => navigate(absoluteRoutes.studio.uploads()),
+        iconType: 'warning',
+      })
     }
-    return asset
-  })
+  }, [updateAsset, uploadsState, displaySnackbar, navigate])
 
-  // Check if liaison data and video title is available
-  uploadsStateWithVideoTitles.map((asset) => {
-    if (!channelLoading && !videosLoading && (!asset?.liaisonJudgement || !asset?.ipfsContentId)) {
-      console.warn(`Asset does not contain liaisonJudgement. ContentId: ${asset?.contentId}`)
-    }
-    if (!channelLoading && !videosLoading && asset?.type === 'video' && !asset?.title) {
-      console.warn(`Video type asset does not contain title. ContentId: ${asset.contentId}`)
-    }
-  })
+  const filteredUploadStateWithProgress: AssetUploadWithProgress[] = uploadsState
+    .filter((asset) => asset.owner === activeChannelId && !ignoredAssetsIds.includes(asset.contentId))
+    .map((asset) => ({
+      ...asset,
+      progress: uploadsProgress[asset.contentId] ?? 0,
+    }))
 
   // Grouping all assets by parent id (videos, channel)
-  const uploadsStateGroupedByParentObjectId = Object.values(
-    uploadsStateWithVideoTitles.reduce((acc: GroupByParentObjectIdAcc, asset) => {
+  const groupedUploadsState = Object.values(
+    filteredUploadStateWithProgress.reduce((acc: GroupByParentObjectIdAcc, asset) => {
       if (!asset) {
         return acc
       }
       const key = asset.parentObject.id
-      !acc[key] ? (acc[key] = [{ ...asset }]) : acc[key].push(asset)
+      if (!acc[key]) {
+        acc[key] = []
+      }
+      acc[key].push(asset)
       return acc
     }, {})
   )
 
-  // Will set all incompleted assets' status to error on initial mount
-  const isInitialMount = useRef(true)
-  useEffect(() => {
-    if (!isInitialMount.current) {
-      return
-    }
-    uploadsState.forEach((asset) => {
-      if (asset.lastStatus !== 'completed' && asset.lastStatus !== 'error') {
-        updateAsset(asset.contentId, 'error')
-      }
-    })
-    isInitialMount.current = false
-  }, [updateAsset, uploadsState])
-
   const displayUploadingNotification = useRef(
     debounce(() => {
       displaySnackbar({
@@ -182,7 +140,22 @@ export const UploadManagerProvider: React.FC = ({ children }) => {
   )
 
   const startFileUpload = useCallback(
-    async (file: File | Blob | null, asset: InputAssetUpload, storageUrl: string, opts?: StartFileUploadOptions) => {
+    async (file: File | Blob | null, asset: InputAssetUpload, opts?: StartFileUploadOptions) => {
+      let storageUrl: string, storageProviderId: string
+      try {
+        const storageProvider = getStorageProvider()
+        if (!storageProvider) {
+          return
+        }
+        storageUrl = storageProvider.url
+        storageProviderId = storageProvider.id
+      } catch (e) {
+        console.error('Failed to find storage provider', e)
+        return
+      }
+
+      console.debug(`Uploading to ${storageUrl}`)
+
       const setAssetUploadProgress = (progress: number) => {
         setUploadsProgress((prevState) => ({ ...prevState, [asset.contentId]: progress }))
       }
@@ -191,9 +164,9 @@ export const UploadManagerProvider: React.FC = ({ children }) => {
         setAssetsFiles((prevState) => [...prevState, { contentId: asset.contentId, blob: file }])
       }
 
-      rax.attach()
-      const assetUrl = createStorageNodeUrl(asset.contentId, storageUrl)
       try {
+        rax.attach()
+        const assetUrl = createStorageNodeUrl(asset.contentId, storageUrl)
         if (!fileInState && !file) {
           throw Error('File was not provided nor found')
         }
@@ -222,15 +195,22 @@ export const UploadManagerProvider: React.FC = ({ children }) => {
           raxConfig: {
             retry: RETRIES_COUNT,
             noResponseRetries: RETRIES_COUNT,
+            // add 400 to default list of codes to retry
+            // seems storage node sometimes fails to calculate the IFPS hash correctly
+            // trying again in that case should succeed
+            statusCodesToRetry: [
+              [100, 199],
+              [400, 400],
+              [429, 429],
+              [500, 599],
+            ],
+            retryDelay: RETRY_DELAY,
+            backoffType: 'static',
             onRetryAttempt: (err) => {
               const cfg = rax.getConfig(err)
               if (cfg?.currentRetryAttempt === 1) {
                 updateAsset(asset.contentId, 'reconnecting')
               }
-
-              if (cfg?.currentRetryAttempt === RETRIES_COUNT) {
-                throw new ReconnectFailedError(err)
-              }
             },
           },
           onUploadProgress: setUploadProgressThrottled,
@@ -246,23 +226,29 @@ export const UploadManagerProvider: React.FC = ({ children }) => {
         pendingNotificationsCounts.current.uploaded++
         displayUploadedNotification.current()
       } catch (e) {
-        if (e instanceof ReconnectFailedError) {
-          console.error('Failed to reconnect to storage provider', { storageUrl, reason: e.reason })
-          updateAsset(asset.contentId, 'reconnectionError')
-          displaySnackbar({
-            title: 'Asset failing to reconnect',
-            description: 'Host is not responding',
-            actionText: 'Go to uploads',
-            onActionClick: () => navigate(absoluteRoutes.studio.uploads()),
-            iconType: 'warning',
-          })
-        } else {
-          console.error('Unknown upload error', e)
-          updateAsset(asset.contentId, 'error')
+        console.error('Failed to upload to storage provider', { storageUrl, error: e })
+        updateAsset(asset.contentId, 'error')
+        setAssetUploadProgress(0)
+
+        const axiosError = e as AxiosError
+        const networkFailure =
+          axiosError.isAxiosError &&
+          (!axiosError.response?.status || (axiosError.response.status < 400 && axiosError.response.status >= 500))
+        if (networkFailure) {
+          markStorageProviderNotWorking(storageProviderId)
         }
+
+        const snackbarDescription = networkFailure ? 'Host is not responding' : 'Unexpected error occurred'
+        displaySnackbar({
+          title: 'Failed to upload asset',
+          description: snackbarDescription,
+          actionText: 'Go to uploads',
+          onActionClick: () => navigate(absoluteRoutes.studio.uploads()),
+          iconType: 'warning',
+        })
       }
     },
-    [addAsset, assetsFiles, displaySnackbar, navigate, updateAsset]
+    [addAsset, assetsFiles, displaySnackbar, getStorageProvider, markStorageProviderNotWorking, navigate, updateAsset]
   )
 
   const isLoading = channelLoading || videosLoading
@@ -272,7 +258,7 @@ export const UploadManagerProvider: React.FC = ({ children }) => {
       value={{
         startFileUpload,
         isLoading,
-        uploadsState: uploadsStateGroupedByParentObjectId,
+        uploadsState: groupedUploadsState,
       }}
     >
       {children}
@@ -280,18 +266,10 @@ export const UploadManagerProvider: React.FC = ({ children }) => {
   )
 }
 
-const useUploadsManagerContext = () => {
+export const useUploadsManager = () => {
   const ctx = useContext(UploadManagerContext)
   if (ctx === undefined) {
     throw new Error('useUploadsManager must be used within a UploadManagerProvider')
   }
   return ctx
 }
-
-export const useUploadsManager = (channelId: ChannelId) => {
-  const { uploadsState, ...rest } = useUploadsManagerContext()
-  return {
-    uploadsState,
-    ...rest,
-  }
-}

+ 1 - 1
src/joystream-lib/api.ts

@@ -163,7 +163,7 @@ export class JoystreamJs {
                     }
                   }
                   this.logError(`Extrinsic failed: "${errorMsg}"`)
-                  reject(new ExtrinsicFailedError(event))
+                  reject(new ExtrinsicFailedError(event, errorMsg))
                 } else if (event.method === 'ExtrinsicSuccess') {
                   const blockHash = status.asFinalized
                   this.api.rpc.chain

+ 2 - 2
src/joystream-lib/errors.ts

@@ -6,8 +6,8 @@ export class ExtrinsicUnknownError extends Error {}
 export class ExtrinsicFailedError extends Error {
   extrinsicFailedEvent: GenericEvent
 
-  constructor(event: GenericEvent) {
-    super()
+  constructor(event: GenericEvent, message?: string) {
+    super(message)
     this.extrinsicFailedEvent = event
   }
 }

+ 5 - 4
src/utils/cachingAssets.ts

@@ -98,23 +98,24 @@ type NormalizedVideoEdge = Omit<VideoEdge, 'node'> & {
 }
 
 export const removeVideoFromCache = (videoId: string, client: ApolloClient<object>) => {
-  client.cache.evict({ id: `Video:${videoId}` })
+  const videoRef = `Video:${videoId}`
   client.cache.modify({
     fields: {
       videos: (existingVideos = []) => {
         return existingVideos.filter((video: VideoFieldsFragment) => video.id !== videoId)
       },
-      videosConnection: (existing = {}, opts) => {
+      videosConnection: (existing = {}) => {
         return (
           existing && {
             ...existing,
-            totalCount: existing.edges.find((edge: NormalizedVideoEdge) => edge.node.__ref === `Video:${videoId}`)
+            totalCount: existing.edges.find((edge: NormalizedVideoEdge) => edge.node.__ref === videoRef)
               ? existing.totalCount - 1
               : existing.totalCount,
-            edges: existing.edges.filter((edge: NormalizedVideoEdge) => edge.node.__ref !== `Video:${videoId}`),
+            edges: existing.edges.filter((edge: NormalizedVideoEdge) => edge.node.__ref !== videoRef),
           }
         )
       },
     },
   })
+  client.cache.evict({ id: videoRef })
 }

+ 10 - 17
src/views/playground/Playgrounds/UploadFiles.tsx

@@ -1,13 +1,11 @@
 import React, { useState } from 'react'
 
-import { useRandomStorageProviderUrl } from '@/api/hooks'
 import { useAuthorizedUser, useUploadsManager } from '@/hooks'
 import { Button, TextField } from '@/shared/components'
 
 export const UploadFiles = () => {
   const { activeChannelId } = useAuthorizedUser()
-  const { startFileUpload, uploadsState } = useUploadsManager(activeChannelId)
-  const { getRandomStorageProviderUrl } = useRandomStorageProviderUrl()
+  const { startFileUpload, uploadsState } = useUploadsManager()
   const [contentId, setContentId] = useState('')
   const [file, setFile] = useState<File | null>(null)
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -18,23 +16,18 @@ export const UploadFiles = () => {
   }
 
   const handleUploadClick = () => {
-    const randomStorageProviderUrl = getRandomStorageProviderUrl()
-    if (!file || !randomStorageProviderUrl) {
+    if (!file) {
       return
     }
-    startFileUpload(
-      file,
-      {
-        contentId: contentId,
-        type: 'avatar',
-        parentObject: {
-          type: 'channel',
-          id: activeChannelId,
-        },
-        owner: activeChannelId,
+    startFileUpload(file, {
+      contentId: contentId,
+      type: 'avatar',
+      parentObject: {
+        type: 'channel',
+        id: activeChannelId,
       },
-      randomStorageProviderUrl
-    )
+      owner: activeChannelId,
+    })
   }
 
   return (

+ 25 - 36
src/views/studio/CreateEditChannelView/CreateEditChannelView.tsx

@@ -3,7 +3,7 @@ import { Controller, FieldError, useForm } from 'react-hook-form'
 import { useNavigate } from 'react-router-dom'
 import { CSSTransition } from 'react-transition-group'
 
-import { useChannel, useRandomStorageProviderUrl } from '@/api/hooks'
+import { useChannel } from '@/api/hooks'
 import { AssetAvailability } from '@/api/queries'
 import { ImageCropDialog, ImageCropDialogImperativeHandle, StudioContainer } from '@/components'
 import { languages } from '@/config/languages'
@@ -77,8 +77,6 @@ const CreateEditChannelView: React.FC<CreateEditChannelViewProps> = ({ newChanne
   const [avatarHashPromise, setAvatarHashPromise] = useState<Promise<string> | null>(null)
   const [coverHashPromise, setCoverHashPromise] = useState<Promise<string> | null>(null)
 
-  const { getRandomStorageProviderUrl } = useRandomStorageProviderUrl()
-
   const { activeMemberId, activeChannelId, setActiveUser, refetchActiveMembership } = useUser()
   const { joystream } = useJoystream()
   const { fee, handleTransaction } = useTransactionManager()
@@ -90,7 +88,7 @@ const CreateEditChannelView: React.FC<CreateEditChannelViewProps> = ({ newChanne
   const { channel, loading, error, refetch: refetchChannel, client } = useChannel(activeChannelId || '', {
     skip: newChannel || !activeChannelId,
   })
-  const { startFileUpload } = useUploadsManager(activeChannelId || '')
+  const { startFileUpload } = useUploadsManager()
 
   const {
     register,
@@ -231,40 +229,31 @@ const CreateEditChannelView: React.FC<CreateEditChannelViewProps> = ({ newChanne
     }
 
     const uploadAssets = (channelId: ChannelId) => {
-      const storageProviderUrl = getRandomStorageProviderUrl()
-      if (data.avatar.blob && avatarContentId && storageProviderUrl) {
-        startFileUpload(
-          data.avatar.blob,
-          {
-            contentId: avatarContentId,
-            owner: channelId,
-            parentObject: {
-              type: 'channel',
-              id: channelId,
-            },
-            dimensions: data.avatar.assetDimensions ?? undefined,
-            imageCropData: data.avatar.imageCropData ?? undefined,
-            type: 'avatar',
+      if (data.avatar.blob && avatarContentId) {
+        startFileUpload(data.avatar.blob, {
+          contentId: avatarContentId,
+          owner: channelId,
+          parentObject: {
+            type: 'channel',
+            id: channelId,
           },
-          storageProviderUrl
-        )
+          dimensions: data.avatar.assetDimensions ?? undefined,
+          imageCropData: data.avatar.imageCropData ?? undefined,
+          type: 'avatar',
+        })
       }
-      if (data.cover.blob && coverContentId && storageProviderUrl) {
-        startFileUpload(
-          data.cover.blob,
-          {
-            contentId: coverContentId,
-            owner: channelId,
-            parentObject: {
-              type: 'channel',
-              id: channelId,
-            },
-            dimensions: data.cover.assetDimensions ?? undefined,
-            imageCropData: data.cover.imageCropData ?? undefined,
-            type: 'cover',
+      if (data.cover.blob && coverContentId) {
+        startFileUpload(data.cover.blob, {
+          contentId: coverContentId,
+          owner: channelId,
+          parentObject: {
+            type: 'channel',
+            id: channelId,
           },
-          storageProviderUrl
-        )
+          dimensions: data.cover.assetDimensions ?? undefined,
+          imageCropData: data.cover.imageCropData ?? undefined,
+          type: 'cover',
+        })
       }
     }
 
@@ -310,7 +299,7 @@ const CreateEditChannelView: React.FC<CreateEditChannelViewProps> = ({ newChanne
           : joystream.updateChannel(activeChannelId ?? '', activeMemberId, metadata, assets, updateStatus),
       onTxFinalize: uploadAssets,
       onTxSync: refetchDataAndCacheAssets,
-      onTxClose: (completed) => completed && newChannel && navigate(absoluteRoutes.studio.videos()),
+      onTxClose: (completed) => (completed && newChannel ? navigate(absoluteRoutes.studio.videos()) : undefined),
       successMessage: {
         title: newChannel ? 'Channel successfully created!' : 'Channel successfully updated!',
         description: newChannel

+ 53 - 50
src/views/studio/EditVideoSheet/EditVideoSheet.tsx

@@ -3,7 +3,6 @@ import { formatISO } from 'date-fns'
 import React, { useEffect, useState } from 'react'
 import { FieldNamesMarkedBoolean } from 'react-hook-form'
 
-import { useRandomStorageProviderUrl } from '@/api/hooks'
 import {
   GetVideosConnectionDocument,
   GetVideosConnectionQuery,
@@ -56,10 +55,9 @@ export const EditVideoSheet: React.FC = () => {
   const { removeDraft } = useDrafts('video', activeChannelId)
 
   // transaction management
-  const { getRandomStorageProviderUrl } = useRandomStorageProviderUrl()
   const [thumbnailHashPromise, setThumbnailHashPromise] = useState<Promise<string> | null>(null)
   const [videoHashPromise, setVideoHashPromise] = useState<Promise<string> | null>(null)
-  const { startFileUpload } = useUploadsManager(activeChannelId)
+  const { startFileUpload } = useUploadsManager()
   const { joystream } = useJoystream()
   const { fee, handleTransaction } = useTransactionManager()
   const client = useApolloClient()
@@ -163,43 +161,37 @@ export const EditVideoSheet: React.FC = () => {
       }
     }
 
-    const uploadAssets = (videoId: VideoId) => {
-      const randomStorageProviderUrl = getRandomStorageProviderUrl()
-
-      if (videoInputFile?.blob && videoContentId && randomStorageProviderUrl) {
+    const uploadAssets = async (videoId: VideoId) => {
+      const uploadPromises: Promise<unknown>[] = []
+      if (videoInputFile?.blob && videoContentId) {
         const { mediaPixelWidth: width, mediaPixelHeight: height } = videoInputFile
-        startFileUpload(
-          videoInputFile.blob,
-          {
-            contentId: videoContentId,
-            owner: activeChannelId,
-            parentObject: {
-              type: 'video',
-              id: videoId,
-            },
+        const uploadPromise = startFileUpload(videoInputFile.blob, {
+          contentId: videoContentId,
+          owner: activeChannelId,
+          parentObject: {
             type: 'video',
-            dimensions: width && height ? { width, height } : undefined,
+            id: videoId,
           },
-          randomStorageProviderUrl
-        )
+          type: 'video',
+          dimensions: width && height ? { width, height } : undefined,
+        })
+        uploadPromises.push(uploadPromise)
       }
-      if (thumbnailInputFile?.blob && thumbnailContentId && randomStorageProviderUrl) {
-        startFileUpload(
-          thumbnailInputFile.blob,
-          {
-            contentId: thumbnailContentId,
-            owner: activeChannelId,
-            parentObject: {
-              type: 'video',
-              id: videoId,
-            },
-            type: 'thumbnail',
-            dimensions: thumbnailInputFile.assetDimensions,
-            imageCropData: thumbnailInputFile.imageCropData,
+      if (thumbnailInputFile?.blob && thumbnailContentId) {
+        const uploadPromise = startFileUpload(thumbnailInputFile.blob, {
+          contentId: thumbnailContentId,
+          owner: activeChannelId,
+          parentObject: {
+            type: 'video',
+            id: videoId,
           },
-          randomStorageProviderUrl
-        )
+          type: 'thumbnail',
+          dimensions: thumbnailInputFile.assetDimensions,
+          imageCropData: thumbnailInputFile.imageCropData,
+        })
+        uploadPromises.push(uploadPromise)
       }
+      await Promise.all(uploadPromises)
     }
 
     const refetchDataAndCacheAssets = async (videoId: VideoId) => {
@@ -243,22 +235,33 @@ export const EditVideoSheet: React.FC = () => {
       })
     }
 
-    handleTransaction({
-      preProcess: processAssets,
-      txFactory: (updateStatus) =>
-        isNew
-          ? joystream.createVideo(activeMemberId, activeChannelId, metadata, assets, updateStatus)
-          : joystream.updateVideo(selectedVideoTab.id, activeMemberId, activeChannelId, metadata, assets, updateStatus),
-      onTxFinalize: uploadAssets,
-      onTxSync: refetchDataAndCacheAssets,
-      onTxClose: (completed) => completed && setSheetState('minimized'),
-      successMessage: {
-        title: isNew ? 'Video successfully created!' : 'Video successfully updated!',
-        description: isNew
-          ? 'Your video was created and saved on the blockchain. Upload of video assets may still be in progress.'
-          : 'Changes to your video were saved on the blockchain.',
-      },
-    })
+    try {
+      await handleTransaction({
+        preProcess: processAssets,
+        txFactory: (updateStatus) =>
+          isNew
+            ? joystream.createVideo(activeMemberId, activeChannelId, metadata, assets, updateStatus)
+            : joystream.updateVideo(
+                selectedVideoTab.id,
+                activeMemberId,
+                activeChannelId,
+                metadata,
+                assets,
+                updateStatus
+              ),
+        onTxFinalize: uploadAssets,
+        onTxSync: refetchDataAndCacheAssets,
+        onTxClose: (completed) => (completed ? setSheetState('minimized') : undefined),
+        successMessage: {
+          title: isNew ? 'Video successfully created!' : 'Video successfully updated!',
+          description: isNew
+            ? 'Your video was created and saved on the blockchain. Upload of video assets may still be in progress.'
+            : 'Changes to your video were saved on the blockchain.',
+        },
+      })
+    } catch (e) {
+      console.error('Transaction handler failed', e)
+    }
   }
 
   const toggleMinimizedSheet = () => {

+ 11 - 42
src/views/studio/MyUploadsView/AssetLine/AssetLine.tsx

@@ -1,12 +1,10 @@
-import React, { useCallback, useState, useRef } from 'react'
+import React, { useCallback, useRef } from 'react'
 import { DropzoneOptions, useDropzone } from 'react-dropzone'
 import { useNavigate } from 'react-router'
 
-import { useRandomStorageProviderUrl } from '@/api/hooks'
-import { LiaisonJudgement } from '@/api/queries'
 import { ImageCropDialog, ImageCropDialogImperativeHandle } from '@/components'
 import { absoluteRoutes } from '@/config/routes'
-import { useUploadsManager, useAuthorizedUser, useDialog } from '@/hooks'
+import { useUploadsManager, useDialog } from '@/hooks'
 import { AssetUploadWithProgress } from '@/hooks/useUploadsManager/types'
 import { Text, CircularProgressbar, Button } from '@/shared/components'
 import { SvgAlertError, SvgAlertSuccess, SvgGlyphFileImage, SvgGlyphFileVideo, SvgGlyphUpload } from '@/shared/icons'
@@ -31,9 +29,7 @@ type AssetLineProps = {
 
 const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
   const navigate = useNavigate()
-  const { activeChannelId } = useAuthorizedUser()
-  const { startFileUpload } = useUploadsManager(activeChannelId)
-  const { getRandomStorageProviderUrl } = useRandomStorageProviderUrl()
+  const { startFileUpload } = useUploadsManager()
 
   const thumbnailDialogRef = useRef<ImageCropDialogImperativeHandle>(null)
   const avatarDialogRef = useRef<ImageCropDialogImperativeHandle>(null)
@@ -70,10 +66,6 @@ const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
       if (fileHash !== asset.ipfsContentId) {
         openDifferentFileDialog()
       } else {
-        const randomStorageProviderUrl = getRandomStorageProviderUrl()
-        if (!randomStorageProviderUrl) {
-          return
-        }
         startFileUpload(
           file,
           {
@@ -85,42 +77,28 @@ const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
             },
             type: asset.type,
           },
-          randomStorageProviderUrl,
           {
             isReUpload: true,
           }
         )
       }
     },
-    [
-      asset.contentId,
-      asset.ipfsContentId,
-      asset.owner,
-      asset.parentObject.id,
-      asset.parentObject.type,
-      asset.type,
-      getRandomStorageProviderUrl,
-      openDifferentFileDialog,
-      startFileUpload,
-    ]
+    [asset, openDifferentFileDialog, startFileUpload]
   )
 
+  const isVideo = asset.type === 'video'
   const { getRootProps, getInputProps, open: openFileSelect } = useDropzone({
     onDrop,
     maxFiles: 1,
     multiple: false,
     noClick: true,
     noKeyboard: true,
+    accept: isVideo ? 'video/*' : 'image/*',
   })
 
-  const isVideo = asset.type === 'video'
   const fileTypeText = isVideo ? 'Video file' : `${asset.type.charAt(0).toUpperCase() + asset.type.slice(1)} image`
 
   const handleChangeHost = () => {
-    const randomStorageProviderUrl = getRandomStorageProviderUrl()
-    if (!randomStorageProviderUrl) {
-      return
-    }
     startFileUpload(
       null,
       {
@@ -132,7 +110,6 @@ const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
         },
         type: asset.type,
       },
-      randomStorageProviderUrl,
       {
         changeHost: true,
       }
@@ -144,10 +121,6 @@ const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
     if (fileHash !== asset.ipfsContentId) {
       openDifferentFileDialog()
     } else {
-      const randomStorageProviderUrl = getRandomStorageProviderUrl()
-      if (!randomStorageProviderUrl) {
-        return
-      }
       startFileUpload(
         croppedBlob,
         {
@@ -159,7 +132,6 @@ const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
           },
           type: asset.type,
         },
-        randomStorageProviderUrl,
         {
           isReUpload: true,
         }
@@ -184,19 +156,16 @@ const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
 
   const renderStatusMessage = (asset: AssetUploadWithProgress) => {
     if (asset.lastStatus === 'reconnecting') {
-      return 'Trying to reconnect...'
+      return 'Reconnecting...'
     }
-    if (asset.lastStatus === 'reconnectionError') {
+    if (asset.lastStatus === 'error') {
       return (
         <Button size="small" variant="secondary" icon={<SvgGlyphUpload />} onClick={handleChangeHost}>
-          Change host
+          Try again
         </Button>
       )
     }
-    if (
-      asset.lastStatus === 'error' ||
-      (asset.lastStatus === 'inProgress' && asset.progress === 0 && asset.liaisonJudgement === LiaisonJudgement.Pending)
-    ) {
+    if (asset.lastStatus === 'missing') {
       return (
         <div {...getRootProps()}>
           <input {...getInputProps()} />
@@ -212,7 +181,7 @@ const AssetLine: React.FC<AssetLineProps> = ({ isLast = false, asset }) => {
     if (asset.lastStatus === 'completed') {
       return <SvgAlertSuccess />
     }
-    if (asset.lastStatus === 'error') {
+    if (asset.lastStatus === 'error' || asset.lastStatus === 'missing') {
       return <SvgAlertError />
     }
     return (

+ 2 - 4
src/views/studio/MyUploadsView/PlaceholderItems.tsx → src/views/studio/MyUploadsView/AssetsGroupUploadBar/AssetGroupUploadBarPlaceholder.tsx

@@ -9,9 +9,9 @@ import {
   AssetsGroupUploadBarContainer,
   AssetsInfoContainer,
   UploadInfoContainer,
-} from './AssetsGroupUploadBar/AssetsGroupUploadBar.style'
+} from './AssetsGroupUploadBar.style'
 
-const Placeholders = () => {
+export const AssetGroupUploadBarPlaceholder: React.FC = () => {
   return (
     <Container>
       <AssetsGroupUploadBarContainer style={{ backgroundColor: `${colors.gray[800]}` }}>
@@ -36,5 +36,3 @@ const StyledPlaceholderThumbnail = styled(Placeholder)`
     display: block;
   }
 `
-
-export const placeholderItems = Array(5).fill(Placeholders)

+ 42 - 21
src/views/studio/MyUploadsView/AssetsGroupUploadBar/AssetsGroupUploadBar.tsx

@@ -1,9 +1,10 @@
 import React, { useState, useRef } from 'react'
 
-import { LiaisonJudgement } from '@/api/queries'
+import { useChannel, useVideo } from '@/api/hooks'
 import { AssetUploadWithProgress } from '@/hooks/useUploadsManager/types'
 import { Text } from '@/shared/components'
 import { SvgAlertError, SvgNavChannel, SvgOutlineVideo } from '@/shared/icons'
+import { AssetGroupUploadBarPlaceholder } from '@/views/studio/MyUploadsView/AssetsGroupUploadBar/AssetGroupUploadBarPlaceholder'
 
 import {
   Container,
@@ -28,44 +29,58 @@ const AssetsGroupUploadBar: React.FC<AssetsGroupBarUploadProps> = ({ uploadData
 
   const isChannelType = uploadData[0].parentObject.type === 'channel'
 
+  const { video, loading: videoLoading } = useVideo(uploadData[0].parentObject.id, { skip: isChannelType })
+  const { channel, loading: channelLoading } = useChannel(uploadData[0].parentObject.id, { skip: !isChannelType })
+
   const isWaiting = uploadData.every((file) => file.progress === 0 && file.lastStatus === 'inProgress')
   const isCompleted = uploadData.every((file) => file.lastStatus === 'completed')
   const errorsCount = uploadData.filter(({ lastStatus }) => lastStatus === 'error').length
-  const reconnectionErrorsCount = uploadData.filter((file) => file.lastStatus === 'reconnectionError').length
-  const isPendingCount = uploadData.filter(
-    (file) =>
-      file.liaisonJudgement === LiaisonJudgement.Pending && file.progress === 0 && file.lastStatus !== 'completed'
-  ).length
+  const missingAssetsCount = uploadData.filter(({ lastStatus }) => lastStatus === 'missing').length
 
   const allAssetsSize = uploadData.reduce((acc, file) => acc + file.size, 0)
   const alreadyUploadedSize = uploadData.reduce((acc, file) => acc + (file.progress / 100) * file.size, 0)
   const masterProgress = Math.floor((alreadyUploadedSize / allAssetsSize) * 100)
 
-  const videoTitle = uploadData.find((asset) => asset.type === 'video')?.title
-  const assetsGroupTitleText = isChannelType ? 'Channel assets' : videoTitle
+  const assetsGroupTitleText = isChannelType ? 'Channel assets' : video?.title
   const assetsGroupNumberText = `${uploadData.length} asset${uploadData.length > 1 ? 's' : ''}`
 
   const renderAssetsGroupInfo = () => {
-    if (reconnectionErrorsCount) {
-      return (
-        <Text variant="subtitle2">{`(${reconnectionErrorsCount}) Asset${
-          reconnectionErrorsCount > 1 ? 's' : ''
-        } upload failed`}</Text>
-      )
-    }
     if (errorsCount) {
-      return <Text variant="subtitle2">{`(${errorsCount}) Asset${errorsCount > 1 ? 's' : ''} lost connection`}</Text>
+      return <Text variant="subtitle2">{`(${errorsCount}) Asset${errorsCount > 1 ? 's' : ''} upload failed`}</Text>
     }
-    if (isPendingCount) {
+    if (missingAssetsCount) {
       return (
-        <Text variant="subtitle2">{`(${isPendingCount}) Asset${isPendingCount > 1 ? 's' : ''} lost connection`}</Text>
+        <Text variant="subtitle2">{`(${missingAssetsCount}) Asset${
+          missingAssetsCount > 1 ? 's' : ''
+        } lost connection`}</Text>
       )
     }
     if (isWaiting) {
       return <Text variant="subtitle2">Waiting for upload...</Text>
     }
+    if (isCompleted) {
+      return <Text variant="subtitle2">Completed</Text>
+    }
+
+    return <Text variant="subtitle2">{`Uploading... (${masterProgress}%)`}</Text>
+  }
+
+  const enrichedUploadData =
+    (isChannelType && (channelLoading || !channel)) || (!isChannelType && (videoLoading || !video))
+      ? uploadData
+      : uploadData.map((asset) => {
+          const typeToAsset = {
+            'video': video?.mediaDataObject,
+            'thumbnail': video?.thumbnailPhotoDataObject,
+            'avatar': channel?.avatarPhotoDataObject,
+            'cover': channel?.coverPhotoDataObject,
+          }
+          const fetchedAsset = typeToAsset[asset.type]
+          return { ...asset, ipfsContentId: fetchedAsset?.ipfsContentId }
+        })
 
-    return <Text variant="subtitle2">{`Uploaded (${isCompleted ? 100 : masterProgress}%)`}</Text>
+  if (videoLoading || channelLoading) {
+    return <AssetGroupUploadBarPlaceholder />
   }
 
   return (
@@ -76,7 +91,13 @@ const AssetsGroupUploadBar: React.FC<AssetsGroupBarUploadProps> = ({ uploadData
       >
         <ProgressBar progress={isCompleted ? 100 : masterProgress} />
         <Thumbnail>
-          {errorsCount ? <SvgAlertError /> : isChannelType ? <SvgNavChannel /> : <SvgOutlineVideo />}
+          {errorsCount || missingAssetsCount ? (
+            <SvgAlertError />
+          ) : isChannelType ? (
+            <SvgNavChannel />
+          ) : (
+            <SvgOutlineVideo />
+          )}
         </Thumbnail>
         <AssetsInfoContainer>
           <Text variant="h6">{assetsGroupTitleText}</Text>
@@ -92,7 +113,7 @@ const AssetsGroupUploadBar: React.FC<AssetsGroupBarUploadProps> = ({ uploadData
         </UploadInfoContainer>
       </AssetsGroupUploadBarContainer>
       <AssetsDrawerContainer isActive={isAssetsDrawerActive} ref={drawer} maxHeight={drawer?.current?.scrollHeight}>
-        {uploadData.map((file, idx) => (
+        {enrichedUploadData.map((file, idx) => (
           <AssetLine key={file.contentId} asset={file} isLast={uploadData.length === idx + 1} />
         ))}
       </AssetsDrawerContainer>

+ 7 - 6
src/views/studio/MyUploadsView/MyUploadsView.tsx

@@ -1,23 +1,24 @@
 import React from 'react'
 
-import { useAuthorizedUser, useUploadsManager } from '@/hooks'
+import { useUploadsManager } from '@/hooks'
 
 import { AssetsGroupUploadBar } from './AssetsGroupUploadBar'
+import { AssetGroupUploadBarPlaceholder } from './AssetsGroupUploadBar/AssetGroupUploadBarPlaceholder'
 import { EmptyUploadsView } from './EmptyUploadsView'
 import { StyledText, UploadsContainer } from './MyUploadsView.style'
-import { placeholderItems } from './PlaceholderItems'
 
-const MyUploadsView = () => {
-  const { activeChannelId } = useAuthorizedUser()
-  const { uploadsState, isLoading } = useUploadsManager(activeChannelId)
+const MyUploadsView: React.FC = () => {
+  const { uploadsState, isLoading } = useUploadsManager()
 
   const hasUploads = uploadsState.length > 0
 
+  const placeholderItems = Array.from({ length: 5 }).map((_, idx) => <AssetGroupUploadBarPlaceholder key={idx} />)
+
   return (
     <UploadsContainer>
       <StyledText variant="h2">My uploads</StyledText>
       {isLoading ? (
-        placeholderItems.map((Placeholder, idx) => <Placeholder key={`placeholder-${idx}`} />)
+        placeholderItems
       ) : hasUploads ? (
         uploadsState.map((files) => <AssetsGroupUploadBar key={files[0].parentObject.id} uploadData={files} />)
       ) : (

+ 3 - 3
src/views/studio/MyVideosView/MyVideosView.tsx

@@ -19,8 +19,8 @@ import {
 
 const TABS = ['All Videos', 'Public', 'Drafts', 'Unlisted'] as const
 const SORT_OPTIONS = [
-  { name: 'Newest first', value: VideoOrderByInput.CreatedAtAsc },
-  { name: 'Oldest first', value: VideoOrderByInput.CreatedAtDesc },
+  { name: 'Newest first', value: VideoOrderByInput.CreatedAtDesc },
+  { name: 'Oldest first', value: VideoOrderByInput.CreatedAtAsc },
 ]
 
 const INITIAL_VIDEOS_PER_ROW = 4
@@ -33,7 +33,7 @@ export const MyVideosView = () => {
   const { displaySnackbar } = useSnackbar()
   const [videosPerRow, setVideosPerRow] = useState(INITIAL_VIDEOS_PER_ROW)
   const [sortVideosBy, setSortVideosBy] = useState<typeof SORT_OPTIONS[number]['value'] | undefined>(
-    VideoOrderByInput.CreatedAtAsc
+    VideoOrderByInput.CreatedAtDesc
   )
   const [tabIdToRemoveViaSnackbar, setTabIdToRemoveViaSnackbar] = useState<string>()
   const videosPerPage = ROWS_AMOUNT * videosPerRow