Browse Source

Release v3.4.0

attemka 1 year ago
parent
commit
7aebefbea4
100 changed files with 1375 additions and 1423 deletions
  1. 19 0
      CHANGELOG.md
  2. 1 1
      ci/nginx/default.conf.template
  3. 52 8
      docs/operator-guide.md
  4. 1 1
      package.json
  5. 2 2
      packages/atlas-meta-server/src/tags.ts
  6. 4 4
      packages/atlas/atlas.config.yml
  7. 1 1
      packages/atlas/package.json
  8. 20 2
      packages/atlas/src/MainLayout.tsx
  9. 2 3
      packages/atlas/src/api/client/cache.ts
  10. 0 74
      packages/atlas/src/api/client/index.ts
  11. 7 3
      packages/atlas/src/api/hooks/channel.ts
  12. 2 1
      packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts
  13. 0 1
      packages/atlas/src/api/queries/__generated__/bids.generated.tsx
  14. 0 15
      packages/atlas/src/api/queries/__generated__/channels.generated.tsx
  15. 0 4
      packages/atlas/src/api/queries/__generated__/comments.generated.tsx
  16. 0 12
      packages/atlas/src/api/queries/__generated__/featured.generated.tsx
  17. 0 69
      packages/atlas/src/api/queries/__generated__/fragments.generated.tsx
  18. 0 3
      packages/atlas/src/api/queries/__generated__/memberships.generated.tsx
  19. 0 57
      packages/atlas/src/api/queries/__generated__/nfts.generated.tsx
  20. 0 90
      packages/atlas/src/api/queries/__generated__/notifications.generated.tsx
  21. 0 4
      packages/atlas/src/api/queries/__generated__/transactionEvents.generated.tsx
  22. 0 60
      packages/atlas/src/api/queries/__generated__/videos.generated.tsx
  23. 0 1
      packages/atlas/src/api/queries/fragments.graphql
  24. 1 1
      packages/atlas/src/components/ActionBar/ActionBar.tsx
  25. 36 0
      packages/atlas/src/components/AssetImage/AssetImage.tsx
  26. 1 0
      packages/atlas/src/components/AssetImage/index.ts
  27. 19 0
      packages/atlas/src/components/AssetVideo/AssetVideo.tsx
  28. 1 1
      packages/atlas/src/components/Avatar/Avatar.stories.tsx
  29. 2 22
      packages/atlas/src/components/Avatar/Avatar.styles.ts
  30. 11 45
      packages/atlas/src/components/Avatar/Avatar.tsx
  31. 4 4
      packages/atlas/src/components/Avatar/AvatarGroup.stories.tsx
  32. 6 6
      packages/atlas/src/components/Avatar/AvatarGroup.tsx
  33. 1 0
      packages/atlas/src/components/Fee/Fee.tsx
  34. 3 6
      packages/atlas/src/components/MembershipInfo/MembershipInfo.tsx
  35. 3 1
      packages/atlas/src/components/MinimizedPlayer/MinimizedPlayer.tsx
  36. 20 17
      packages/atlas/src/components/NftCarousel/components/MarketplaceCarouselCard/NftCarouselDetails.tsx
  37. 130 12
      packages/atlas/src/components/NumberFormat/NumberFormat.tsx
  38. 1 1
      packages/atlas/src/components/OutputPill/OutputPill.stories.tsx
  39. 3 3
      packages/atlas/src/components/OutputPill/OutputPill.tsx
  40. 1 1
      packages/atlas/src/components/OwnerPill/OwnerPill.tsx
  41. 8 4
      packages/atlas/src/components/Searchbar/SearchBox/Result.tsx
  42. 9 0
      packages/atlas/src/components/Searchbar/Searchbar.styles.ts
  43. 4 4
      packages/atlas/src/components/Section/Section.stories.tsx
  44. 1 1
      packages/atlas/src/components/Section/SectionHeader/SectionHeader.stories.tsx
  45. 3 0
      packages/atlas/src/components/Table/Table.styles.ts
  46. 11 14
      packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx
  47. 4 2
      packages/atlas/src/components/TopSellingChannelsTable/TopSellingChannelsTable.tsx
  48. 1 1
      packages/atlas/src/components/WidgetTile/WidgetTile.styles.ts
  49. 5 1
      packages/atlas/src/components/_auth/SignInModal/SignInModal.tsx
  50. 1 1
      packages/atlas/src/components/_auth/SignInModal/SignInSteps/SignInModalMembershipStep.tsx
  51. 1 1
      packages/atlas/src/components/_channel/ChannelCard/ChannelCard.tsx
  52. 2 2
      packages/atlas/src/components/_channel/ChannelCover/ChannelCover.stories.tsx
  53. 2 1
      packages/atlas/src/components/_channel/ChannelCover/ChannelCover.styles.ts
  54. 10 17
      packages/atlas/src/components/_channel/ChannelCover/ChannelCover.tsx
  55. 1 1
      packages/atlas/src/components/_channel/ChannelLink/ChannelLink.tsx
  56. 1 1
      packages/atlas/src/components/_channel/ChannelWithVideos/ChannelWithVideos.tsx
  57. 4 4
      packages/atlas/src/components/_channel/CollectorsBox/CollectorsBox.stories.tsx
  58. 1 1
      packages/atlas/src/components/_channel/CollectorsBox/CollectorsBox.tsx
  59. 3 3
      packages/atlas/src/components/_comments/Comment/Comment.tsx
  60. 3 3
      packages/atlas/src/components/_comments/Comment/InternalComment.tsx
  61. 2 2
      packages/atlas/src/components/_comments/CommentEditHistory/CommentEditHistory.tsx
  62. 4 4
      packages/atlas/src/components/_comments/CommentRow/CommentRow.tsx
  63. 3 3
      packages/atlas/src/components/_comments/CommentSnapshot/CommentSnaphsot.tsx
  64. 18 18
      packages/atlas/src/components/_inputs/ComboBox/ComboBox.stories.tsx
  65. 2 1
      packages/atlas/src/components/_inputs/ComboBox/ComboBox.styles.ts
  66. 4 2
      packages/atlas/src/components/_inputs/ComboBox/ComboBox.tsx
  67. 1 1
      packages/atlas/src/components/_inputs/Input/Input.stories.tsx
  68. 4 4
      packages/atlas/src/components/_inputs/MemberComboBox/MemberComboBox.tsx
  69. 2 2
      packages/atlas/src/components/_inputs/SubtitlesBox/SubtitlesBox.tsx
  70. 1 1
      packages/atlas/src/components/_navigation/SidenavViewer/FollowedChannels.tsx
  71. 5 5
      packages/atlas/src/components/_navigation/TopbarStudio/TopbarStudio.tsx
  72. 3 3
      packages/atlas/src/components/_navigation/TopbarViewer/TopbarViewer.tsx
  73. 3 3
      packages/atlas/src/components/_nft/NftCard/Members.tsx
  74. 1 0
      packages/atlas/src/components/_nft/NftTile/NftTile.styles.ts
  75. 2 1
      packages/atlas/src/components/_nft/NftTile/NftTile.tsx
  76. 1 0
      packages/atlas/src/components/_nft/NftTile/NftTileDetails.styles.ts
  77. 14 6
      packages/atlas/src/components/_nft/NftTile/NftTileDetails.tsx
  78. 7 7
      packages/atlas/src/components/_nft/NftTileViewer/NftTileViewer.tsx
  79. 5 61
      packages/atlas/src/components/_nft/NftWidget/NftHistory.styles.ts
  80. 20 44
      packages/atlas/src/components/_nft/NftWidget/NftHistory.tsx
  81. 10 8
      packages/atlas/src/components/_nft/NftWidget/NftWidget.hooks.ts
  82. 46 48
      packages/atlas/src/components/_nft/NftWidget/NftWidget.styles.ts
  83. 72 542
      packages/atlas/src/components/_nft/NftWidget/NftWidget.tsx
  84. 41 0
      packages/atlas/src/components/_nft/NftWidget/NftWidget.types.ts
  85. 54 0
      packages/atlas/src/components/_nft/NftWidget/NftWidgetContent.styles.ts
  86. 542 0
      packages/atlas/src/components/_nft/NftWidget/NftWidgetContent.tsx
  87. 3 3
      packages/atlas/src/components/_notifications/NotificationTile/NotificationTile.tsx
  88. 14 14
      packages/atlas/src/components/_overlays/AcceptBidDialog/AcceptBidList.tsx
  89. 1 1
      packages/atlas/src/components/_overlays/ImageCropModal/ImageCropModal.stories.tsx
  90. 6 3
      packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdown.tsx
  91. 3 3
      packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownList.tsx
  92. 22 10
      packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx
  93. 2 2
      packages/atlas/src/components/_overlays/SendTransferDialogs/SendFundsDialog.tsx
  94. 3 3
      packages/atlas/src/components/_overlays/SendTransferDialogs/WithdrawFundsDialog.tsx
  95. 4 2
      packages/atlas/src/components/_video/BackgroundVideoPlayer/BackgroundVideoPlayer.styles.ts
  96. 6 4
      packages/atlas/src/components/_video/BackgroundVideoPlayer/BackgroundVideoPlayer.tsx
  97. 2 2
      packages/atlas/src/components/_video/VideoHero/VideoHero.tsx
  98. 3 3
      packages/atlas/src/components/_video/VideoPlayer/VideoOverlay.tsx
  99. 2 1
      packages/atlas/src/components/_video/VideoPlayer/VideoOverlays/EndingOverlay.styles.ts
  100. 8 8
      packages/atlas/src/components/_video/VideoPlayer/VideoOverlays/EndingOverlay.tsx

+ 19 - 0
CHANGELOG.md

@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [3.4.0] - 2023-07-03
+
+### Added
+
+- Added collapsed view to NFT widget
+- Added fiat representation to prices
+- Added environment variables description to operators guide
+
+### Changed
+
+- Changed assets resolution algorithm
+- Required NodeJS version bumped to 18
+
+### Fixed
+
+- Fixed bug with wallet connection
+- Fixed bug with minimized player showing error screen
+- Minor markup fixes
+
 ## [3.3.6] - 2023-06-23
 
 ### Changed

+ 1 - 1
ci/nginx/default.conf.template

@@ -17,7 +17,7 @@ server {
     server_tokens off;
 
     location / {
-        if ($http_user_agent ~* "baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator") {
+        if ($http_user_agent ~* "googlebot|YandexBot|baiduspider|twitterbot|facebookexternalhit|discordbot|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|Rocket.Chat|Minds|Taringa|redditbot") {
             proxy_pass http://${META_SERVER_HOSTNAME};
             break;
         }

+ 52 - 8
docs/operator-guide.md

@@ -167,14 +167,60 @@ joystream-cli content:createVideoCategory "My category" "My category description
 
 Once you run the above command, you can use the query above with a `orderBy: createdAt_DESC` argument to get the ID of the newly created category. You can then add it to the `content.categories` config entry.
 
-#### YouTube Partner Program
+In order to enable YPP content in atlas, `googleConsoleClientId` variable needs to be provided. You can read more about it [here](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid).
 
-All the YPP (YouTube Partner Program) parameters could be found in `atlas.config.yml` file located in `packages/atlas` directory.
-`features.ypp` section contains all the necessary params for setting up [youtube-synch](https://github.com/Joystream/youtube-synch/) and customizing user's rewards.
+#### Environment variables
 
-Once youtube-synch is setup, you'll need to provide its API URL in `youtubeSyncApiUrl` variable.
+Atlas uses environment variables, so you can customize it for your specific needs. `.env` file is located in `/packages/atlas/src` directory.
+Below is the list of all the variables used by Atlas with a short description:
 
-In order to enable YPP content in atlas, `googleConsoleClientId` variable needs to be provided. You can read more about it [here](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid).
+**Required:**
+
+`VITE_ENV` - used for making a production build.
+
+`VITE_ENV_SELECTION_ENABLED` - enables environment selection dropdown in the admin panel. Recommended to be set to `false` in production.
+
+`VITE_AVATAR_SERVICE_URL` - URL for avatar service - used to upload member's avatar.
+
+`VITE_HCAPTCHA_SITE_KEY` - hCaptcha site key. See [Captcha](#captcha) section for more info.
+
+`VITE_{environment}_ORION_URL` - Orion URL. See [Orion](#orion) section for more info.
+
+`VITE_{environment}_QUERY_NODE_SUBSCRIPTION_URL` - Orion's Query Node URL. See [Query Node](#query-node) section for more info.
+
+`VITE_{environment}_NODE_URL` - RPC Node URL. See [RPC Node](#rpc-node) section for more info.
+
+`VITE_{environment}_FAUCET_URL` - Faucet URL. See [Member Faucet](#member-faucet) section for more info.
+
+**Optional**:
+
+`VITE_DEFAULT_DATA_ENV` - if `VITE_ENV` is set to `true`, atlas will use `VITE_PRODUCTION_` URLs. Changing this variable can overwrite this.
+
+`VITE_FORCE_MAINTENANCE` - Setting this to `true` will enable the maintenance mode. Orion has its own KillSwitch mechanism which will also enable maintenance mode for Atlas, but this variable can be used when Orion is down or temporarily unavailable.
+
+`VITE_GEOLOCATION_SERVICE_URL` - URL for geolocation service - used to determine user's location to find the closest distributor. See [Geolocation service](#geolocation-service) section for more info.
+
+`VITE_APP_ID` - id of the app for wrapping metadata. You can receive an Id by [creating an app](https://github.com/Joystream/joystream/tree/master/cli#joystream-cli-appscreateapp) in joystream-cli.
+
+`VITE_APP_NAME` - name of the app for wrapping metadata.
+
+`VITE_GOOGLE_CONSOLE_CLIENT_ID` - ID of your Google console client. Used to enable the [youtube-sync](https://github.com/Joystream/youtube-synch) service for YPP.
+
+`VITE_YOUTUBE_SYNC_API_URL` - URL of your youtube-sync instance.
+
+`VITE_YOUTUBE_COLLABORATOR_MEMBER_ID` - The collaborator can add videos to a channel for a user. Every time a user signs up with the program, we're sending the `updateChannel` extrinsic and adding a collaborator to that channel. The collaborator is defined on the youtube-synch backend and needs to be the same in the front-end.
+
+`VITE_GA_ID` - Google Analytics ID. Used to enable Google Analytics.
+
+`VITE_SEGMENT_ID` - Segment ID. Used to enable Segment analytics tool.
+
+`VITE_SENTRY_DSN` - Sentry DSN. Used to enable Sentry error tracking.
+
+`VITE_LIVESESSION_ID` - LiveSession ID. Used to enable LiveSession analytics. See [Livesession](#livesession) section for more info.
+
+`VITE_OPTIMIZE_ID` - Optimize ID. Used to enable Google Optimize.
+
+`VITE_USERSNAP_ID` - Usersnap ID. Used to enable Usersnap.
 
 #### Terms of Service, Copyright Policy and Privacy Policy
 
@@ -200,9 +246,7 @@ Orion is the main backend service for Atlas - providing indexed blockchain data
 
 ### Query Node
 
-Query Node (QN) is a service that processes blockchain events and stores them in a database. Orion proxies requests from Atlas to QN, so it will need a QN URL provided for it to work. You can find more info about QN in [its repo](https://github.com/Joystream/joystream/tree/master/query-node). As a gateway operator you probably want to run your own QN, but it also should be fine to rely on a publicly available instance, like `https://query.joystream.org/graphql`.
-
-Atlas uses Websocket connection to QN to receive real-time updates about QN state and to properly update the UI. Endpoint for this connection must be passed as the `VITE_PRODUCTION_QUERY_NODE_SUBSCRIPTION_URL` environment variable.
+Query Node (QN) is a service that processes blockchain events and stores them in a database. Orion v2 has it's own Query Node included by default, so you don't need to run it separately. However, if you are running Orion v1, you will need to run QN separately. You can find more information about QN in [its repo](https://github.com/Joystream/joystream/tree/master/query-node).
 
 ### RPC Node
 

+ 1 - 1
package.json

@@ -80,7 +80,7 @@
     "blake3": "patch:blake3@npm:2.1.7#.yarn/patches/blake3-npm-2.1.7-7bf40c44b4"
   },
   "engines": {
-    "node": "^16"
+    "node": "^18"
   },
   "packageManager": "yarn@3.2.1"
 }

+ 2 - 2
packages/atlas-meta-server/src/tags.ts

@@ -54,7 +54,7 @@ export const generateCommonMetaTags = (
 
 export const generateVideoMetaTags = (
   video: BasicVideoFieldsFragment,
-  thumbnailUrl: string,
+  thumbnailUrls: string[],
   appName: string,
   baseAppUrl: string,
   twitterId?: string
@@ -67,7 +67,7 @@ export const generateVideoMetaTags = (
     videoUrl,
     videoTitle,
     video.description || videoTitle,
-    thumbnailUrl,
+    thumbnailUrls[0],
     twitterId
   )
 

+ 4 - 4
packages/atlas/atlas.config.yml

@@ -12,7 +12,7 @@ general:
   joystreamDiscordUrl: 'https://discord.gg/DE9UN3YpRP' # URL for Joystream Discord - used for support when errors occur
   appContentFocus: 'web3 & crypto' # Content focus of given app. Provide a string, for example `web & crypto` or `sport`
 storage:
-  assetResponseTimeout: 20000 # Timeout for asset response in ms - after this timeout, different distributor will be tried
+  assetResponseTimeout: 2_000 # Timeout for asset response in ms - after this timeout, different distributor will be tried
   assetUploadStatusPollingInterval: 2000 # Interval for polling asset upload status in ms - polling begins once asset is uploaded and is finished once QN reports the asset as accepted
   uploadProcessingTimeout: 60000 # Timeout for processing uploaded asset in ms - after this timeout, upload will be considered failed
   minimumDistributorRefetchTime: 1000 # Minimum time before refetching distributors list in ms - refetching is done if certain bag is not found on any distributor
@@ -24,7 +24,7 @@ storage:
 
 joystream:
   tokenTicker: 'JOY' # Ticker for the token used in the app
-  tokenPriceFeedUrl: null # URL for the token price feed - used to display token price in the app
+  tokenPriceFeedUrl: 'https://status.joystream.org/price' # URL for the token price feed - used to display token price in the app
   alternativeNodes:
     - name: 'Jsgenesis (Europe/UK)'
       url: 'wss://testnet-rpc-3-uk.joystream.org'
@@ -131,7 +131,7 @@ features:
 
         ## Other ways Joystream Channels can get rewards
 
-        Joystream DAO operating model encapsulates rewards for Joystream channels paid out outside of the Youtube Partnership Program. Gleev Operator, JS Genesis AS has no responsibility or control over such mechanisms and they are listed below.
+        Joystream DAO operating model encapsulates rewards for Joystream channels paid out outside of the YouTube Partnership Program. Gleev Operator, JS Genesis AS has no responsibility or control over such mechanisms and they are listed below.
 
         1. Payout Proposal to Channels by DAO Council.
         2. Direct Payment from DAO Working Group Budget by WG Lead.
@@ -586,7 +586,7 @@ legal:
 
     Joystream Standard License is the default setting for all uploads. It allows creators to retain all ownership over their content, which published on Joystream network storage nodes, while giving any App built and operated by JS Genesis, Joystream DAO or external operators that work with Joystream Blockchain to broadcast this content.
 
-    For content automatically synced from Youtube via Youtube Partnership Programme, YouTube Standard License is substituted by Joystream Standard License. 
+    For content automatically synced from YouTube via YouTube Partnership Program, YouTube Standard License is substituted by Joystream Standard License. 
 
     2. CCO
 

+ 1 - 1
packages/atlas/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@joystream/atlas",
   "description": "UI for consuming Joystream - a user governed video platform",
-  "version": "3.3.6",
+  "version": "3.4.0",
   "license": "GPL-3.0",
   "scripts": {
     "start": "vite",

+ 20 - 2
packages/atlas/src/MainLayout.tsx

@@ -1,11 +1,12 @@
 import loadable from '@loadable/component'
 import { FC, useEffect, useRef, useState } from 'react'
-import { Route, Routes, useLocation, useNavigationType } from 'react-router-dom'
+import { Route, Routes, useLocation, useNavigationType, useSearchParams } from 'react-router-dom'
 
 import { StudioLoading } from '@/components/_loaders/StudioLoading'
 import { CookiePopover } from '@/components/_overlays/CookiePopover'
 import { atlasConfig } from '@/config'
 import { BASE_PATHS, absoluteRoutes } from '@/config/routes'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { transitions } from '@/styles'
 import { RoutingState } from '@/types/routing'
 import { isBrowserOutdated } from '@/utils/browser'
@@ -40,6 +41,7 @@ const LoadablePlaygroundLayout = loadable(() => import('./views/playground/Playg
 export const MainLayout: FC = () => {
   const scrollPosition = useRef<number>(0)
   const location = useLocation()
+  const [searchParams] = useSearchParams()
   const navigationType = useNavigationType()
   const [cachedLocation, setCachedLocation] = useState(location)
   const locationState = location.state as RoutingState
@@ -54,11 +56,24 @@ export const MainLayout: FC = () => {
     },
     onExitClick: () => closeDialog(),
   })
+  const { trackPageView } = useSegmentAnalytics()
 
   useEffect(() => {
+    // had to include this timeout to make sure the page title is updated
+    const trackRequestTimeout = setTimeout(
+      () =>
+        trackPageView(
+          document.title,
+          'viewer',
+          (location.pathname === absoluteRoutes.viewer.ypp() && searchParams.get('referrer')) || undefined
+        ),
+      1000
+    )
+
     if (!atlasConfig.analytics.sentry?.dsn) {
       return
     }
+
     const stopReplay = async () => await SentryLogger.replay?.stop()
 
     if (location.pathname === absoluteRoutes.viewer.ypp()) {
@@ -70,7 +85,10 @@ export const MainLayout: FC = () => {
         stopReplay()
       }
     }
-  }, [location.pathname])
+    return () => {
+      clearTimeout(trackRequestTimeout)
+    }
+  }, [location.pathname, trackPageView, searchParams])
 
   const { clearOverlays } = useOverlayManager()
 

+ 2 - 3
packages/atlas/src/api/client/cache.ts

@@ -273,10 +273,9 @@ const cache = new InMemoryCache({
     StorageDataObject: {
       fields: {
         resolvedUrls: {
-          read: (resolvedUrl, { readField }) => {
+          read: (resolvedUrls, { readField }) => {
             const isAccepted = readField('isAccepted')
-
-            return isAccepted ? resolvedUrl : []
+            return isAccepted ? resolvedUrls : []
           },
         },
         size: {

+ 0 - 74
packages/atlas/src/api/client/index.ts

@@ -3,17 +3,11 @@ import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
 import { getMainDefinition } from '@apollo/client/utilities'
 import { createClient } from 'graphql-ws'
 
-import { atlasConfig } from '@/config'
 import { ORION_GRAPHQL_URL, QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL } from '@/config/env'
-import { logDistributorPerformance, testAssetDownload } from '@/providers/assets/assets.helpers'
 import { useUserLocationStore } from '@/providers/userLocation'
-import { AssetLogger, ConsoleLogger, DistributorEventEntry, SentryLogger } from '@/utils/logs'
-import { TimeoutError, withTimeout } from '@/utils/misc'
 
 import cache from './cache'
 
-import { StorageDataObject } from '../queries/__generated__/baseTypes.generated'
-
 const delayLink = new ApolloLink((operation, forward) => {
   const ctx = operation.getContext()
   if (!ctx.delay) {
@@ -29,9 +23,6 @@ const delayLink = new ApolloLink((operation, forward) => {
   })
 })
 
-const MAX_ALLOWED_RETRIES = 10
-const bannedDistributorUrls: Record<string, number> = {}
-
 const createApolloClient = () => {
   const subscriptionLink = new GraphQLWsLink(
     createClient({
@@ -71,71 +62,6 @@ const createApolloClient = () => {
   return new ApolloClient({
     cache,
     link: operationSplitLink,
-    resolvers: {
-      StorageDataObject: {
-        resolvedUrl: async (parent: StorageDataObject) => {
-          if (!parent.isAccepted) {
-            return null
-          }
-
-          // skip distributor url if he failed more than n times(where n is MAX_ALLOWED_RETRIES)
-          const resolvedUrls = parent.resolvedUrls?.filter((url) => {
-            const distributorUrl = url.split(`/${atlasConfig.storage.assetPath}/${parent.id}`)[0]
-            return (bannedDistributorUrls[distributorUrl] || 0) <= MAX_ALLOWED_RETRIES
-          })
-
-          for (const resolvedUrl of resolvedUrls) {
-            if (!parent.type) {
-              return null
-            }
-
-            if (parent.type.__typename === 'DataObjectTypeChannelPayoutsPayload') {
-              // if this is a payload file skip testing and just return first url.
-              return resolvedUrls[0]
-            }
-            const distributorUrl = resolvedUrl.split(`/${atlasConfig.storage.assetPath}/${parent.id}`)[0]
-
-            const assetTestPromise = testAssetDownload(resolvedUrl, parent.type)
-            const assetTestPromiseWithTimeout = withTimeout(assetTestPromise, atlasConfig.storage.assetResponseTimeout)
-            const eventEntry: DistributorEventEntry = {
-              distributorUrl,
-              dataObjectId: parent.id,
-              dataObjectType: parent.type?.__typename,
-            }
-
-            try {
-              await assetTestPromiseWithTimeout
-
-              logDistributorPerformance(resolvedUrl, eventEntry)
-              return resolvedUrl
-            } catch (err) {
-              if (err instanceof MediaError) {
-                SentryLogger.error('Error during asset download test, media is not supported', 'AssetsManager', err, {
-                  asset: { parent, resolvedUrl, mediaError: err },
-                })
-                return null
-              }
-              bannedDistributorUrls[distributorUrl] = (bannedDistributorUrls[distributorUrl] || 0) + 1
-              if (err instanceof TimeoutError) {
-                AssetLogger.logDistributorResponseTimeout(eventEntry)
-                ConsoleLogger.warn(
-                  `Distributor didn't respond in ${atlasConfig.storage.assetResponseTimeout} seconds`,
-                  {
-                    dataObject: parent,
-                  }
-                )
-              } else {
-                AssetLogger.logDistributorError(eventEntry)
-                SentryLogger.error('Error during asset download test', 'AssetsManager', err, {
-                  asset: { parent, resolvedUrl },
-                })
-              }
-            }
-          }
-          return null
-        },
-      },
-    },
   })
 }
 

+ 7 - 3
packages/atlas/src/api/hooks/channel.ts

@@ -23,6 +23,7 @@ import {
   useGetTop10ChannelsQuery,
   useUnfollowChannelMutation,
 } from '@/api/queries/__generated__/channels.generated'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 
 export const useBasicChannel = (
   id: string,
@@ -82,9 +83,11 @@ export const useBasicChannels = (
 
 export const useFollowChannel = (opts?: MutationHookOptions<FollowChannelMutation>) => {
   const [followChannel, rest] = useFollowChannelMutation()
+  const { trackChannelFollow } = useSegmentAnalytics()
   return {
-    followChannel: (id: string) =>
-      followChannel({
+    followChannel: (id: string) => {
+      trackChannelFollow(id)
+      return followChannel({
         ...opts,
         variables: {
           channelId: id,
@@ -100,7 +103,8 @@ export const useFollowChannel = (opts?: MutationHookOptions<FollowChannelMutatio
             },
           })
         },
-      }),
+      })
+    },
     ...rest,
   }
 }

+ 2 - 1
packages/atlas/src/api/queries/__generated__/baseTypes.generated.ts

@@ -3850,9 +3850,11 @@ export type MutationSetVideoViewPerIpTimeLimitArgs = {
 
 export type MutationSetVideoWeightsArgs = {
   commentsWeight: Scalars['Float']
+  joysteamTimestampSubWeight: Scalars['Float']
   newnessWeight: Scalars['Float']
   reactionsWeight: Scalars['Float']
   viewsWeight: Scalars['Float']
+  ytTimestampSubWeight: Scalars['Float']
 }
 
 export type MutationSignAppActionCommitmentArgs = {
@@ -6573,7 +6575,6 @@ export type StorageDataObject = {
   ipfsHash: Scalars['String']
   /** Whether the data object was uploaded and accepted by the storage provider */
   isAccepted: Scalars['Boolean']
-  resolvedUrl?: Maybe<Scalars['String']>
   /** Resolved asset urls */
   resolvedUrls: Array<Scalars['String']>
   /** Data object size in bytes */

+ 0 - 1
packages/atlas/src/api/queries/__generated__/bids.generated.tsx

@@ -39,7 +39,6 @@ export type GetBidsQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean

+ 0 - 15
packages/atlas/src/api/queries/__generated__/channels.generated.tsx

@@ -46,7 +46,6 @@ export type GetFullChannelQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -70,7 +69,6 @@ export type GetFullChannelQuery = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -89,7 +87,6 @@ export type GetFullChannelQuery = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -131,7 +128,6 @@ export type GetExtendedBasicChannelsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -189,7 +185,6 @@ export type GetExtendedFullChannelsQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -213,7 +208,6 @@ export type GetExtendedFullChannelsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -232,7 +226,6 @@ export type GetExtendedFullChannelsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -279,7 +272,6 @@ export type GetBasicChannelsConnectionQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -340,7 +332,6 @@ export type GetTop10ChannelsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -380,7 +371,6 @@ export type GetDiscoverChannelsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -423,7 +413,6 @@ export type GetChannelNftCollectorsQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -493,7 +482,6 @@ export type GetPayloadDataByCommitmentQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -647,7 +635,6 @@ export type GetTopSellingChannelsFromThreePeriodsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -681,7 +668,6 @@ export type GetTopSellingChannelsFromThreePeriodsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -715,7 +701,6 @@ export type GetTopSellingChannelsFromThreePeriodsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean

+ 0 - 4
packages/atlas/src/api/queries/__generated__/comments.generated.tsx

@@ -34,7 +34,6 @@ export type GetCommentQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -101,7 +100,6 @@ export type GetCommentRepliesConnectionQuery = {
                     __typename?: 'StorageDataObject'
                     id: string
                     resolvedUrls: Array<string>
-                    resolvedUrl?: string | null
                     createdAt: Date
                     size: string
                     isAccepted: boolean
@@ -166,7 +164,6 @@ export type GetUserCommentsAndVideoCommentsConnectionQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -222,7 +219,6 @@ export type GetUserCommentsAndVideoCommentsConnectionQuery = {
                     __typename?: 'StorageDataObject'
                     id: string
                     resolvedUrls: Array<string>
-                    resolvedUrl?: string | null
                     createdAt: Date
                     size: string
                     isAccepted: boolean

+ 0 - 12
packages/atlas/src/api/queries/__generated__/featured.generated.tsx

@@ -36,7 +36,6 @@ export type GetVideoHeroQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -56,7 +55,6 @@ export type GetVideoHeroQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -104,7 +102,6 @@ export type GetVideoHeroQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -128,7 +125,6 @@ export type GetVideoHeroQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -161,7 +157,6 @@ export type GetVideoHeroQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -223,7 +218,6 @@ export type GetVideoHeroQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -265,7 +259,6 @@ export type GetVideoHeroQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -302,7 +295,6 @@ export type GetVideoHeroQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -367,7 +359,6 @@ export type GetAllCategoriesFeaturedVideosQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -387,7 +378,6 @@ export type GetAllCategoriesFeaturedVideosQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -468,7 +458,6 @@ export type GetCategoryFeaturedVideosQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -488,7 +477,6 @@ export type GetCategoryFeaturedVideosQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean

+ 0 - 69
packages/atlas/src/api/queries/__generated__/fragments.generated.tsx

@@ -21,7 +21,6 @@ export type BasicChannelFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -66,7 +65,6 @@ export type FullChannelFieldsFragment = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -90,7 +88,6 @@ export type FullChannelFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -109,7 +106,6 @@ export type FullChannelFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -157,7 +153,6 @@ export type ExtendedFullChannelFieldsFragment = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -181,7 +176,6 @@ export type ExtendedFullChannelFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -200,7 +194,6 @@ export type ExtendedFullChannelFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -234,7 +227,6 @@ export type ExtendedBasicChannelFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -266,7 +258,6 @@ export type BasicMembershipFieldsFragment = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -308,7 +299,6 @@ export type FullMembershipFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -327,7 +317,6 @@ export type FullMembershipFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -353,7 +342,6 @@ export type FullMembershipFieldsFragment = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -378,7 +366,6 @@ export type StorageDataObjectFieldsFragment = {
   __typename?: 'StorageDataObject'
   id: string
   resolvedUrls: Array<string>
-  resolvedUrl?: string | null
   createdAt: Date
   size: string
   isAccepted: boolean
@@ -433,7 +420,6 @@ export type SubtitlesFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -472,7 +458,6 @@ export type BasicVideoFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -492,7 +477,6 @@ export type BasicVideoFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -540,7 +524,6 @@ export type BasicVideoFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -564,7 +547,6 @@ export type BasicVideoFieldsFragment = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -597,7 +579,6 @@ export type BasicVideoFieldsFragment = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -659,7 +640,6 @@ export type BasicVideoFieldsFragment = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -701,7 +681,6 @@ export type BasicVideoFieldsFragment = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -738,7 +717,6 @@ export type BasicVideoFieldsFragment = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -804,7 +782,6 @@ export type FullVideoFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -823,7 +800,6 @@ export type FullVideoFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -866,7 +842,6 @@ export type FullVideoFieldsFragment = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -890,7 +865,6 @@ export type FullVideoFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -909,7 +883,6 @@ export type FullVideoFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -965,7 +938,6 @@ export type FullVideoFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -989,7 +961,6 @@ export type FullVideoFieldsFragment = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -1022,7 +993,6 @@ export type FullVideoFieldsFragment = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -1084,7 +1054,6 @@ export type FullVideoFieldsFragment = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1126,7 +1095,6 @@ export type FullVideoFieldsFragment = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1163,7 +1131,6 @@ export type FullVideoFieldsFragment = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1201,7 +1168,6 @@ export type FullVideoFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -1252,7 +1218,6 @@ export type BasicNftFieldsFragment = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -1276,7 +1241,6 @@ export type BasicNftFieldsFragment = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -1309,7 +1273,6 @@ export type BasicNftFieldsFragment = {
                     __typename?: 'StorageDataObject'
                     id: string
                     resolvedUrls: Array<string>
-                    resolvedUrl?: string | null
                     createdAt: Date
                     size: string
                     isAccepted: boolean
@@ -1371,7 +1334,6 @@ export type BasicNftFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1413,7 +1375,6 @@ export type BasicNftFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1450,7 +1411,6 @@ export type BasicNftFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1508,7 +1468,6 @@ export type FullNftFieldsFragment = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -1528,7 +1487,6 @@ export type FullNftFieldsFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -1576,7 +1534,6 @@ export type FullNftFieldsFragment = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1600,7 +1557,6 @@ export type FullNftFieldsFragment = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -1633,7 +1589,6 @@ export type FullNftFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1695,7 +1650,6 @@ export type FullNftFieldsFragment = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1737,7 +1691,6 @@ export type FullNftFieldsFragment = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1774,7 +1727,6 @@ export type FullNftFieldsFragment = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1829,7 +1781,6 @@ export type FullNftFieldsFragment = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -1853,7 +1804,6 @@ export type FullNftFieldsFragment = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -1886,7 +1836,6 @@ export type FullNftFieldsFragment = {
                     __typename?: 'StorageDataObject'
                     id: string
                     resolvedUrls: Array<string>
-                    resolvedUrl?: string | null
                     createdAt: Date
                     size: string
                     isAccepted: boolean
@@ -1948,7 +1897,6 @@ export type FullNftFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1990,7 +1938,6 @@ export type FullNftFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2027,7 +1974,6 @@ export type FullNftFieldsFragment = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2077,7 +2023,6 @@ export type BasicBidFieldsFragment = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -2127,7 +2072,6 @@ export type FullBidFieldsFragment = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -2178,7 +2122,6 @@ export type CommentFieldsFragment = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -2235,7 +2178,6 @@ export type MetaprotocolTransactionResultFields_MetaprotocolTransactionResultCom
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -2289,7 +2231,6 @@ export type MetaprotocolTransactionResultFields_MetaprotocolTransactionResultCom
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -2343,7 +2284,6 @@ export type MetaprotocolTransactionResultFields_MetaprotocolTransactionResultCom
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -2397,7 +2337,6 @@ export type MetaprotocolTransactionResultFields_MetaprotocolTransactionResultCom
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -2462,7 +2401,6 @@ export type BasicNftOwnerFields_NftOwnerChannel_Fragment = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -2501,7 +2439,6 @@ export type BasicNftOwnerFields_NftOwnerMember_Fragment = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -2549,7 +2486,6 @@ export type BasicFeaturedVideoFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -2569,7 +2505,6 @@ export type BasicFeaturedVideoFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -2639,7 +2574,6 @@ export type BasicVideoFeaturedInCategoryFragment = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -2659,7 +2593,6 @@ export type BasicVideoFeaturedInCategoryFragment = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -2713,7 +2646,6 @@ export type BasicVideoActivityFieldsFragment = {
     __typename?: 'StorageDataObject'
     id: string
     resolvedUrls: Array<string>
-    resolvedUrl?: string | null
     createdAt: Date
     size: string
     isAccepted: boolean
@@ -2743,7 +2675,6 @@ export const StorageDataObjectFieldsFragmentDoc = gql`
   fragment StorageDataObjectFields on StorageDataObject {
     id
     resolvedUrls
-    resolvedUrl @client
     createdAt
     size
     isAccepted

+ 0 - 3
packages/atlas/src/api/queries/__generated__/memberships.generated.tsx

@@ -32,7 +32,6 @@ export type GetMembershipsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -51,7 +50,6 @@ export type GetMembershipsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -77,7 +75,6 @@ export type GetMembershipsQuery = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean

+ 0 - 57
packages/atlas/src/api/queries/__generated__/nfts.generated.tsx

@@ -44,7 +44,6 @@ export type GetNftQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -64,7 +63,6 @@ export type GetNftQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -112,7 +110,6 @@ export type GetNftQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -136,7 +133,6 @@ export type GetNftQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -169,7 +165,6 @@ export type GetNftQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -231,7 +226,6 @@ export type GetNftQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -273,7 +267,6 @@ export type GetNftQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -310,7 +303,6 @@ export type GetNftQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -365,7 +357,6 @@ export type GetNftQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -389,7 +380,6 @@ export type GetNftQuery = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -422,7 +412,6 @@ export type GetNftQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -484,7 +473,6 @@ export type GetNftQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -526,7 +514,6 @@ export type GetNftQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -563,7 +550,6 @@ export type GetNftQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -631,7 +617,6 @@ export type GetNftsQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -651,7 +636,6 @@ export type GetNftsQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -699,7 +683,6 @@ export type GetNftsQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -723,7 +706,6 @@ export type GetNftsQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -756,7 +738,6 @@ export type GetNftsQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -818,7 +799,6 @@ export type GetNftsQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -860,7 +840,6 @@ export type GetNftsQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -897,7 +876,6 @@ export type GetNftsQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -952,7 +930,6 @@ export type GetNftsQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -976,7 +953,6 @@ export type GetNftsQuery = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -1009,7 +985,6 @@ export type GetNftsQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -1071,7 +1046,6 @@ export type GetNftsQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1113,7 +1087,6 @@ export type GetNftsQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1150,7 +1123,6 @@ export type GetNftsQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1224,7 +1196,6 @@ export type GetNftsConnectionQuery = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -1244,7 +1215,6 @@ export type GetNftsConnectionQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -1292,7 +1262,6 @@ export type GetNftsConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1316,7 +1285,6 @@ export type GetNftsConnectionQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -1349,7 +1317,6 @@ export type GetNftsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1411,7 +1378,6 @@ export type GetNftsConnectionQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -1453,7 +1419,6 @@ export type GetNftsConnectionQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -1490,7 +1455,6 @@ export type GetNftsConnectionQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -1545,7 +1509,6 @@ export type GetNftsConnectionQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1569,7 +1532,6 @@ export type GetNftsConnectionQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -1602,7 +1564,6 @@ export type GetNftsConnectionQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1664,7 +1625,6 @@ export type GetNftsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1706,7 +1666,6 @@ export type GetNftsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1743,7 +1702,6 @@ export type GetNftsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1804,7 +1762,6 @@ export type GetFeaturedNftsVideosQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -1832,7 +1789,6 @@ export type GetFeaturedNftsVideosQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -1852,7 +1808,6 @@ export type GetFeaturedNftsVideosQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -1900,7 +1855,6 @@ export type GetFeaturedNftsVideosQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1924,7 +1878,6 @@ export type GetFeaturedNftsVideosQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -1957,7 +1910,6 @@ export type GetFeaturedNftsVideosQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -2019,7 +1971,6 @@ export type GetFeaturedNftsVideosQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -2061,7 +2012,6 @@ export type GetFeaturedNftsVideosQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -2098,7 +2048,6 @@ export type GetFeaturedNftsVideosQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -2153,7 +2102,6 @@ export type GetFeaturedNftsVideosQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2177,7 +2125,6 @@ export type GetFeaturedNftsVideosQuery = {
               __typename?: 'StorageDataObject'
               id: string
               resolvedUrls: Array<string>
-              resolvedUrl?: string | null
               createdAt: Date
               size: string
               isAccepted: boolean
@@ -2210,7 +2157,6 @@ export type GetFeaturedNftsVideosQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -2272,7 +2218,6 @@ export type GetFeaturedNftsVideosQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -2314,7 +2259,6 @@ export type GetFeaturedNftsVideosQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -2351,7 +2295,6 @@ export type GetFeaturedNftsVideosQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean

+ 0 - 90
packages/atlas/src/api/queries/__generated__/notifications.generated.tsx

@@ -52,7 +52,6 @@ export type GetNotificationsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -88,7 +87,6 @@ export type GetNotificationsConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -119,7 +117,6 @@ export type GetNotificationsConnectionQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -156,7 +153,6 @@ export type GetNotificationsConnectionQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -194,7 +190,6 @@ export type GetNotificationsConnectionQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -236,7 +231,6 @@ export type GetNotificationsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -266,7 +260,6 @@ export type GetNotificationsConnectionQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -303,7 +296,6 @@ export type GetNotificationsConnectionQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -341,7 +333,6 @@ export type GetNotificationsConnectionQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -391,7 +382,6 @@ export type GetNotificationsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -432,7 +422,6 @@ export type GetNotificationsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -462,7 +451,6 @@ export type GetNotificationsConnectionQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -499,7 +487,6 @@ export type GetNotificationsConnectionQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -537,7 +524,6 @@ export type GetNotificationsConnectionQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -579,7 +565,6 @@ export type GetNotificationsConnectionQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -609,7 +594,6 @@ export type GetNotificationsConnectionQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -648,7 +632,6 @@ export type GetNotificationsConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -678,7 +661,6 @@ export type GetNotificationsConnectionQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -715,7 +697,6 @@ export type GetNotificationsConnectionQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -753,7 +734,6 @@ export type GetNotificationsConnectionQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -811,7 +791,6 @@ export type GetNftHistoryQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -851,7 +830,6 @@ export type GetNftHistoryQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -894,7 +872,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -932,7 +909,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -975,7 +951,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1013,7 +988,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1051,7 +1025,6 @@ export type GetNftHistoryQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1094,7 +1067,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1132,7 +1104,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1176,7 +1147,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1214,7 +1184,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1264,7 +1233,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1302,7 +1270,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1339,7 +1306,6 @@ export type GetNftHistoryQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1382,7 +1348,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1420,7 +1385,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1461,7 +1425,6 @@ export type GetNftHistoryQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1503,7 +1466,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1541,7 +1503,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1585,7 +1546,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1623,7 +1583,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1666,7 +1625,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1704,7 +1662,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1742,7 +1699,6 @@ export type GetNftHistoryQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1785,7 +1741,6 @@ export type GetNftHistoryQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1823,7 +1778,6 @@ export type GetNftHistoryQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1901,7 +1855,6 @@ export type GetNftActivitiesQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1933,7 +1886,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1973,7 +1925,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2011,7 +1962,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -2049,7 +1999,6 @@ export type GetNftActivitiesQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -2085,7 +2034,6 @@ export type GetNftActivitiesQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -2116,7 +2064,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2149,7 +2096,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2186,7 +2132,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2224,7 +2169,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -2267,7 +2211,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2305,7 +2248,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -2343,7 +2285,6 @@ export type GetNftActivitiesQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -2373,7 +2314,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2404,7 +2344,6 @@ export type GetNftActivitiesQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -2440,7 +2379,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2478,7 +2416,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -2513,7 +2450,6 @@ export type GetNftActivitiesQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -2549,7 +2485,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2587,7 +2522,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -2637,7 +2571,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2675,7 +2608,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -2713,7 +2645,6 @@ export type GetNftActivitiesQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -2743,7 +2674,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2776,7 +2706,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -2813,7 +2742,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2851,7 +2779,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -2892,7 +2819,6 @@ export type GetNftActivitiesQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -2931,7 +2857,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -2969,7 +2894,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -3000,7 +2924,6 @@ export type GetNftActivitiesQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -3030,7 +2953,6 @@ export type GetNftActivitiesQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -3066,7 +2988,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -3104,7 +3025,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -3139,7 +3059,6 @@ export type GetNftActivitiesQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -3175,7 +3094,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -3213,7 +3131,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -3254,7 +3171,6 @@ export type GetNftActivitiesQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -3284,7 +3200,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -3321,7 +3236,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -3359,7 +3273,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean
@@ -3395,7 +3308,6 @@ export type GetNftActivitiesQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -3432,7 +3344,6 @@ export type GetNftActivitiesQuery = {
                                     __typename?: 'StorageDataObject'
                                     id: string
                                     resolvedUrls: Array<string>
-                                    resolvedUrl?: string | null
                                     createdAt: Date
                                     size: string
                                     isAccepted: boolean
@@ -3470,7 +3381,6 @@ export type GetNftActivitiesQuery = {
                                   __typename?: 'StorageDataObject'
                                   id: string
                                   resolvedUrls: Array<string>
-                                  resolvedUrl?: string | null
                                   createdAt: Date
                                   size: string
                                   isAccepted: boolean

+ 0 - 4
packages/atlas/src/api/queries/__generated__/transactionEvents.generated.tsx

@@ -61,7 +61,6 @@ export type GetMetaprotocolTransactionStatusEventsQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -114,7 +113,6 @@ export type GetMetaprotocolTransactionStatusEventsQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -167,7 +165,6 @@ export type GetMetaprotocolTransactionStatusEventsQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -220,7 +217,6 @@ export type GetMetaprotocolTransactionStatusEventsQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean

+ 0 - 60
packages/atlas/src/api/queries/__generated__/videos.generated.tsx

@@ -47,7 +47,6 @@ export type GetFullVideoQuery = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -66,7 +65,6 @@ export type GetFullVideoQuery = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -109,7 +107,6 @@ export type GetFullVideoQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -133,7 +130,6 @@ export type GetFullVideoQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -152,7 +148,6 @@ export type GetFullVideoQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -208,7 +203,6 @@ export type GetFullVideoQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -232,7 +226,6 @@ export type GetFullVideoQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -265,7 +258,6 @@ export type GetFullVideoQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -327,7 +319,6 @@ export type GetFullVideoQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -369,7 +360,6 @@ export type GetFullVideoQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -406,7 +396,6 @@ export type GetFullVideoQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -444,7 +433,6 @@ export type GetFullVideoQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -500,7 +488,6 @@ export type GetBasicVideosConnectionQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -520,7 +507,6 @@ export type GetBasicVideosConnectionQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -568,7 +554,6 @@ export type GetBasicVideosConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -592,7 +577,6 @@ export type GetBasicVideosConnectionQuery = {
                     __typename?: 'StorageDataObject'
                     id: string
                     resolvedUrls: Array<string>
-                    resolvedUrl?: string | null
                     createdAt: Date
                     size: string
                     isAccepted: boolean
@@ -625,7 +609,6 @@ export type GetBasicVideosConnectionQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -687,7 +670,6 @@ export type GetBasicVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -729,7 +711,6 @@ export type GetBasicVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -766,7 +747,6 @@ export type GetBasicVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -851,7 +831,6 @@ export type GetFullVideosConnectionQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -870,7 +849,6 @@ export type GetFullVideosConnectionQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -913,7 +891,6 @@ export type GetFullVideosConnectionQuery = {
                       __typename?: 'StorageDataObject'
                       id: string
                       resolvedUrls: Array<string>
-                      resolvedUrl?: string | null
                       createdAt: Date
                       size: string
                       isAccepted: boolean
@@ -937,7 +914,6 @@ export type GetFullVideosConnectionQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -956,7 +932,6 @@ export type GetFullVideosConnectionQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -1012,7 +987,6 @@ export type GetFullVideosConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -1036,7 +1010,6 @@ export type GetFullVideosConnectionQuery = {
                     __typename?: 'StorageDataObject'
                     id: string
                     resolvedUrls: Array<string>
-                    resolvedUrl?: string | null
                     createdAt: Date
                     size: string
                     isAccepted: boolean
@@ -1069,7 +1042,6 @@ export type GetFullVideosConnectionQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1131,7 +1103,6 @@ export type GetFullVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1173,7 +1144,6 @@ export type GetFullVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1210,7 +1180,6 @@ export type GetFullVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -1248,7 +1217,6 @@ export type GetFullVideosConnectionQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -1301,7 +1269,6 @@ export type GetBasicVideosQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -1321,7 +1288,6 @@ export type GetBasicVideosQuery = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -1369,7 +1335,6 @@ export type GetBasicVideosQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1393,7 +1358,6 @@ export type GetBasicVideosQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -1426,7 +1390,6 @@ export type GetBasicVideosQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1488,7 +1451,6 @@ export type GetBasicVideosQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1530,7 +1492,6 @@ export type GetBasicVideosQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1567,7 +1528,6 @@ export type GetBasicVideosQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1643,7 +1603,6 @@ export type GetFullVideosQuery = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -1662,7 +1621,6 @@ export type GetFullVideosQuery = {
       __typename?: 'StorageDataObject'
       id: string
       resolvedUrls: Array<string>
-      resolvedUrl?: string | null
       createdAt: Date
       size: string
       isAccepted: boolean
@@ -1705,7 +1663,6 @@ export type GetFullVideosQuery = {
                   __typename?: 'StorageDataObject'
                   id: string
                   resolvedUrls: Array<string>
-                  resolvedUrl?: string | null
                   createdAt: Date
                   size: string
                   isAccepted: boolean
@@ -1729,7 +1686,6 @@ export type GetFullVideosQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -1748,7 +1704,6 @@ export type GetFullVideosQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -1804,7 +1759,6 @@ export type GetFullVideosQuery = {
                           __typename?: 'StorageDataObject'
                           id: string
                           resolvedUrls: Array<string>
-                          resolvedUrl?: string | null
                           createdAt: Date
                           size: string
                           isAccepted: boolean
@@ -1828,7 +1782,6 @@ export type GetFullVideosQuery = {
                 __typename?: 'StorageDataObject'
                 id: string
                 resolvedUrls: Array<string>
-                resolvedUrl?: string | null
                 createdAt: Date
                 size: string
                 isAccepted: boolean
@@ -1861,7 +1814,6 @@ export type GetFullVideosQuery = {
                         __typename?: 'StorageDataObject'
                         id: string
                         resolvedUrls: Array<string>
-                        resolvedUrl?: string | null
                         createdAt: Date
                         size: string
                         isAccepted: boolean
@@ -1923,7 +1875,6 @@ export type GetFullVideosQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -1965,7 +1916,6 @@ export type GetFullVideosQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -2002,7 +1952,6 @@ export type GetFullVideosQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -2040,7 +1989,6 @@ export type GetFullVideosQuery = {
         __typename?: 'StorageDataObject'
         id: string
         resolvedUrls: Array<string>
-        resolvedUrl?: string | null
         createdAt: Date
         size: string
         isAccepted: boolean
@@ -2098,7 +2046,6 @@ export type GetMostViewedVideosConnectionQuery = {
             __typename?: 'StorageDataObject'
             id: string
             resolvedUrls: Array<string>
-            resolvedUrl?: string | null
             createdAt: Date
             size: string
             isAccepted: boolean
@@ -2118,7 +2065,6 @@ export type GetMostViewedVideosConnectionQuery = {
           __typename?: 'StorageDataObject'
           id: string
           resolvedUrls: Array<string>
-          resolvedUrl?: string | null
           createdAt: Date
           size: string
           isAccepted: boolean
@@ -2166,7 +2112,6 @@ export type GetMostViewedVideosConnectionQuery = {
                               __typename?: 'StorageDataObject'
                               id: string
                               resolvedUrls: Array<string>
-                              resolvedUrl?: string | null
                               createdAt: Date
                               size: string
                               isAccepted: boolean
@@ -2190,7 +2135,6 @@ export type GetMostViewedVideosConnectionQuery = {
                     __typename?: 'StorageDataObject'
                     id: string
                     resolvedUrls: Array<string>
-                    resolvedUrl?: string | null
                     createdAt: Date
                     size: string
                     isAccepted: boolean
@@ -2223,7 +2167,6 @@ export type GetMostViewedVideosConnectionQuery = {
                             __typename?: 'StorageDataObject'
                             id: string
                             resolvedUrls: Array<string>
-                            resolvedUrl?: string | null
                             createdAt: Date
                             size: string
                             isAccepted: boolean
@@ -2285,7 +2228,6 @@ export type GetMostViewedVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -2327,7 +2269,6 @@ export type GetMostViewedVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean
@@ -2364,7 +2305,6 @@ export type GetMostViewedVideosConnectionQuery = {
                                 __typename?: 'StorageDataObject'
                                 id: string
                                 resolvedUrls: Array<string>
-                                resolvedUrl?: string | null
                                 createdAt: Date
                                 size: string
                                 isAccepted: boolean

+ 0 - 1
packages/atlas/src/api/queries/fragments.graphql

@@ -91,7 +91,6 @@ fragment FullMembershipFields on Membership {
 fragment StorageDataObjectFields on StorageDataObject {
   id
   resolvedUrls
-  resolvedUrl @client
   createdAt
   size
   isAccepted

+ 1 - 1
packages/atlas/src/components/ActionBar/ActionBar.tsx

@@ -65,7 +65,7 @@ export const ActionBar = forwardRef<HTMLDivElement, ActionBarProps>(
     )
 
     return (
-      <ActionBarContainer ref={ref} className={className} isActive={isActive}>
+      <ActionBarContainer ref={ref} className={`${className} action-bar`} isActive={isActive}>
         {fee && !isNoneCrypto && (
           <FeeContainer>
             <Fee variant={smMatch ? 'h400' : 'h200'} withToken amount={fee || new BN(0)} loading={feeLoading} />

+ 36 - 0
packages/atlas/src/components/AssetImage/AssetImage.tsx

@@ -0,0 +1,36 @@
+import { FC, ImgHTMLAttributes, ReactNode } from 'react'
+import { CSSTransition, SwitchTransition } from 'react-transition-group'
+
+import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader'
+import { useGetAssetUrl } from '@/hooks/useGetAssetUrl'
+import { cVar, transitions } from '@/styles'
+
+export type AssetImageProps = {
+  isLoading?: boolean
+  resolvedUrls: string[] | undefined | null
+  imagePlaceholder?: ReactNode
+} & Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'>
+
+export const AssetImage: FC<AssetImageProps> = ({ resolvedUrls, isLoading, imagePlaceholder, ...imgProps }) => {
+  const { url, isLoading: isResolving } = useGetAssetUrl(resolvedUrls, 'image')
+
+  const loading = isLoading || isResolving
+
+  return (
+    <SwitchTransition>
+      <CSSTransition
+        key={String(loading)}
+        timeout={parseInt(cVar('animationTimingFast', true))}
+        classNames={transitions.names.fade}
+      >
+        {loading ? (
+          <SkeletonLoader className={imgProps.className} />
+        ) : imagePlaceholder && !url ? (
+          <>{imagePlaceholder}</>
+        ) : (
+          <img {...imgProps} src={url} />
+        )}
+      </CSSTransition>
+    </SwitchTransition>
+  )
+}

+ 1 - 0
packages/atlas/src/components/AssetImage/index.ts

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

+ 19 - 0
packages/atlas/src/components/AssetVideo/AssetVideo.tsx

@@ -0,0 +1,19 @@
+import { VideoHTMLAttributes, forwardRef } from 'react'
+
+import { useGetAssetUrl } from '@/hooks/useGetAssetUrl'
+
+export type AssetVideoProps = {
+  resolvedVideoUrls: string[]
+  resolvedPosterUrls: string[]
+} & Omit<VideoHTMLAttributes<HTMLVideoElement>, 'src' | 'poster'>
+
+export const AssetVideo = forwardRef<HTMLVideoElement, AssetVideoProps>(
+  ({ resolvedVideoUrls, resolvedPosterUrls, ...props }: AssetVideoProps, ref) => {
+    const { url: videoSrc } = useGetAssetUrl(resolvedVideoUrls, 'video')
+    const { url: posterSrc } = useGetAssetUrl(resolvedPosterUrls, 'image')
+
+    return <video {...props} ref={ref} src={videoSrc} poster={posterSrc} />
+  }
+)
+
+AssetVideo.displayName = 'AssetVideo'

+ 1 - 1
packages/atlas/src/components/Avatar/Avatar.stories.tsx

@@ -6,7 +6,7 @@ export default {
   title: 'other/Avatar',
   component: Avatar,
   args: {
-    assetUrl: 'https://picsum.photos/200/300',
+    assetUrls: ['https://picsum.photos/200/300'],
   },
 } as Meta<AvatarProps>
 

+ 2 - 22
packages/atlas/src/components/Avatar/Avatar.styles.ts

@@ -4,6 +4,7 @@ import styled from '@emotion/styled'
 
 import { SvgActionAddImage, SvgActionEdit, SvgIllustrativeFileFailed } from '@/assets/icons'
 import { SvgAvatarSilhouette } from '@/assets/illustrations'
+import { AssetImage } from '@/components/AssetImage'
 import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader'
 import { cVar, square, zIndex } from '@/styles'
 
@@ -58,27 +59,6 @@ export const Overlay = styled.div<{ isEdit?: boolean }>`
     `};
 `
 
-// const coverAvatarCss = css`
-//   ${square('64px')};
-
-//   ${media.md} {
-//     ${square('88px')};
-//   }
-// `
-
-// const channelAvatarCss = css`
-//   ${square('88px')};
-//   ${media.md} {
-//     ${square('136px')};
-//   }
-// `
-// const channelCardAvatarCss = css`
-//   ${square('88px')};
-//   ${media.md} {
-//     ${square('104px')};
-//   }
-// `
-
 export const sharedAvatarHoverStyles = (props: { disableHoverDimm?: boolean }) => css`
   ::after {
     border: 1px solid ${cVar('colorBackgroundAlpha')};
@@ -154,7 +134,7 @@ export const StyledSkeletonLoader = styled(SkeletonLoader)`
   left: 0;
 `
 
-export const StyledImage = styled.img`
+export const StyledImage = styled(AssetImage)`
   position: absolute;
   left: 0;
   width: 100%;

+ 11 - 45
packages/atlas/src/components/Avatar/Avatar.tsx

@@ -1,9 +1,6 @@
-import { FC, MouseEvent, PropsWithChildren, useCallback, useEffect } from 'react'
-import { CSSTransition, SwitchTransition } from 'react-transition-group'
+import { FC, MouseEvent, PropsWithChildren, useCallback } from 'react'
 
 import { SvgActionNewChannel } from '@/assets/icons'
-import { cVar, transitions } from '@/styles'
-import { validateImage } from '@/utils/image'
 
 import {
   AvatarSize,
@@ -24,9 +21,8 @@ import { Text } from '../Text'
 
 export type AvatarProps = PropsWithChildren<{
   onClick?: (event: MouseEvent<HTMLElement>) => void
-  onImageValidation?: (validImage: boolean) => void
   onError?: () => void
-  assetUrl?: string | null
+  assetUrls?: string[] | null
   hasAvatarUploadFailed?: boolean
   loading?: boolean
   className?: string
@@ -38,7 +34,7 @@ export type AvatarProps = PropsWithChildren<{
 }>
 
 export const Avatar: FC<AvatarProps> = ({
-  assetUrl,
+  assetUrls,
   hasAvatarUploadFailed,
   loading = false,
   size = 32,
@@ -49,31 +45,10 @@ export const Avatar: FC<AvatarProps> = ({
   clickable,
   onError,
   onClick,
-  onImageValidation,
   disableHoverDimm,
 }) => {
   const isEditable = !loading && editable && size !== 32 && size !== 24
 
-  const checkIfImageIsValid = useCallback(async () => {
-    if (!assetUrl) {
-      onImageValidation?.(true)
-      return
-    }
-    try {
-      await validateImage(assetUrl)
-      onImageValidation?.(true)
-    } catch (error) {
-      onImageValidation?.(false)
-    }
-  }, [assetUrl, onImageValidation])
-
-  useEffect(() => {
-    if (!assetUrl) {
-      return
-    }
-    checkIfImageIsValid()
-  }, [assetUrl, checkIfImageIsValid])
-
   const getEditableIconSize = useCallback(() => {
     const smallIconSizes = [24, 32, 40]
     if (smallIconSizes.includes(size)) {
@@ -96,9 +71,9 @@ export const Avatar: FC<AvatarProps> = ({
     >
       {(clickable || !!onClick) && (
         <IconAndOverlayWrapper>
-          <Overlay isEdit={isEditable && !!assetUrl} />
+          <Overlay isEdit={isEditable && !!assetUrls} />
           {isEditable &&
-            (assetUrl ? (
+            (assetUrls ? (
               <StyledSvgActionEdit width={getEditableIconSize()} height={getEditableIconSize()} />
             ) : (
               <StyledSvgActionAddImage width={getEditableIconSize()} height={getEditableIconSize()} />
@@ -120,21 +95,12 @@ export const Avatar: FC<AvatarProps> = ({
             )}
           </NewChannelAvatar>
         ) : (
-          <SwitchTransition>
-            <CSSTransition
-              key={loading ? 'placeholder' : 'content'}
-              timeout={parseInt(cVar('animationTimingFast', true))}
-              classNames={transitions.names.fade}
-            >
-              {loading ? (
-                <StyledSkeletonLoader rounded />
-              ) : assetUrl ? (
-                <StyledImage src={assetUrl} onError={onError} />
-              ) : (
-                <SilhouetteAvatar />
-              )}
-            </CSSTransition>
-          </SwitchTransition>
+          <StyledImage
+            resolvedUrls={assetUrls}
+            onError={onError}
+            isLoading={loading}
+            imagePlaceholder={<SilhouetteAvatar />}
+          />
         ))}
       {children && (loading ? <StyledSkeletonLoader rounded /> : <ChildrenWrapper>{children}</ChildrenWrapper>)}
     </Container>

+ 4 - 4
packages/atlas/src/components/Avatar/AvatarGroup.stories.tsx

@@ -28,9 +28,9 @@ export const Default = Template.bind({})
 
 Default.args = {
   avatars: [
-    { url: 'https://i.pravatar.cc/300', tooltipText: 'Jane' },
-    { url: 'https://i.pravatar.cc/300', tooltipText: 'John' },
-    { url: 'https://i.pravatar.cc/300', tooltipText: 'William' },
-    { url: 'https://i.pravatar.cc/300', tooltipText: 'One line description' },
+    { urls: ['https://i.pravatar.cc/300'], tooltipText: 'Jane' },
+    { urls: ['https://i.pravatar.cc/300'], tooltipText: 'John' },
+    { urls: ['https://i.pravatar.cc/300'], tooltipText: 'William' },
+    { urls: ['https://i.pravatar.cc/300'], tooltipText: 'One line description' },
   ],
 }

+ 6 - 6
packages/atlas/src/components/Avatar/AvatarGroup.tsx

@@ -22,7 +22,7 @@ type SharedAvatarGroupAvatarProps = PropsWithChildren<{
 
 export type AvatarGroupUrlAvatar = {
   __typename?: 'AvatarGroupUrlAvatar'
-  url?: string | null
+  urls?: string[] | null
 } & SharedAvatarGroupAvatarProps
 
 type AvatarGroupMemberAvatar = BasicMembershipFieldsFragment & SharedAvatarGroupAvatarProps
@@ -102,23 +102,23 @@ type SingleAvatarProps = {
   size: AvatarProps['size']
 }
 const SingleAvatar: FC<SingleAvatarProps> = ({ avatar, loading: loadingProp, size }) => {
-  const { url: memberAvatarUrl, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(
+  const { urls: memberAvatarUrls, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(
     avatar.__typename === 'Membership' ? avatar : null
   )
 
   let loading: boolean
-  let url: string | null | undefined
+  let urls: string[] | null | undefined
   if (avatar.__typename === 'Membership') {
-    url = memberAvatarUrl
+    urls = memberAvatarUrls
     loading = memberAvatarLoading || avatar.loading || loadingProp || false
   } else {
-    url = (avatar as AvatarGroupUrlAvatar).url
+    urls = (avatar as AvatarGroupUrlAvatar).urls
     loading = avatar.loading || loadingProp || false
   }
   return (
     <StyledAvatar
       loading={loading}
-      assetUrl={url}
+      assetUrls={urls}
       size={size}
       disableHoverDimm
       onClick={(e) => {

+ 1 - 0
packages/atlas/src/components/Fee/Fee.tsx

@@ -49,6 +49,7 @@ export const Fee: FC<FeeProps> = ({
             variant={variant}
             color={loading ? 'colorTextMuted' : color}
             withTooltip
+            withDenomination="after"
             format="short"
             margin={{ right: 1 }}
           />

+ 3 - 6
packages/atlas/src/components/MembershipInfo/MembershipInfo.tsx

@@ -14,11 +14,10 @@ import { CopyAddressButton } from '../_buttons/CopyAddressButton/CopyAddressButt
 import { SkeletonLoader } from '../_loaders/SkeletonLoader'
 
 export type MembershipInfoProps = {
-  avatarUrl?: string | null
+  avatarUrls?: string[] | null
   avatarLoading?: boolean
   hasAvatarUploadFailed?: boolean
   onAvatarEditClick?: (event: MouseEvent<HTMLElement>) => void
-  onImageValidation?: (validImage: boolean) => void
   handle?: string | null
   address?: string | null
   loading?: boolean
@@ -29,11 +28,10 @@ export type MembershipInfoProps = {
 
 export const MembershipInfo: FC<MembershipInfoProps> = ({
   address,
-  avatarUrl,
+  avatarUrls,
   avatarLoading,
   hasAvatarUploadFailed,
   onAvatarEditClick,
-  onImageValidation,
   handle,
   loading,
   isOwner,
@@ -54,9 +52,8 @@ export const MembershipInfo: FC<MembershipInfoProps> = ({
             <Avatar
               size={smMatch ? 136 : 88}
               editable={editable}
-              onImageValidation={onImageValidation}
               onClick={onAvatarEditClick}
-              assetUrl={avatarUrl}
+              assetUrls={avatarUrls}
               loading={avatarLoading}
               hasAvatarUploadFailed={hasAvatarUploadFailed}
             />

+ 3 - 1
packages/atlas/src/components/MinimizedPlayer/MinimizedPlayer.tsx

@@ -15,6 +15,7 @@ type MiniVideoProps = {
 export const MinimizedPlayer = forwardRef<HTMLVideoElement, MiniVideoProps>(
   ({ isInView, author, title, ...videoPlayerProps }, ref) => {
     const [forceExit, setForceExit] = useState(false)
+    const [hasError, setHasError] = useState(false)
     const [isPaused, setIsPaused] = useState(false)
     const [wasPausedOnTop, setWasPausedTop] = useState(false)
     const isAllowed = usePersonalDataStore((state) => state.allowMinimizedPleyer)
@@ -27,7 +28,7 @@ export const MinimizedPlayer = forwardRef<HTMLVideoElement, MiniVideoProps>(
       }
     }, [isInView, isPaused])
 
-    const inView = isAllowed && mdMatch && !wasPausedOnTop ? isInView || forceExit : true
+    const inView = isAllowed && mdMatch && !wasPausedOnTop ? isInView || forceExit || hasError : true
 
     return (
       <Wrapper isInView={inView}>
@@ -42,6 +43,7 @@ export const MinimizedPlayer = forwardRef<HTMLVideoElement, MiniVideoProps>(
             setIsPaused(false)
             videoPlayerProps.onPlay?.()
           }}
+          onError={() => setHasError(true)}
           onMinimizedExit={() => setForceExit(true)}
           {...videoPlayerProps}
         />

+ 20 - 17
packages/atlas/src/components/NftCarousel/components/MarketplaceCarouselCard/NftCarouselDetails.tsx

@@ -6,7 +6,7 @@ import { CSSTransition } from 'react-transition-group'
 import { getNftStatus } from '@/api/hooks/nfts'
 import { GetFeaturedNftsVideosQuery } from '@/api/queries/__generated__/nfts.generated'
 import { SvgActionNotForSale } from '@/assets/icons'
-import { AvatarGroup } from '@/components/Avatar/AvatarGroup'
+import { AvatarGroup, AvatarGroupAvatar } from '@/components/Avatar/AvatarGroup'
 import { JoyTokenIcon } from '@/components/JoyTokenIcon'
 import {
   Container,
@@ -43,34 +43,34 @@ export const NftCarouselDetails = ({
   const { convertBlockToMsTimestamp } = useBlockTimeEstimation()
   const nftStatus = getNftStatus(nft, nft?.video)
 
-  const creatorAvatarUrl =
+  const creatorAvatarUrls =
     nft.owner.__typename === 'NftOwnerChannel'
-      ? nft.owner.channel.avatarPhoto?.resolvedUrl
-      : getMemberAvatar(nft.owner.member).url
-  const thumbnailUrl = nft.video.thumbnailPhoto?.resolvedUrl
-  const mediaUrl = nft.video.media?.resolvedUrl
+      ? nft.owner.channel.avatarPhoto?.resolvedUrls
+      : getMemberAvatar(nft.owner.member).urls
+  const thumbnailUrls = nft.video.thumbnailPhoto?.resolvedUrls
+  const mediaUrls = nft.video.media?.resolvedUrls
   const plannedEndDateBlockTimestamp =
     nftStatus?.status === 'auction' &&
     nftStatus.auctionPlannedEndBlock &&
     convertBlockToMsTimestamp(nftStatus.auctionPlannedEndBlock)
-  const isLoading = !thumbnailUrl || !mediaUrl
+  const isLoading = !thumbnailUrls || !mediaUrls
   const name = nft.owner.__typename === 'NftOwnerChannel' ? nft.video.channel.title : nft.owner.member.handle
   const owner = useMemo(
     () =>
       nft?.owner.__typename === 'NftOwnerChannel'
         ? {
             name,
-            assetUrl: creatorAvatarUrl,
+            assetUrls: creatorAvatarUrls,
             onClick: () => navigate(absoluteRoutes.viewer.channel(nft.video.channel.id)),
           }
         : nft?.owner.__typename === 'NftOwnerMember'
         ? {
             name,
-            assetUrl: creatorAvatarUrl,
+            assetUrls: creatorAvatarUrls,
             onClick: () => name && navigate(absoluteRoutes.viewer.member(name)),
           }
         : undefined,
-    [creatorAvatarUrl, name, navigate, nft]
+    [creatorAvatarUrls, name, navigate, nft]
   )
 
   const nftDetails = useMemo(
@@ -83,7 +83,7 @@ export const NftCarouselDetails = ({
           : undefined,
       creator: {
         name: nft?.video.channel.title || undefined,
-        assetUrl: creatorAvatarUrl,
+        assetUrl: creatorAvatarUrls,
         onClick: () => navigate(absoluteRoutes.viewer.channel(nft?.video.channel.id)),
       },
       title: nft.video.title,
@@ -97,20 +97,20 @@ export const NftCarouselDetails = ({
           ? hapiBnToTokenNumber(nftStatus.startingPrice)
           : undefined,
     }),
-    [creatorAvatarUrl, navigate, nft, nftStatus]
+    [creatorAvatarUrls, navigate, nft, nftStatus]
   )
 
   const avatars = useMemo(
-    () => [
+    (): AvatarGroupAvatar[] => [
       {
-        url: nftDetails.creator?.assetUrl,
+        urls: nftDetails.creator?.assetUrl,
         tooltipText: `Creator: ${nftDetails.creator?.name}`,
         onClick: nftDetails.creator?.onClick,
       },
       ...(owner
         ? [
             {
-              url: owner?.assetUrl,
+              urls: owner?.assetUrls,
               tooltipText: `Owner: ${owner?.name}`,
               onClick: owner?.onClick,
             },
@@ -168,8 +168,8 @@ export const NftCarouselDetails = ({
           onPause={() => setIsPaused(true)}
           onPlay={() => setIsPaused(false)}
           preload="auto"
-          src={mediaUrl ?? undefined}
-          poster={thumbnailUrl ?? undefined}
+          src={mediaUrls ?? undefined}
+          poster={thumbnailUrls ?? undefined}
           handleActions={active}
           videoPlaytime={30}
           onEnded={slideNext}
@@ -191,6 +191,7 @@ export const NftCarouselDetails = ({
                     caption="BUY NOW"
                     content={nftDetails.buyNow}
                     icon={<JoyTokenIcon size={smMatch ? 24 : 16} variant="silver" />}
+                    withDenomination
                   />
                 )}
                 {nftDetails.topBid && (
@@ -200,6 +201,7 @@ export const NftCarouselDetails = ({
                     caption="TOP BID"
                     content={nftDetails.topBid}
                     icon={<JoyTokenIcon size={smMatch ? 24 : 16} variant="silver" />}
+                    withDenomination
                   />
                 )}
                 {nftDetails.minBid && (
@@ -209,6 +211,7 @@ export const NftCarouselDetails = ({
                     caption="MIN BID"
                     content={nftDetails.minBid}
                     icon={<JoyTokenIcon size={smMatch ? 24 : 16} variant="silver" />}
+                    withDenomination
                   />
                 )}
                 {nftStatus?.status === 'idle' && (

+ 130 - 12
packages/atlas/src/components/NumberFormat/NumberFormat.tsx

@@ -1,11 +1,14 @@
+import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 import BN from 'bn.js'
-import { forwardRef, useRef } from 'react'
+import { ReactNode, forwardRef, useRef } from 'react'
 import { mergeRefs } from 'react-merge-refs'
 
 import { Text, TextProps, TextVariant } from '@/components/Text'
 import { atlasConfig } from '@/config'
 import { hapiBnToTokenNumber } from '@/joystream-lib/utils'
+import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
+import { sizes } from '@/styles'
 import { formatNumber } from '@/utils/number'
 
 import { Tooltip } from '../Tooltip'
@@ -19,8 +22,13 @@ export type NumberFormatProps = {
   variant?: TextVariant
   displayedValue?: string | number
   isNegative?: boolean
+  icon?: ReactNode
+  withDenomination?: boolean | 'horizontal' | 'vertical' | 'before' | 'after'
+  denominationAlign?: 'left' | 'right'
 } & Omit<TextProps, 'children' | 'variant'>
 
+const TEXT_DENOMINATION_ALIGNMENTS: NumberFormatProps['withDenomination'][] = ['before', 'after']
+
 export const NumberFormat = forwardRef<HTMLHeadingElement, NumberFormatProps>(
   (
     {
@@ -32,25 +40,37 @@ export const NumberFormat = forwardRef<HTMLHeadingElement, NumberFormatProps>(
       displayedValue,
       isNegative,
       color,
+      withDenomination: _withDenomination,
+      denominationAlign = 'left',
+      icon,
       ...textProps
     },
     ref
   ) => {
+    const withDenomination = atlasConfig.joystream.tokenPriceFeedUrl ? _withDenomination : undefined
+    const { convertTokensToUSD } = useTokenPrice()
     const internalValue = BN.isBN(value) ? hapiBnToTokenNumber(value) : value
+    const fiatValue = convertTokensToUSD(internalValue)
     const textRef = useRef<HTMLHeadingElement>(null)
+    const denominationRef = useRef<HTMLHeadingElement>(null)
     const bnValue = new BN(value)
     let formattedValue
+    let formattedDenominatedValue
     let tooltipText
     switch (isNegative || bnValue.isNeg() ? 'full' : format) {
       case 'short':
-        formattedValue = internalValue ? (internalValue > 0.01 ? formatNumberShort(internalValue) : `< 0.01`) : 0
+        formattedValue = internalValue ? (internalValue > 0.01 ? formatNumberShort(internalValue) : `<0.01`) : 0
+        formattedDenominatedValue = fiatValue ? (fiatValue > 0.01 ? formatNumberShort(fiatValue) : `<$0.01`) : 0
         tooltipText = formatNumber(internalValue)
         break
       case 'full':
         formattedValue = tooltipText = formatNumber(internalValue)
+        formattedDenominatedValue = fiatValue ? formatNumber(fiatValue) : 0
         break
       case 'dollar':
         formattedValue = formatDollars(internalValue)
+        formattedDenominatedValue = fiatValue ? formatDollars(fiatValue) : 0
+
         tooltipText = new Intl.NumberFormat('en-US', { maximumSignificantDigits, ...currencyFormatOptions })
           .format(internalValue)
           .replaceAll(',', ' ')
@@ -61,32 +81,130 @@ export const NumberFormat = forwardRef<HTMLHeadingElement, NumberFormatProps>(
     const hasTooltip =
       withTooltip &&
       ((format === 'short' && (internalValue > 999 || hasDecimals)) || (format === 'dollar' && hasDecimals))
+    const shouldShowDenominationTooltip = fiatValue && fiatValue <= 0.01
     const content = (
-      <StyledText
-        {...textProps}
-        color={bnValue.isNeg() || isNegative ? 'colorTextError' : color}
-        variant={variant}
-        ref={mergeRefs([ref, textRef])}
-      >
-        {displayedValue || formattedValue}
-        {withToken && ` ${atlasConfig.joystream.tokenTicker}`}
-      </StyledText>
+      <ContentContainer>
+        {withDenomination === 'before' && (
+          <Text
+            className="denomination"
+            as="span"
+            color={bnValue.isNeg() || isNegative ? 'colorTextError' : 'colorTextMuted'}
+            variant={variant}
+            ref={denominationRef}
+          >
+            ({formattedDenominatedValue !== '<$0.01' ? '$' : ''}
+            {formattedDenominatedValue}){' '}
+          </Text>
+        )}
+        <StyledText
+          {...textProps}
+          color={bnValue.isNeg() || isNegative ? 'colorTextError' : color}
+          variant={variant}
+          ref={mergeRefs([ref, textRef])}
+        >
+          {displayedValue || formattedValue}
+          {withToken && ` ${atlasConfig.joystream.tokenTicker}`}
+        </StyledText>
+        {withDenomination === 'after' && (
+          <Text
+            className="denomination"
+            as="span"
+            color={bnValue.isNeg() || isNegative ? 'colorTextError' : 'colorTextMuted'}
+            variant={variant}
+            ref={denominationRef}
+          >
+            {' '}
+            ({formattedDenominatedValue !== '<$0.01' ? '$' : ''}
+            {formattedDenominatedValue}){' '}
+          </Text>
+        )}
+      </ContentContainer>
     )
 
     return (
       <>
-        {content}
+        {withDenomination ? (
+          <Container orientation={withDenomination}>
+            {icon ? (
+              <IconContainer>
+                {icon}
+                {content}
+              </IconContainer>
+            ) : (
+              content
+            )}
+            {!TEXT_DENOMINATION_ALIGNMENTS.includes(withDenomination) && (
+              <Denomination
+                align={denominationAlign}
+                className="denomination"
+                as="span"
+                color={bnValue.isNeg() || isNegative ? 'colorTextError' : 'colorTextMuted'}
+                variant="t100"
+                ref={denominationRef}
+              >
+                {formattedDenominatedValue !== '<$0.01' ? '$' : ''}
+                {formattedDenominatedValue}
+              </Denomination>
+            )}
+          </Container>
+        ) : icon ? (
+          <IconContainer>
+            {icon}
+            {content}
+          </IconContainer>
+        ) : (
+          content
+        )}
+
         <Tooltip reference={textRef} placement="top" delay={[500, null]} text={hasTooltip ? tooltipText : undefined} />
+        <Tooltip
+          reference={denominationRef}
+          placement="top"
+          delay={[500, null]}
+          text={shouldShowDenominationTooltip ? `$${fiatValue?.toPrecision(2)}` : undefined}
+        />
       </>
     )
   }
 )
 NumberFormat.displayName = 'Number'
 
+export const ContentContainer = styled.div`
+  display: inline-block;
+`
+
 const StyledText = styled(Text)`
   display: inline-block;
 `
 
+const Denomination = styled(Text)<{ align: 'right' | 'left' }>`
+  display: inline-block;
+  text-align: ${(props) => props.align};
+`
+
+const Container = styled.div<{ orientation: NumberFormatProps['withDenomination'] }>`
+  display: flex;
+  flex-direction: column;
+  gap: ${sizes(1)};
+  width: fit-content;
+  ${(props) =>
+    props.orientation === 'horizontal' &&
+    css`
+      width: 100%;
+      flex-direction: row;
+      align-items: center;
+      justify-content: space-between;
+    `}
+`
+
+const IconContainer = styled.div`
+  display: inline-grid;
+  grid-auto-flow: column;
+  grid-auto-columns: max-content;
+  align-items: center;
+  gap: ${sizes(1)};
+`
+
 const maximumSignificantDigits = 21
 
 const currencyFormatOptions = {

+ 1 - 1
packages/atlas/src/components/OutputPill/OutputPill.stories.tsx

@@ -6,7 +6,7 @@ export default {
   title: 'Other/OutputPill',
   component: OutputPill,
   args: {
-    avatarUri: 'https://placedog.net/100/100',
+    avatarUrls: ['https://placedog.net/100/100'],
     handle: 'Member',
     withAvatar: true,
     readonly: false,

+ 3 - 3
packages/atlas/src/components/OutputPill/OutputPill.tsx

@@ -5,7 +5,7 @@ import { Text } from '@/components/Text'
 import { OutputPillWrapper, RemoveButton, StyledAvatar, StyledSVGCloseIcon } from './OutputPill.styles'
 
 export type OutputPillProps = {
-  avatarUri?: string | null
+  avatarUrls?: string[] | null
   handle?: string | null
   onDeleteClick?: () => void
   className?: string
@@ -16,7 +16,7 @@ export type OutputPillProps = {
   onKeyPress?: (event: KeyboardEvent<HTMLButtonElement>) => void
 }
 export const OutputPill: FC<OutputPillProps> = ({
-  avatarUri,
+  avatarUrls,
   handle,
   onDeleteClick,
   className,
@@ -35,7 +35,7 @@ export const OutputPill: FC<OutputPillProps> = ({
 
   return (
     <OutputPillWrapper className={className} withoutButton={!onDeleteClick || readonly}>
-      {withAvatar && <StyledAvatar size={24} assetUrl={avatarUri} loading={isLoadingAvatar} />}
+      {withAvatar && <StyledAvatar size={24} assetUrls={avatarUrls} loading={isLoadingAvatar} />}
       <Text variant="t200" as="p">
         {handle}
       </Text>

+ 1 - 1
packages/atlas/src/components/OwnerPill/OwnerPill.tsx

@@ -7,7 +7,7 @@ import { Avatar } from '../Avatar'
 export type OwnerPillProps = {
   handle?: string
   avatar?: {
-    assetUrl?: string | null
+    assetUrls?: string[] | null
     loading?: boolean
   }
   title?: string

+ 8 - 4
packages/atlas/src/components/Searchbar/SearchBox/Result.tsx

@@ -3,6 +3,7 @@ import { FC, useCallback, useMemo } from 'react'
 import { BasicChannelFieldsFragment, BasicVideoFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
 import { Text } from '@/components/Text'
 import { absoluteRoutes } from '@/config/routes'
+import { useGetAssetUrl } from '@/hooks/useGetAssetUrl'
 
 import { ResultTitle } from './ResultTitle'
 import { ResultWrapper } from './ResultWrapper'
@@ -34,8 +35,11 @@ export const Result: FC<ResultProps> = ({
   loading,
 }) => {
   const title = video ? video.title : channel?.title
-  const channelAvatar = channel?.avatarPhoto?.resolvedUrl
-  const videoThumbnail = video?.thumbnailPhoto?.resolvedUrl
+  const { url: channelAvatar, isLoading: isLoadingAvatar } = useGetAssetUrl(channel?.avatarPhoto?.resolvedUrls, 'image')
+  const { url: videoThumbnail, isLoading: isLoadingThumbnail } = useGetAssetUrl(
+    video?.thumbnailPhoto?.resolvedUrls,
+    'image'
+  )
   const to = useMemo(() => {
     if (video) {
       return absoluteRoutes.viewer.video(video.id)
@@ -58,12 +62,12 @@ export const Result: FC<ResultProps> = ({
   return (
     <ResultWrapper to={to} selected={selected} handleSelectedItem={onSelected} selectedItem={selectedItem}>
       <ResultContent>
-        {loading ? (
+        {loading && (video ? isLoadingThumbnail : isLoadingAvatar) ? (
           <StyledSkeletonLoader width={video ? '64px' : '32px'} height={video ? '40px' : '32px'} rounded={!!channel} />
         ) : channel && !thumbnailUrl ? (
           <StyledSvgAvatarSilhouette width={32} height={32} />
         ) : (
-          <ResultThumbnail src={thumbnailUrl || ''} rounded={!!channel} />
+          <ResultThumbnail src={thumbnailUrl} rounded={!!channel} />
         )}
         <div>
           <Title as="span" color={!selected ? 'colorText' : undefined} variant="t200-strong">

+ 9 - 0
packages/atlas/src/components/Searchbar/Searchbar.styles.ts

@@ -80,6 +80,12 @@ export const InnerContainer = styled.div<{ hasFocus: boolean; hasQuery: boolean
   }
 `
 
+export const StyledButton = styled(Button)`
+  width: 100% !important;
+  height: 100% !important;
+  border-radius: unset !important;
+`
+
 export const StyledSvgOutlineSearch = styled(SvgControlsSearchAlt, { shouldForwardProp: isPropValid })<{
   highlighted?: boolean
 }>`
@@ -104,6 +110,9 @@ export const SearchHelper = styled(Text)`
 export const SearchButton = styled(Button)`
   padding: 0;
   margin: 0 auto;
+  width: 100%;
+  height: 100%;
+  border-radius: unset;
 
   ${media.md} {
     display: none;

+ 4 - 4
packages/atlas/src/components/Section/Section.stories.tsx

@@ -262,7 +262,7 @@ const CarouselTemplate: StoryFn<SectionProps<unknown>> = () => {
               <VideoTile
                 loadingDetails={true}
                 loadingAvatar={true}
-                thumbnailUrl={`http://placekitten.com/g/${320 + idx}/180`}
+                thumbnailUrls={[`http://placekitten.com/g/${320 + idx}/180`]}
               />
             </RankingNumberTile>
           )),
@@ -286,7 +286,7 @@ const CarouselTemplate: StoryFn<SectionProps<unknown>> = () => {
               <VideoTile
                 loadingDetails={true}
                 loadingAvatar={true}
-                thumbnailUrl={`https://place.dog/${320 + idx}/180`}
+                thumbnailUrls={[`https://place.dog/${320 + idx}/180`]}
               />
             </RankingNumberTile>
           )),
@@ -308,7 +308,7 @@ const CarouselTemplate: StoryFn<SectionProps<unknown>> = () => {
           children: placeholderItems.map((_, idx) => (
             <NftTile
               key={idx}
-              thumbnail={{ type: 'video', thumbnailUrl: `https://place.dog/${320 + idx}/180` }}
+              thumbnail={{ type: 'video', thumbnailUrls: [`https://place.dog/${320 + idx}/180`] }}
               title={`Nft number ${idx}`}
             />
           )),
@@ -332,7 +332,7 @@ const CarouselTemplate: StoryFn<SectionProps<unknown>> = () => {
               key={idx}
               loadingDetails={true}
               loadingAvatar={true}
-              thumbnailUrl={`https://place.dog/${320 + idx}/180`}
+              thumbnailUrls={[`https://place.dog/${320 + idx}/180`]}
             />
           )),
         }}

+ 1 - 1
packages/atlas/src/components/Section/SectionHeader/SectionHeader.stories.tsx

@@ -215,7 +215,7 @@ const WithTitleTemplate: StoryFn<SectionHeaderProps<unknown>> = () => {
           nodeStart: {
             type: 'avatar',
             avatarProps: {
-              assetUrl: 'https://placekitten.com/g/200/300',
+              assetUrls: ['https://placekitten.com/g/200/300'],
             },
           },
         }}

+ 3 - 0
packages/atlas/src/components/Table/Table.styles.ts

@@ -43,6 +43,9 @@ export const Th = styled(Text)`
 export const Td = styled(Text)`
   ${cellStyles};
 
+  align-items: start;
+  flex-direction: column;
+  justify-content: center;
   box-shadow: ${cVar('effectDividersBottom')};
 `
 

+ 11 - 14
packages/atlas/src/components/TablePaymentsHistory/TablePaymentsHistory.tsx

@@ -18,7 +18,6 @@ import { formatDateTime } from '@/utils/time'
 
 import {
   DialogText,
-  JoyAmountWrapper,
   JoystreamSvgWrapper,
   SenderItem,
   StyledJoyTokenIcon,
@@ -116,7 +115,7 @@ const Sender = ({ sender }: { sender: PaymentHistory['sender'] }) => {
     }
   )
   const member = memberships?.find((member) => member.controllerAccount === sender)
-  const { url: avatarUrl, isLoadingAsset: avatarLoading } = getMemberAvatar(member)
+  const { urls: avatarUrls, isLoadingAsset: avatarLoading } = getMemberAvatar(member)
 
   if (sender === 'council') {
     return (
@@ -135,7 +134,7 @@ const Sender = ({ sender }: { sender: PaymentHistory['sender'] }) => {
     return (
       <StyledLink to={absoluteRoutes.viewer.member(member.handle)}>
         <SenderItem
-          nodeStart={<Avatar assetUrl={avatarUrl} size={32} loading={avatarLoading} />}
+          nodeStart={<Avatar assetUrls={avatarUrls} size={32} loading={avatarLoading} />}
           label={member?.handle}
           isInteractive={false}
         />
@@ -174,16 +173,14 @@ const Type = ({ type }: { type: PaymentType }) => {
 const TokenAmount = ({ tokenAmount }: { tokenAmount: BN }) => {
   const isNegative = tokenAmount.isNeg()
   return (
-    <JoyAmountWrapper>
-      <StyledJoyTokenIcon variant="gray" error={isNegative} />
-      <StyledNumberFormat
-        variant="t200-strong"
-        as="p"
-        value={tokenAmount}
-        margin={{ left: 1 }}
-        format="short"
-        color={isNegative ? 'colorTextError' : 'colorTextStrong'}
-      />
-    </JoyAmountWrapper>
+    <StyledNumberFormat
+      icon={<StyledJoyTokenIcon variant="gray" error={isNegative} />}
+      variant="t200-strong"
+      as="p"
+      value={tokenAmount}
+      format="short"
+      color={isNegative ? 'colorTextError' : 'colorTextStrong'}
+      withDenomination
+    />
   )
 }

+ 4 - 2
packages/atlas/src/components/TopSellingChannelsTable/TopSellingChannelsTable.tsx

@@ -100,13 +100,15 @@ export const TopSellingChannelsTable = () => {
           ),
           salesVolume: (
             <JoyAmountWrapper>
-              <JoyTokenIcon variant="gray" />
               <NumberFormat
+                icon={<JoyTokenIcon variant="gray" />}
                 variant="t200-strong"
                 as="p"
                 value={new BN(data.amount)}
                 margin={{ left: 1 }}
                 format="short"
+                withDenomination
+                denominationAlign="right"
               />
             </JoyAmountWrapper>
           ),
@@ -199,7 +201,7 @@ const Channel = ({ channel }: { channel: BasicChannelFieldsFragment }) => {
   return (
     <StyledLink to={absoluteRoutes.viewer.channel(channel.id)} title={channel.title || ''}>
       <StyledListItem
-        nodeStart={<Avatar assetUrl={channel.avatarPhoto?.resolvedUrl ?? undefined} />}
+        nodeStart={<Avatar assetUrls={channel.avatarPhoto?.resolvedUrls ?? undefined} />}
         label={channel.title}
         isInteractive={false}
         nodeEnd={

+ 1 - 1
packages/atlas/src/components/WidgetTile/WidgetTile.styles.ts

@@ -24,7 +24,7 @@ export const Content = styled.div`
   align-self: flex-end;
   gap: ${sizes(4)};
 
-  ${media.lg} {
+  ${media.md} {
     gap: ${sizes(6)};
     grid-template-columns: 1fr auto;
     align-items: center;

+ 5 - 1
packages/atlas/src/components/_auth/SignInModal/SignInModal.tsx

@@ -8,6 +8,7 @@ import { Button } from '@/components/_buttons/Button'
 import { DialogButtonProps } from '@/components/_overlays/Dialog'
 import { atlasConfig } from '@/config'
 import { FAUCET_URL } from '@/config/env'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { MemberId } from '@/joystream-lib/types'
 import { hapiBnToTokenNumber } from '@/joystream-lib/utils'
 import { useJoystream } from '@/providers/joystream/joystream.hooks'
@@ -56,6 +57,7 @@ export const SignInModal: FC = () => {
   )
 
   const { displaySnackbar } = useSnackbar()
+  const { identifyUser } = useSegmentAnalytics()
   const { walletStatus, refetchUserMemberships, setActiveUser, isLoggedIn } = useUser()
   const { signInModalOpen, setSignInModalOpen } = useUserStore(
     (state) => ({ signInModalOpen: state.signInModalOpen, setSignInModalOpen: state.actions.setSignInModalOpen }),
@@ -114,6 +116,7 @@ export const SignInModal: FC = () => {
         captchaToken: data.captchaToken,
       }
       const response = await faucetMutation(body)
+
       return response.data
     },
     [avatarMutation, faucetMutation]
@@ -131,6 +134,7 @@ export const SignInModal: FC = () => {
           const lastCreatedMembership = data.memberships[data.memberships.length - 1]
           if (lastCreatedMembership) {
             setActiveUser({ accountId: selectedAddress, memberId: lastCreatedMembership.id, channelId: null })
+            identifyUser(lastCreatedMembership.id)
             displaySnackbar({
               title: 'Your membership has been created',
               description: 'Browse, watch, create, collect videos across the platform and have fun!',
@@ -218,11 +222,11 @@ export const SignInModal: FC = () => {
       displaySnackbar,
       goToNextStep,
       goToPreviousStep,
+      identifyUser,
       joystream,
       refetchUserMemberships,
       selectedAddress,
       setActiveUser,
-      setPreviouslyFailedData,
       setSignInModalOpen,
     ]
   )

+ 1 - 1
packages/atlas/src/components/_auth/SignInModal/SignInSteps/SignInModalMembershipStep.tsx

@@ -148,7 +148,7 @@ export const SignInModalMembershipStep: FC<SignInModalMembershipStepProps> = ({
                       !!imageInputFile?.blob
                     )
                   }
-                  assetUrl={imageInputFile?.url}
+                  assetUrls={imageInputFile?.url ? [imageInputFile.url] : undefined}
                   editable
                 />
                 <ImageCropModal

+ 1 - 1
packages/atlas/src/components/_channel/ChannelCard/ChannelCard.tsx

@@ -46,7 +46,7 @@ export const ChannelCard: FC<ChannelCardProps> = ({
   return (
     <ChannelCardArticle className={className} activeDisabled={activeDisabled}>
       <ChannelCardAnchor onClick={onClick} to={channel?.id ? absoluteRoutes.viewer.channel(channel.id) : ''}>
-        <StyledAvatar loading={loading} assetUrl={channel?.avatarPhoto?.resolvedUrl} />
+        <StyledAvatar loading={loading} assetUrls={channel?.avatarPhoto?.resolvedUrls} />
         <SwitchTransition>
           <CSSTransition
             key={loading ? 'placeholder' : 'content'}

+ 2 - 2
packages/atlas/src/components/_channel/ChannelCover/ChannelCover.stories.tsx

@@ -9,7 +9,7 @@ export default {
     editable: { table: { required: false } },
   },
   args: {
-    assetUrl: 'https://eu-central-1.linodeobjects.com/atlas-assets/channel-posters/2.jpg',
+    assetUrls: ['https://eu-central-1.linodeobjects.com/atlas-assets/channel-posters/2.jpg'],
   },
 } as Meta<ChannelCoverProps>
 
@@ -30,5 +30,5 @@ export const Default = Template.bind({})
 
 export const WithNoImage = Template.bind({})
 WithNoImage.args = {
-  assetUrl: undefined,
+  assetUrls: undefined,
 }

+ 2 - 1
packages/atlas/src/components/_channel/ChannelCover/ChannelCover.styles.ts

@@ -2,6 +2,7 @@ import styled from '@emotion/styled'
 
 import { SvgIllustrativeFileFailed } from '@/assets/icons'
 import { SvgBgPattern } from '@/assets/illustrations'
+import { AssetImage } from '@/components/AssetImage'
 import { Button } from '@/components/_buttons/Button'
 import { cVar, media, sizes, transitions, zIndex } from '@/styles'
 
@@ -21,7 +22,7 @@ export const Media = styled.div`
   overflow: hidden;
 `
 
-export const CoverImage = styled.img`
+export const CoverImage = styled(AssetImage)`
   width: 100%;
   position: absolute;
   top: 0;

+ 10 - 17
packages/atlas/src/components/_channel/ChannelCover/ChannelCover.tsx

@@ -1,9 +1,7 @@
 import { FC, MouseEvent } from 'react'
-import { CSSTransition, TransitionGroup } from 'react-transition-group'
 
 import { SvgActionImage, SvgActionImageFile } from '@/assets/icons'
 import { Text } from '@/components/Text'
-import { transitions } from '@/styles'
 
 import {
   CoverImage,
@@ -19,7 +17,7 @@ import {
 } from './ChannelCover.styles'
 
 export type ChannelCoverProps = {
-  assetUrl?: string | null
+  assetUrls?: string[] | null
   hasCoverUploadFailed?: boolean
   editable?: boolean
   disabled?: boolean
@@ -27,7 +25,7 @@ export type ChannelCoverProps = {
 }
 
 export const ChannelCover: FC<ChannelCoverProps> = ({
-  assetUrl,
+  assetUrls,
   hasCoverUploadFailed,
   editable,
   disabled,
@@ -41,22 +39,17 @@ export const ChannelCover: FC<ChannelCoverProps> = ({
             <EditCoverDesktopOverlay onClick={onCoverEditClick}>
               <SvgActionImage />
               <Text as="span" variant="t200-strong" margin={{ top: 1 }} color="colorCoreNeutral100">{`${
-                assetUrl ? 'Edit ' : 'Add '
+                assetUrls ? 'Edit ' : 'Add '
               } cover image`}</Text>
             </EditCoverDesktopOverlay>
             <EditCoverMobileButton icon={<SvgActionImageFile />} onClick={onCoverEditClick} variant="tertiary" />
           </EditableControls>
         )}
         <Media>
-          <TransitionGroup>
-            <CSSTransition
-              key={assetUrl ? 'cover' : 'pattern'}
-              timeout={parseInt(transitions.timings.loading)}
-              classNames={transitions.names.fade}
-            >
-              {assetUrl ? (
-                <CoverImage src={assetUrl} />
-              ) : hasCoverUploadFailed ? (
+          <CoverImage
+            resolvedUrls={assetUrls}
+            imagePlaceholder={
+              hasCoverUploadFailed ? (
                 <FailedUploadContainer>
                   <StyledSvgIllustrativeFileFailed />
                   <Text as="span" variant="t100" color="colorText">
@@ -65,9 +58,9 @@ export const ChannelCover: FC<ChannelCoverProps> = ({
                 </FailedUploadContainer>
               ) : (
                 <StyledBackgroundPattern />
-              )}
-            </CSSTransition>
-          </TransitionGroup>
+              )
+            }
+          />
         </Media>
       </MediaWrapper>
     </CoverWrapper>

+ 1 - 1
packages/atlas/src/components/_channel/ChannelLink/ChannelLink.tsx

@@ -70,7 +70,7 @@ export const ChannelLink: FC<ChannelLinkProps> = ({
             withHandle={!hideHandle}
             loading={!displayedChannel}
             size={avatarSize}
-            assetUrl={displayedChannel?.channel.avatarPhoto?.resolvedUrl}
+            assetUrls={displayedChannel?.channel.avatarPhoto?.resolvedUrls}
           />
         </StyledLink>
       )}

+ 1 - 1
packages/atlas/src/components/_channel/ChannelWithVideos/ChannelWithVideos.tsx

@@ -77,7 +77,7 @@ export const ChannelWithVideos: FC<ChannelWithVideosProps> = memo(({ channelId }
         <StyledAvatar
           size={mdMatch ? 136 : 88}
           loading={isLoading}
-          assetUrl={extendedChannel?.channel.avatarPhoto?.resolvedUrl}
+          assetUrls={extendedChannel?.channel.avatarPhoto?.resolvedUrls}
         />
         <InfoWrapper>
           {isLoading ? (

+ 4 - 4
packages/atlas/src/components/_channel/CollectorsBox/CollectorsBox.stories.tsx

@@ -45,19 +45,19 @@ export const Default = Template.bind({})
 export const WithLessThan5Collectors = Template.bind({})
 WithLessThan5Collectors.args = {
   collectors: [
-    { url: 'https://thispersondoesnotexist.com/image', tooltipText: 'William', nftsAmount: 6 },
+    { urls: ['https://thispersondoesnotexist.com/image'], tooltipText: 'William', nftsAmount: 6 },
     {
-      url: 'https://eu-central-1.linodeobjects.com/atlas-assets/channel-posters/2.jpg',
+      urls: ['https://eu-central-1.linodeobjects.com/atlas-assets/channel-posters/2.jpg'],
       tooltipText: 'Someone',
       nftsAmount: 1,
     },
-    { url: 'https://thispersondoesnotexist.com/image', tooltipText: 'Someone else', nftsAmount: 7 },
+    { urls: ['https://thispersondoesnotexist.com/image'], tooltipText: 'Someone else', nftsAmount: 7 },
   ],
 }
 
 const TemplateWithChannelCover: StoryFn<CollectorsBoxProps> = (args) => (
   <div style={{ position: 'relative' }}>
-    <ChannelCover assetUrl="https://eu-central-1.linodeobjects.com/atlas-assets/channel-posters/2.jpg" />
+    <ChannelCover assetUrls={['https://eu-central-1.linodeobjects.com/atlas-assets/channel-posters/2.jpg']} />
     <div style={{ position: 'absolute', bottom: -32, right: 0 }}>
       <CollectorsBox {...args} />
     </div>

+ 1 - 1
packages/atlas/src/components/_channel/CollectorsBox/CollectorsBox.tsx

@@ -67,7 +67,7 @@ export const CollectorsBox: FC<CollectorsBoxProps> = ({ collectors, maxShowedCol
         {mappedCollectors.map((collector, idx) => (
           <StyledLink key={idx} to={collector.memberUrl || ''}>
             <ListItem
-              nodeStart={<Avatar size={40} assetUrl={collector.url} />}
+              nodeStart={<Avatar size={40} assetUrls={collector.urls} />}
               nodeEnd={
                 <Text as="span" variant="t100" color="colorText">
                   Owns {collector.nftsAmount}

+ 3 - 3
packages/atlas/src/components/_comments/Comment/Comment.tsx

@@ -65,7 +65,7 @@ export const Comment: FC<CommentProps> = memo(
         skip: !commentId,
       }
     )
-    const { isLoadingAsset: isMemberAvatarLoading, url: memberAvatarUrl } = getMemberAvatar(activeMembership)
+    const { isLoadingAsset: isMemberAvatarLoading, urls: memberAvatarUrls } = getMemberAvatar(activeMembership)
 
     const commentIdQueryParam = useRouterQuery(QUERY_PARAMS.COMMENT_ID)
     const reactionPopoverDismissed = usePersonalDataStore((state) => state.reactionPopoverDismissed)
@@ -280,7 +280,7 @@ export const Comment: FC<CommentProps> = memo(
           processing={editCommentInputIsProcessing}
           readOnly={!memberId}
           memberHandle={activeMembership?.handle}
-          memberAvatarUrl={memberAvatarUrl}
+          memberAvatarUrls={memberAvatarUrls}
           isMemberAvatarLoading={isMemberAvatarLoading}
           value={editCommentInputText}
           hasInitialValueChanged={comment?.text !== editCommentInputText}
@@ -334,7 +334,7 @@ export const Comment: FC<CommentProps> = memo(
               ref={replyCommentInputRef}
               fee={replyCommentFee}
               feeLoading={replyCommentFeeLoading}
-              memberAvatarUrl={memberAvatarUrl}
+              memberAvatarUrls={memberAvatarUrls}
               isMemberAvatarLoading={isMemberAvatarLoading}
               processing={replyCommentInputIsProcessing}
               readOnly={!memberId}

+ 3 - 3
packages/atlas/src/components/_comments/Comment/InternalComment.tsx

@@ -115,11 +115,11 @@ export const InternalComment: FC<InternalCommentProps> = ({
 
   const popoverRef = useRef<PopoverImperativeHandle>(null)
   const isTouchDevice = useTouchDevice()
-  const { url: memberAvatarUrl, isLoadingAsset: isMemberAvatarLoading } = getMemberAvatar(author)
+  const { urls: memberAvatarUrls, isLoadingAsset: isMemberAvatarLoading } = getMemberAvatar(author)
   const filteredDuplicatedAvatars = repliesCount
     ? replyAvatars
       ? [...new Map(replyAvatars?.map((item) => [item.handle, item])).values()]
-      : createPlaceholderData(repliesCount, { url: undefined })
+      : createPlaceholderData(repliesCount, { urls: undefined })
     : []
 
   const tooltipDate = createdAt ? `${formatDate(createdAt || new Date())} at ${format(createdAt, 'HH:mm')}` : undefined
@@ -185,7 +185,7 @@ export const InternalComment: FC<InternalCommentProps> = ({
       highlighted={highlighted}
       isMemberAvatarLoading={loading || isMemberAvatarLoading}
       memberUrl={memberUrl}
-      memberAvatarUrl={memberAvatarUrl}
+      memberAvatarUrls={memberAvatarUrls}
       onMouseEnter={() => setCommentHover(true)}
       onMouseLeave={() => setCommentHover(false)}
     >

+ 2 - 2
packages/atlas/src/components/_comments/CommentEditHistory/CommentEditHistory.tsx

@@ -18,7 +18,7 @@ type CommentEditHistoryProps = {
 
 export const CommentEditHistory: FC<CommentEditHistoryProps> = ({ originalComment }) => {
   const { commentEdits, loading } = useCommentEdits(originalComment?.id)
-  const { url: memberAvatarUrl, isLoadingAsset } = getMemberAvatar(originalComment?.author)
+  const { urls: memberAvatarUrls, isLoadingAsset } = getMemberAvatar(originalComment?.author)
 
   const placeholderItems = createPlaceholderData(3)
 
@@ -47,7 +47,7 @@ export const CommentEditHistory: FC<CommentEditHistoryProps> = ({ originalCommen
                   memberUrl={absoluteRoutes.viewer.member(originalComment?.author?.handle)}
                   text={commentEdit.data.text}
                   loading={loading}
-                  memberAvatarUrl={memberAvatarUrl || ''}
+                  memberAvatarUrls={memberAvatarUrls}
                   isMemberAvatarLoading={isLoadingAsset}
                 />
               )

+ 4 - 4
packages/atlas/src/components/_comments/CommentRow/CommentRow.tsx

@@ -11,7 +11,7 @@ export type CommentRowProps = PropsWithChildren<{
   processing?: boolean
   highlighted?: boolean
   isMemberAvatarLoading?: boolean
-  memberAvatarUrl?: string | null
+  memberAvatarUrls?: string[] | null
   memberUrl?: string
   className?: string
   isInput?: boolean
@@ -24,7 +24,7 @@ export const CommentRow: FC<CommentRowProps> = ({
   processing,
   highlighted,
   children,
-  memberAvatarUrl,
+  memberAvatarUrls,
   isMemberAvatarLoading,
   memberUrl = '',
   className,
@@ -61,10 +61,10 @@ export const CommentRow: FC<CommentRowProps> = ({
         <div>
           {memberUrl ? (
             <Link to={memberUrl}>
-              <Avatar assetUrl={memberAvatarUrl} size={avatarSize} loading={isMemberAvatarLoading} clickable />
+              <Avatar assetUrls={memberAvatarUrls} size={avatarSize} loading={isMemberAvatarLoading} clickable />
             </Link>
           ) : (
-            <Avatar assetUrl={memberAvatarUrl} size={avatarSize} loading={isMemberAvatarLoading} />
+            <Avatar assetUrls={memberAvatarUrls} size={avatarSize} loading={isMemberAvatarLoading} />
           )}
         </div>
         <div className={className}>{children}</div>

+ 3 - 3
packages/atlas/src/components/_comments/CommentSnapshot/CommentSnaphsot.tsx

@@ -21,7 +21,7 @@ import { CommentBody } from '../CommentBody'
 
 export type CommentSnapshotProps = {
   isMemberAvatarLoading?: boolean
-  memberAvatarUrl?: string
+  memberAvatarUrls?: string[] | null
   memberUrl?: string
   memberHandle?: string
   loading?: boolean
@@ -31,7 +31,7 @@ export type CommentSnapshotProps = {
 }
 
 export const CommentSnapshot: FC<CommentSnapshotProps> = ({
-  memberAvatarUrl,
+  memberAvatarUrls,
   isMemberAvatarLoading,
   memberHandle,
   loading,
@@ -44,7 +44,7 @@ export const CommentSnapshot: FC<CommentSnapshotProps> = ({
     <ContentWrapper>
       <AvatarWrapper>
         <Link to={memberUrl}>
-          <Avatar assetUrl={memberAvatarUrl} size={40} loading={isMemberAvatarLoading} clickable />
+          <Avatar assetUrls={memberAvatarUrls} size={40} loading={isMemberAvatarLoading} clickable />
         </Link>
         {!last && <Line />}
       </AvatarWrapper>

+ 18 - 18
packages/atlas/src/components/_inputs/ComboBox/ComboBox.stories.tsx

@@ -13,77 +13,77 @@ const MEMBERS = [
   {
     id: 1,
     label: 'Klaudiusz',
-    thumbnailUrl: 'https://placedog.net/57/32?random=1',
+    thumbnailUrls: ['https://placedog.net/57/32?random=1'],
   },
   {
     id: 2,
     label: 'Diego',
-    thumbnailUrl: 'https://placedog.net/57/32?random=2',
+    thumbnailUrls: ['https://placedog.net/57/32?random=2'],
   },
   {
     id: 3,
     label: 'Rafal',
-    thumbnailUrl: 'https://placedog.net/57/32?random=3',
+    thumbnailUrls: ['https://placedog.net/57/32?random=3'],
   },
   {
     id: 3,
     label: 'Loic',
-    thumbnailUrl: 'https://placedog.net/57/32?random=4',
+    thumbnailUrls: ['https://placedog.net/57/32?random=4'],
   },
   {
     id: 4,
     label: 'Bartosz',
-    thumbnailUrl: 'https://placedog.net/57/32?random=5',
+    thumbnailUrls: ['https://placedog.net/57/32?random=5'],
   },
   {
     id: 5,
     label: 'Klaudiusz the Second',
-    thumbnailUrl: 'https://placedog.net/57/32?random=6',
+    thumbnailUrls: ['https://placedog.net/57/32?random=6'],
   },
   {
     id: 6,
     label: 'Diego the Second',
-    thumbnailUrl: 'https://placedog.net/57/32?random=7',
+    thumbnailUrls: ['https://placedog.net/57/32?random=7'],
   },
   {
     id: 7,
     label: 'Rafal the Second',
-    thumbnailUrl: 'https://placedog.net/57/32?random=8',
+    thumbnailUrls: ['https://placedog.net/57/32?random=8'],
   },
   {
     id: 8,
     label: 'Loic the Second',
-    thumbnailUrl: 'https://placedog.net/57/32?random=9',
+    thumbnailUrls: ['https://placedog.net/57/32?random=9'],
   },
   {
     id: 9,
     label: 'Bartosz the Second',
-    thumbnailUrl: 'https://placedog.net/57/32?random=10',
+    thumbnailUrls: ['https://placedog.net/57/32?random=10'],
   },
   {
     id: 10,
     label: 'Klaudiusz the Third',
-    thumbnailUrl: 'https://placedog.net/57/32?random=11',
+    thumbnailUrls: ['https://placedog.net/57/32?random=11'],
   },
   {
     id: 11,
     label: 'Diego the Third',
-    thumbnailUrl: 'https://placedog.net/57/32?random=12',
+    thumbnailUrls: ['https://placedog.net/57/32?random=12'],
   },
   {
     id: 12,
     label: 'Rafal the Third',
-    thumbnailUrl: 'https://placedog.net/57/32?random=13',
+    thumbnailUrls: ['https://placedog.net/57/32?random=13'],
   },
   {
     id: 13,
     label: 'Loic the Third',
-    thumbnailUrl: 'https://placedog.net/57/32?random=14',
+    thumbnailUrls: ['https://placedog.net/57/32?random=14'],
   },
   {
     id: 14,
     label: 'Bartosz the Third',
-    thumbnailUrl: 'https://placedog.net/57/32?random=15',
+    thumbnailUrls: ['https://placedog.net/57/32?random=15'],
   },
 ]
 
@@ -134,7 +134,7 @@ const Template: StoryFn<ComboBoxProps> = (args) => {
 
 export const Default = Template.bind({})
 
-type Member = { label: string; thumbnailUrl: string; id: string }
+type Member = { label: string; thumbnailUrls: string[]; id: string }
 const TemplateWithMembers: StoryFn<ComboBoxProps> = (args) => {
   const [selectedMembers, setSelectedMembers] = useState<Member[]>([])
   const [focusedElement, setFocusedElement] = useState<number | null>(null)
@@ -167,7 +167,7 @@ const TemplateWithMembers: StoryFn<ComboBoxProps> = (args) => {
         items={dropdownItems.map((member) => ({
           id: member.id,
           label: member.label,
-          thumbnailUrl: '',
+          thumbnailUrls: [],
         }))}
         resetOnSelect
       />
@@ -175,7 +175,7 @@ const TemplateWithMembers: StoryFn<ComboBoxProps> = (args) => {
         {selectedMembers.map((member, idx) => (
           <OutputPill
             withAvatar
-            avatarUri={member.thumbnailUrl}
+            avatarUrls={member.thumbnailUrls}
             key={member.id}
             handle={member.label}
             onDeleteClick={() => handleDeleteMember(member.id)}

+ 2 - 1
packages/atlas/src/components/_inputs/ComboBox/ComboBox.styles.ts

@@ -1,6 +1,7 @@
 import styled from '@emotion/styled'
 
 import { SvgActionPlus } from '@/assets/icons'
+import { AssetImage } from '@/components/AssetImage'
 import { cVar, sizes, zIndex } from '@/styles'
 
 export const ComboBoxWrapper = styled.div`
@@ -19,7 +20,7 @@ export const ListWrapper = styled.ul<{ isOpen: boolean }>`
   margin: 0;
 `
 
-export const StyledThumbnail = styled.img`
+export const StyledThumbnail = styled(AssetImage)`
   max-height: 32px;
 `
 

+ 4 - 2
packages/atlas/src/components/_inputs/ComboBox/ComboBox.tsx

@@ -14,7 +14,7 @@ import { Input, InputProps } from '../Input'
 
 type ModifiedListItemProps = ListItemProps & {
   label: string
-  thumbnailUrl?: string
+  thumbnailUrls?: string[]
   isSeparator?: boolean
 }
 
@@ -134,7 +134,9 @@ export const ComboBox = <T extends unknown>(props: ComboBoxProps<T>) => {
                     })}
                     size="large"
                     highlight={highlightedIndex === index}
-                    nodeStart={item.nodeStart || (item.thumbnailUrl && <StyledThumbnail src={item.thumbnailUrl} />)}
+                    nodeStart={
+                      item.nodeStart || (item.thumbnailUrls && <StyledThumbnail resolvedUrls={item.thumbnailUrls} />)
+                    }
                     isSeparator={item.isSeparator}
                   />
                 ))}

+ 1 - 1
packages/atlas/src/components/_inputs/Input/Input.stories.tsx

@@ -60,7 +60,7 @@ const TemplateWithPreffixAndSuffix: StoryFn<InputProps> = (args) => {
       <Input {...args} nodeStart={<Pill label="label" />} nodeEnd="$" />
       <Input
         {...args}
-        nodeStart={<Avatar size={24} assetUrl="https://placedog.net/360/203" />}
+        nodeStart={<Avatar size={24} assetUrls={['https://placedog.net/360/203']} />}
         nodeEnd={<Pill label="500$" />}
       />
       <Input

+ 4 - 4
packages/atlas/src/components/_inputs/MemberComboBox/MemberComboBox.tsx

@@ -137,8 +137,8 @@ type AvatarWithResolvedAssetProps = {
 }
 
 const AvatarWithResolvedAsset: FC<AvatarWithResolvedAssetProps> = ({ member }) => {
-  const { url, isLoadingAsset } = getMemberAvatar(member)
-  return <Avatar size={32} assetUrl={url} loading={isLoadingAsset} />
+  const { urls, isLoadingAsset } = getMemberAvatar(member)
+  return <Avatar size={32} assetUrls={urls} loading={isLoadingAsset} />
 }
 
 type StyledOutputPillWithResolvedAssetProps = {
@@ -154,12 +154,12 @@ const StyledOutputPillWithResolvedAsset: FC<StyledOutputPillWithResolvedAssetPro
   onKeyPress,
   focused,
 }) => {
-  const { url, isLoadingAsset } = getMemberAvatar(member)
+  const { urls, isLoadingAsset } = getMemberAvatar(member)
   return (
     <StyledOutputPill
       handle={member.handle}
       onDeleteClick={onDeleteClick}
-      avatarUri={url}
+      avatarUrls={urls}
       isLoadingAvatar={isLoadingAsset}
       withAvatar
       onKeyPress={onKeyPress}

+ 2 - 2
packages/atlas/src/components/_inputs/SubtitlesBox/SubtitlesBox.tsx

@@ -8,6 +8,7 @@ import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
 import { ContextMenu } from '@/components/_overlays/ContextMenu'
 import { atlasConfig } from '@/config'
+import { useGetAssetUrl } from '@/hooks/useGetAssetUrl'
 import { useConfirmationModal } from '@/providers/confirmationModal'
 import { SubtitlesInput } from '@/types/subtitles'
 
@@ -42,8 +43,7 @@ export const SubtitlesBox: FC<SubtitleBoxProps> = ({
   const { mutateAsync: subtitlesFetch } = useMutation('subtitles-fetch', (url: string) =>
     axios.get(url, { responseType: 'blob' })
   )
-
-  const url = asset?.resolvedUrl
+  const { url } = useGetAssetUrl(asset?.resolvedUrls, 'subtitle')
 
   const handleDownload = async (url = '') => {
     const response = await subtitlesFetch(url)

+ 1 - 1
packages/atlas/src/components/_navigation/SidenavViewer/FollowedChannels.tsx

@@ -44,7 +44,7 @@ export const ChannelNavItem: FC<NavItemProps & ChannelNavItemProps> = ({
 
   return (
     <NavItem to={to} expanded={expanded} itemName={itemName} onClick={onClick} isSecondary={isSecondary}>
-      <Avatar loading={!extendedChannel} size={32} assetUrl={extendedChannel?.channel.avatarPhoto?.resolvedUrl} />
+      <Avatar loading={!extendedChannel} size={32} assetUrls={extendedChannel?.channel.avatarPhoto?.resolvedUrls} />
       {extendedChannel ? (
         <ChannelTitle as="p" variant="h300" color="colorText">
           {extendedChannel.channel.title}

+ 5 - 5
packages/atlas/src/components/_navigation/TopbarStudio/TopbarStudio.tsx

@@ -31,9 +31,9 @@ export const TopbarStudio: FC<StudioTopbarProps> = ({ hideChannelInfo, isMembers
 
   const currentChannel = activeMembership?.channels.find((channel) => channel.id === channelId)
 
-  const channelAvatarUrl = currentChannel?.avatarPhoto?.resolvedUrl
+  const channelAvatarUrls = currentChannel?.avatarPhoto?.resolvedUrls
 
-  const { url: memberAvatarUrl, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(activeMembership)
+  const { urls: memberAvatarUrls, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(activeMembership)
 
   const [isMemberDropdownActive, setIsMemberDropdownActive] = useState(false)
 
@@ -53,13 +53,13 @@ export const TopbarStudio: FC<StudioTopbarProps> = ({ hideChannelInfo, isMembers
   const avatars: AvatarGroupUrlAvatar[] = channelId
     ? [
         {
-          url: memberAvatarUrl,
+          urls: memberAvatarUrls,
           loading: memberAvatarLoading,
           onClick: handleDrawerToggle,
         },
-        { url: channelAvatarUrl, loading: isAuthLoading, onClick: handleDrawerToggle },
+        { urls: channelAvatarUrls, loading: isAuthLoading, onClick: handleDrawerToggle },
       ]
-    : [{ url: memberAvatarUrl, loading: memberAvatarLoading, onClick: handleDrawerToggle }]
+    : [{ urls: memberAvatarUrls, loading: memberAvatarLoading, onClick: handleDrawerToggle }]
 
   return (
     <>

+ 3 - 3
packages/atlas/src/components/_navigation/TopbarViewer/TopbarViewer.tsx

@@ -33,7 +33,7 @@ export const TopbarViewer: FC = () => {
   const { isLoggedIn, activeMembership, signIn, isAuthLoading } = useUser()
   const [isMemberDropdownActive, setIsMemberDropdownActive] = useState(false)
 
-  const { url: memberAvatarUrl, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(activeMembership)
+  const { urls: memberAvatarUrls, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(activeMembership)
 
   const { pathname, search } = useLocation()
   const mdMatch = useMediaMatch('md')
@@ -126,7 +126,7 @@ export const TopbarViewer: FC = () => {
                       {!mdMatch && !searchOpen && (
                         <StyledAvatar
                           size={40}
-                          assetUrl={memberAvatarUrl}
+                          assetUrls={memberAvatarUrls}
                           loading={memberAvatarLoading}
                           onClick={handleDrawerToggle}
                         />
@@ -134,7 +134,7 @@ export const TopbarViewer: FC = () => {
                       {mdMatch && (
                         <StyledAvatar
                           size={40}
-                          assetUrl={memberAvatarUrl}
+                          assetUrls={memberAvatarUrls}
                           onClick={handleDrawerToggle}
                           loading={memberAvatarLoading}
                         />

+ 3 - 3
packages/atlas/src/components/_nft/NftCard/Members.tsx

@@ -15,7 +15,7 @@ import {
 } from './NftCard.styles'
 
 export type Member = {
-  assetUrl?: string | null
+  assetUrls?: string[] | null
   name?: string | null
 }
 
@@ -30,7 +30,7 @@ type MembersProps =
 export const Members: FC<MembersProps> = ({ caption, members, loading }) => {
   const avatars =
     members && Array.isArray(members)
-      ? members.map((member) => ({ assetUrl: member.assetUrl, ...(member.name ? { tooltipText: member.name } : {}) }))
+      ? members.map((member) => ({ assetUrls: member.assetUrls, ...(member.name ? { tooltipText: member.name } : {}) }))
       : null
   const isArray = Array.isArray(members)
   return (
@@ -53,7 +53,7 @@ export const Members: FC<MembersProps> = ({ caption, members, loading }) => {
       )}
       {!isArray && (
         <AvatarWrapper>
-          <StyledAvatar size={24} assetUrl={members.assetUrl} loading={loading} />
+          <StyledAvatar size={24} assetUrls={members.assetUrls} loading={loading} />
           {loading ? (
             <SkeletonLoader width={64} height={24} />
           ) : (

+ 1 - 0
packages/atlas/src/components/_nft/NftTile/NftTile.styles.ts

@@ -6,4 +6,5 @@ type ContainerProps = {
 
 export const Container = styled.div<ContainerProps>`
   width: ${({ fullWidth }) => (fullWidth ? '100%' : '320px')};
+  overflow: hidden;
 `

+ 2 - 1
packages/atlas/src/components/_nft/NftTile/NftTile.tsx

@@ -76,10 +76,11 @@ export const NftTile: FC<NftTileProps> = ({
       <VideoThumbnail
         type="video"
         videoHref={thumbnail?.videoHref}
+        linkState={{ shouldCollapse: false }}
         onMouseEnter={() => setHovered(true)}
         onMouseLeave={() => setHovered(false)}
         loading={loading}
-        thumbnailUrl={thumbnail?.thumbnailUrl}
+        thumbnailUrls={thumbnail?.thumbnailUrls}
         clickable={false}
         slots={{
           topLeft: views

+ 1 - 0
packages/atlas/src/components/_nft/NftTile/NftTileDetails.styles.ts

@@ -62,6 +62,7 @@ const tileSizeVariants = ({ tileSize }: ContentProps) => css`
 const isContentPropValid = (prop: string) => prop !== 'loading' && prop !== 'tileSize' && prop !== 'shouldHover'
 export const Content = styled(Link, { shouldForwardProp: isContentPropValid })<ContentProps>`
   display: block;
+  height: 100%;
   text-decoration: none;
   background-color: ${cVar('colorBackgroundMuted')};
   transition: background-color ${cVar('animationTransitionFast')};

+ 14 - 6
packages/atlas/src/components/_nft/NftTile/NftTileDetails.tsx

@@ -24,7 +24,7 @@ import {
 } from './NftTileDetails.styles'
 
 export type Member = {
-  assetUrl?: string | null
+  assetUrls?: string[] | null
   name?: string
   onClick?: () => void
   loading?: boolean
@@ -118,6 +118,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
               content="Not for sale"
               icon={<SvgActionNotForSale />}
               secondary
+              withDenomination
             />
           )
         case 'buy-now':
@@ -127,6 +128,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
               caption="Buy now"
               content={buyNowPrice ?? 0}
               icon={<JoyTokenIcon size={16} variant="regular" />}
+              withDenomination
             />
           )
         case 'auction':
@@ -138,6 +140,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
                   caption="Top bid"
                   content={topBid}
                   icon={<JoyTokenIcon size={16} variant="regular" />}
+                  withDenomination
                 />
               ) : (
                 <DetailsContent
@@ -145,6 +148,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
                   caption="Min bid"
                   content={startingPrice ?? 0}
                   icon={<JoyTokenIcon size={16} variant="regular" />}
+                  withDenomination
                 />
               )}
               {!!buyNowPrice && (
@@ -153,6 +157,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
                   caption="Buy now"
                   content={buyNowPrice}
                   icon={<JoyTokenIcon size={16} variant="regular" />}
+                  withDenomination
                 />
               )}
             </>
@@ -163,7 +168,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
     const avatars = useMemo(
       () => [
         {
-          url: creator?.assetUrl,
+          urls: creator?.assetUrls,
           tooltipText: `Creator: ${creator?.name}`,
           onClick: creator?.onClick,
           loading: creator?.loading,
@@ -171,7 +176,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
         ...(owner
           ? [
               {
-                url: owner?.assetUrl,
+                urls: owner?.assetUrls,
                 tooltipText: `Owner: ${owner?.name}`,
                 onClick: owner?.onClick,
                 loading: owner.loading,
@@ -179,7 +184,7 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
             ]
           : []),
       ],
-      [creator?.assetUrl, creator?.loading, creator?.name, creator?.onClick, owner]
+      [creator?.assetUrls, creator?.loading, creator?.name, creator?.onClick, owner]
     )
 
     return (
@@ -254,9 +259,10 @@ type DetailsContentProps = {
   content: number | string | ReactElement | ReactElement[]
   secondary?: boolean
   tileSize: TileSize | undefined
+  withDenomination?: boolean
 }
 export const DetailsContent: FC<DetailsContentProps> = memo(
-  ({ tileSize, caption, icon, content, secondary, avoidIconStyling }) => {
+  ({ tileSize, caption, icon, content, secondary, avoidIconStyling, withDenomination }) => {
     const getSize = () => {
       switch (tileSize) {
         case 'small':
@@ -277,7 +283,7 @@ export const DetailsContent: FC<DetailsContentProps> = memo(
           {caption}
         </Text>
         <DetailsContentWrapper avoidIconStyling={avoidIconStyling} secondary={secondary}>
-          {icon}{' '}
+          {typeof content === 'string' && icon}{' '}
           {typeof content === 'string' ? (
             <Text as="span" variant={getSize().content} color={secondary ? 'colorText' : undefined}>
               {content}
@@ -285,10 +291,12 @@ export const DetailsContent: FC<DetailsContentProps> = memo(
           ) : typeof content === 'number' ? (
             <NumberFormat
               as="span"
+              icon={icon}
               value={content}
               format="short"
               variant={getSize().content}
               color={secondary ? 'colorText' : undefined}
+              withDenomination={withDenomination}
             />
           ) : (
             content

+ 7 - 7
packages/atlas/src/components/_nft/NftTileViewer/NftTileViewer.tsx

@@ -19,9 +19,9 @@ type NftTileViewerProps = {
 export const NftTileViewer: FC<NftTileViewerProps> = ({ nftId, isInCarousel }) => {
   const { nftStatus, nft, loading } = useNft(nftId || '')
   const navigate = useNavigate()
-  const thumbnailUrl = nft?.video.thumbnailPhoto?.resolvedUrl
+  const thumbnailUrls = nft?.video.thumbnailPhoto?.resolvedUrls
   const nftActions = useNftActions()
-  const creatorAvatarUrl = nft?.video.channel.avatarPhoto?.resolvedUrl
+  const creatorAvatarUrls = nft?.video.channel.avatarPhoto?.resolvedUrls
   const nftState = useNftState(nft)
   const {
     auctionPlannedEndDate,
@@ -36,7 +36,7 @@ export const NftTileViewer: FC<NftTileViewerProps> = ({ nftId, isInCarousel }) =
   const ownerMember = nft?.owner.__typename === 'NftOwnerMember' && nft.owner.member
   const ownerChannel = nft?.owner.__typename === 'NftOwnerChannel' && nft.owner.channel
 
-  const { url: ownerMemberAvatarUrl } = getMemberAvatar(ownerMember || null)
+  const { urls: ownerMemberAvatarUrls } = getMemberAvatar(ownerMember || null)
 
   const isAuction = nftStatus?.status === 'auction'
 
@@ -50,14 +50,14 @@ export const NftTileViewer: FC<NftTileViewerProps> = ({ nftId, isInCarousel }) =
   const owner = ownerChannel
     ? {
         name: ownerChannel.title || undefined,
-        assetUrl: creatorAvatarUrl || undefined,
+        assetUrls: creatorAvatarUrls || undefined,
         loading,
         onClick: () => navigate(absoluteRoutes.viewer.channel(ownerChannel.id)),
       }
     : ownerMember
     ? {
         name: ownerMember.handle,
-        assetUrl: ownerMemberAvatarUrl,
+        assetUrls: ownerMemberAvatarUrls,
         loading,
         onClick: () => navigate(absoluteRoutes.viewer.member(ownerMember.handle)),
       }
@@ -83,7 +83,7 @@ export const NftTileViewer: FC<NftTileViewerProps> = ({ nftId, isInCarousel }) =
     loading: loading || !nftId,
     thumbnail: {
       videoHref: absoluteRoutes.viewer.video(nft?.video.id),
-      thumbnailUrl: thumbnailUrl,
+      thumbnailUrls: thumbnailUrls,
       loading: loading,
       thumbnailAlt: `${nft?.video?.title} video thumbnail`,
       type: 'video',
@@ -92,7 +92,7 @@ export const NftTileViewer: FC<NftTileViewerProps> = ({ nftId, isInCarousel }) =
     creator: {
       name: nft?.video.channel.title || undefined,
       loading: loading,
-      assetUrl: creatorAvatarUrl,
+      assetUrls: creatorAvatarUrls,
       onClick: () => navigate(absoluteRoutes.viewer.channel(nft?.video.channel.id)),
     },
     contextMenuItems,

+ 5 - 61
packages/atlas/src/components/_nft/NftWidget/NftHistory.styles.ts

@@ -1,19 +1,15 @@
 import styled from '@emotion/styled'
 
-import { Button } from '@/components/_buttons/Button'
 import { cVar, sizes } from '@/styles'
 
 import { SizeProps, sizeObj } from './NftWidget.styles'
 
-type OpenProps = { 'data-open': boolean }
-
-export const NftHistoryHeader = styled.div<SizeProps & OpenProps>`
+export const NftHistoryHeader = styled.div<SizeProps>`
   display: grid;
   grid-template-columns: 1fr auto;
   align-items: center;
   padding: ${sizes(6)};
   user-select: none;
-  cursor: pointer;
 
   &[data-size=${sizeObj.small}] {
     padding: ${sizes(4)};
@@ -24,58 +20,21 @@ export const NftHistoryHeader = styled.div<SizeProps & OpenProps>`
   }
 `
 
-export const StyledChevronButton = styled(Button)<OpenProps>`
-  transform: rotate(0);
-  transform-origin: center;
-  transition: ${cVar('animationTransitionFast')};
-
-  &[data-open='true'] {
-    transform: rotate(180deg);
-  }
-`
-
-type FadingBlockProps = { width: number; 'data-bottom'?: boolean } & SizeProps
-export const FadingBlock = styled.div<FadingBlockProps>`
-  height: ${sizes(6)};
-  background: linear-gradient(0deg, rgb(11 12 15 / 0) 0%, ${cVar('colorCoreNeutral900')} 100%);
-  position: absolute;
-  width: ${({ width }) => width}px;
-  z-index: 1;
-
-  &[data-size=${sizeObj.small}] {
-    height: ${sizes(4)};
-  }
-
-  &[data-bottom] {
-    transform: rotate(180deg);
-    bottom: 0;
-  }
-`
-
-type HistoryPanelProps = SizeProps & OpenProps
+type HistoryPanelProps = SizeProps
 export const HistoryPanel = styled.div<HistoryPanelProps>`
   background-color: ${cVar('colorBackgroundMuted')};
   position: relative;
   display: grid;
   gap: ${sizes(6)};
-  max-height: 280px;
-  padding: ${sizes(6)};
+  padding: ${sizes(2)} ${sizes(6)} ${sizes(6)} ${sizes(6)};
   overflow: hidden auto;
-  transition: transform ${cVar('animationTransitionFast')};
-  will-change: transform;
+  max-height: 376px;
 
   &[data-size=${sizeObj.small}] {
+    max-height: 400px;
     gap: ${sizes(4)};
     padding: ${sizes(4)};
   }
-
-  transform: translateY(-100%);
-  z-index: -1;
-
-  &[data-open='true'] {
-    transform: translateY(0);
-    z-index: unset;
-  }
 `
 
 export const HistoryItemContainer = styled.div<SizeProps>`
@@ -98,21 +57,6 @@ export const CopyContainer = styled.div`
   display: flex;
 `
 
-export const ValueContainer = styled.div`
-  display: grid;
-  gap: ${sizes(1)};
-  grid-auto-rows: max-content;
-`
-
-export const JoyPlusIcon = styled.div`
-  display: grid;
-  gap: ${sizes(1)};
-  align-items: center;
-  grid-auto-flow: column;
-  grid-auto-columns: max-content;
-  justify-content: end;
-`
-
 export const HistoryPanelContainer = styled.div`
   position: relative;
 `

+ 20 - 44
packages/atlas/src/components/_nft/NftWidget/NftHistory.tsx

@@ -3,54 +3,40 @@ import { FC } from 'react'
 import { useNavigate } from 'react-router'
 
 import { BasicMembershipFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
-import { SvgActionChevronB } from '@/assets/icons'
 import { Avatar } from '@/components/Avatar'
 import { JoyTokenIcon } from '@/components/JoyTokenIcon'
 import { NumberFormat } from '@/components/NumberFormat'
 import { Text } from '@/components/Text'
 import { absoluteRoutes } from '@/config/routes'
-import { useToggle } from '@/hooks/useToggle'
 import { getMemberAvatar } from '@/providers/assets/assets.helpers'
-import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
 import { formatDateTime } from '@/utils/time'
 
 import {
   CopyContainer,
-  FadingBlock,
   HistoryItemContainer,
   HistoryPanel,
   HistoryPanelContainer,
-  JoyPlusIcon,
   NftHistoryHeader,
-  StyledChevronButton,
   TextContainer,
-  ValueContainer,
 } from './NftHistory.styles'
 import { OwnerHandle, Size } from './NftWidget.styles'
 
 type NftHistoryProps = { size: Size; width: number; historyItems: NftHistoryEntry[] }
-export const NftHistory: FC<NftHistoryProps> = ({ size, width, historyItems }) => {
-  const [isOpen, toggleIsOpen] = useToggle()
-
+export const NftHistory: FC<NftHistoryProps> = ({ size, historyItems }) => {
   return (
     <>
-      <NftHistoryHeader data-open={isOpen} data-size={size} onClick={toggleIsOpen}>
+      <NftHistoryHeader data-size={size}>
         <Text as="h3" variant={size === 'small' ? 'h300' : 'h400'}>
           History
         </Text>
-        <StyledChevronButton data-open={isOpen} variant="tertiary" icon={<SvgActionChevronB />} />
       </NftHistoryHeader>
-      {isOpen && (
-        <HistoryPanelContainer>
-          <FadingBlock data-size={size} width={width} />
-          <HistoryPanel data-size={size} data-open={isOpen}>
-            {historyItems.map((props, index) => (
-              <HistoryItem key={index} {...props} size={size} />
-            ))}
-          </HistoryPanel>
-          <FadingBlock data-size={size} width={width} data-bottom />
-        </HistoryPanelContainer>
-      )}
+      <HistoryPanelContainer>
+        <HistoryPanel data-size={size}>
+          {historyItems.map((props, index) => (
+            <HistoryItem key={index} {...props} size={size} />
+          ))}
+        </HistoryPanel>
+      </HistoryPanelContainer>
     </>
   )
 }
@@ -66,16 +52,13 @@ type HistoryItemProps = {
 } & NftHistoryEntry
 export const HistoryItem: FC<HistoryItemProps> = ({ size, member, date, joyAmount, text }) => {
   const navigate = useNavigate()
-  const { url, isLoadingAsset } = getMemberAvatar(member)
-  const { convertHapiToUSD } = useTokenPrice()
-
-  const dollarValue = joyAmount ? convertHapiToUSD(joyAmount) : null
+  const { urls, isLoadingAsset } = getMemberAvatar(member)
 
   return (
     <HistoryItemContainer data-size={size}>
       <Avatar
         onClick={() => navigate(absoluteRoutes.viewer.member(member?.handle))}
-        assetUrl={url}
+        assetUrls={urls}
         loading={isLoadingAsset}
         size={size === 'medium' ? 40 : 32}
       />
@@ -96,22 +79,15 @@ export const HistoryItem: FC<HistoryItemProps> = ({ size, member, date, joyAmoun
         </Text>
       </TextContainer>
       {!!joyAmount && (
-        <ValueContainer>
-          <JoyPlusIcon>
-            <JoyTokenIcon size={16} variant="silver" />
-            <NumberFormat as="span" format="short" value={joyAmount} variant={size === 'medium' ? 'h300' : 'h200'} />
-          </JoyPlusIcon>
-          {dollarValue !== null && (
-            <NumberFormat
-              as="span"
-              format="dollar"
-              variant="t100"
-              color="colorText"
-              value={dollarValue || 0}
-              align="end"
-            />
-          )}
-        </ValueContainer>
+        <NumberFormat
+          as="span"
+          icon={<JoyTokenIcon size={16} variant="silver" />}
+          format="short"
+          value={joyAmount}
+          variant={size === 'medium' ? 'h300' : 'h200'}
+          withDenomination
+          denominationAlign="right"
+        />
       )}
     </HistoryItemContainer>
   )

+ 10 - 8
packages/atlas/src/components/_nft/NftWidget/NftWidget.hooks.ts

@@ -83,9 +83,11 @@ export const useNftWidget = (video: FullVideoFieldsFragment | undefined | null):
   const ownerMember = nftOwner?.__typename === 'NftOwnerMember' ? nftOwner.member : null
   const ownerChannel = nftOwner?.__typename === 'NftOwnerChannel' ? nftOwner.channel : null
 
-  const { url: ownerAvatarUri } = getMemberAvatar(ownerMember)
-  const creatorAvatarUri = ownerChannel?.avatarPhoto?.resolvedUrl
-  const { url: topBidderAvatarUri } = getMemberAvatar(nftStatus?.status === 'auction' ? nftStatus.topBidder : undefined)
+  const { urls: _ownerAvatarUrls } = getMemberAvatar(ownerMember)
+  const creatorAvatarUrls = ownerChannel?.avatarPhoto?.resolvedUrls
+  const { urls: topBidderAvatarUris } = getMemberAvatar(
+    nftStatus?.status === 'auction' ? nftStatus.topBidder : undefined
+  )
 
   const { entries: nftHistory } = useNftHistoryEntries(video?.id ?? '', {
     skip: !nft,
@@ -95,7 +97,7 @@ export const useNftWidget = (video: FullVideoFieldsFragment | undefined | null):
   })
   const isOwnedByChannel = nftOwner?.__typename === 'NftOwnerChannel'
   const ownerHandle = ownerMember?.handle || ownerChannel?.title
-  const ownerAvatar = isOwnedByChannel ? creatorAvatarUri : ownerAvatarUri
+  const ownerAvatarUrls = isOwnedByChannel ? creatorAvatarUrls : _ownerAvatarUrls
 
   const creatorId = ownerMember?.id || ownerChannel?.id
 
@@ -103,7 +105,7 @@ export const useNftWidget = (video: FullVideoFieldsFragment | undefined | null):
     case 'auction': {
       return {
         ownerHandle,
-        ownerAvatar,
+        ownerAvatarUrls,
         creatorId,
         isOwner,
         needsSettling,
@@ -115,7 +117,7 @@ export const useNftWidget = (video: FullVideoFieldsFragment | undefined | null):
           canChangeBid,
           englishTimerState,
           auctionPlannedEndDate,
-          topBidderAvatarUri,
+          topBidderAvatarUris,
           isUserTopBidder,
           userBidUnlockDate,
           startsAtBlock,
@@ -137,7 +139,7 @@ export const useNftWidget = (video: FullVideoFieldsFragment | undefined | null):
     case 'buy-now':
       return {
         ownerHandle,
-        ownerAvatar,
+        ownerAvatarUrls,
         creatorId,
         isOwner,
         needsSettling,
@@ -154,7 +156,7 @@ export const useNftWidget = (video: FullVideoFieldsFragment | undefined | null):
     case 'idle':
       return {
         ownerHandle,
-        ownerAvatar,
+        ownerAvatarUrls,
         creatorId,
         isOwner,
         needsSettling,

+ 46 - 48
packages/atlas/src/components/_nft/NftWidget/NftWidget.styles.ts

@@ -1,6 +1,8 @@
+import isPropValid from '@emotion/is-prop-valid'
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
+import { SvgActionChevronT } from '@/assets/icons'
 import { Avatar } from '@/components/Avatar'
 import { Text } from '@/components/Text'
 import { cVar, sizes } from '@/styles'
@@ -13,13 +15,22 @@ export const Container = styled.div`
   background-color: ${cVar('colorBackgroundMuted')};
   min-width: 0;
 `
+export const CollapsibleWrapper = styled.div<{ collapsed: boolean }>`
+  display: grid;
+  grid-template-rows: ${({ collapsed }) => (collapsed ? '0fr' : '1fr')};
+  transition: grid-template-rows ${cVar('animationTransitionMedium')};
+`
+
+export const CollapsibleElement = styled.div`
+  overflow: hidden;
+`
 
 export const Content = styled.div<SizeProps>`
   display: grid;
   gap: ${sizes(6)};
   grid-template-columns: 1fr 1fr;
   justify-content: space-between;
-  box-shadow: ${cVar('effectDividersTop')}, ${cVar('effectDividersBottom')};
+  box-shadow: ${cVar('effectDividersBottom')};
   padding: ${sizes(6)};
 
   &[data-size=${sizeObj.small}] {
@@ -31,10 +42,11 @@ export const Content = styled.div<SizeProps>`
 
 export const NftOwnerContainer = styled.div<SizeProps>`
   display: grid;
+  box-shadow: ${cVar('effectDividersBottom')};
   gap: ${sizes(1)} ${sizes(6)};
   grid-template:
-    'avatar owner-label' auto
-    'avatar owner' auto / auto 1fr;
+    'avatar owner-label collapsible-button' auto
+    'avatar owner collapsible-button' auto / min-content auto auto;
   align-items: center;
   padding: ${sizes(6)};
 
@@ -44,62 +56,48 @@ export const NftOwnerContainer = styled.div<SizeProps>`
   }
 `
 
-export const OwnerAvatar = styled(Avatar)`
-  grid-area: avatar;
+export const StatusContainer = styled.div`
+  background-color: ${cVar('colorBackground')};
+  padding: ${sizes(1)} ${sizes(6)};
+  display: flex;
+  gap: ${sizes(2)};
+  align-items: center;
 `
 
-export const OwnerLabel = styled(Text)`
-  grid-area: owner-label;
+export const StatusMark = styled.div`
+  width: 11px;
+  height: 11px;
+  background-color: ${cVar('colorTextSuccess')};
+  border-radius: 12px;
+  border: 1px solid ${cVar('colorBackgroundElevatedAlpha')};
 `
 
-export const OwnerHandle = styled(Link)`
-  grid-area: owner;
-  justify-content: start;
-  text-decoration: none;
+export const StyledSvgActionChevronT = styled(SvgActionChevronT, {
+  shouldForwardProp: isPropValid,
+})<{ isCollapsed: boolean }>`
+  transform: rotate(${({ isCollapsed }) => (isCollapsed ? '-180deg' : '0deg')});
+  transition: transform ${cVar('animationTransitionMedium')};
 `
 
-export const ButtonGrid = styled.div<SizeProps & { 'data-two-columns'?: boolean }>`
-  display: grid;
-  gap: ${sizes(4)};
-
-  &[data-size=${sizeObj.small}] {
-    gap: ${sizes(2)};
-  }
+export const OwnerAvatar = styled(Avatar)`
+  grid-area: avatar;
+`
 
-  &[data-two-columns='true'] {
-    grid-template-columns: 1fr 1fr;
-  }
+export const OwnerLabel = styled(Text)`
+  grid-area: owner-label;
 `
 
-export const TopBidderTokenContainer = styled.div<SizeProps>`
+export const CollapsibleButtonWrapper = styled.div`
+  grid-area: collapsible-button;
+  justify-self: end;
   display: flex;
+  gap: ${sizes(3)};
   align-items: center;
-  position: relative;
-  left: -4px;
-  z-index: 10;
-
-  &::before {
-    display: inline-block;
-    position: absolute;
-    content: '';
-    width: 28px;
-    height: 28px;
-    background: ${cVar('colorBackgroundMuted')};
-    border-radius: 100%;
-    left: -2px;
-    top: -2px;
-  }
-
-  &[data-size=${sizeObj.small}] {
-    &::before {
-      width: 21px;
-      height: 21px;
-      left: -2.5px;
-      top: 1.5px;
-    }
-  }
 `
 
-export const TopBidderContainer = styled.div`
-  display: flex;
+export const OwnerHandle = styled(Link)`
+  grid-area: owner;
+  justify-content: start;
+  justify-self: start;
+  text-decoration: none;
 `

+ 72 - 542
packages/atlas/src/components/_nft/NftWidget/NftWidget.tsx

@@ -1,29 +1,19 @@
 import BN from 'bn.js'
-import { differenceInSeconds } from 'date-fns'
-import { FC, memo } from 'react'
+import { FC, useEffect, useState } from 'react'
+import { useLocation } from 'react-router'
 import useResizeObserver from 'use-resize-observer'
 
-import { BasicBidFieldsFragment, FullBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
-import { SvgAlertsInformative24 } from '@/assets/icons'
-import { Avatar } from '@/components/Avatar'
-import { Banner } from '@/components/Banner'
-import { JoyTokenIcon } from '@/components/JoyTokenIcon'
-import { GridItem } from '@/components/LayoutGrid'
-import { NumberFormat } from '@/components/NumberFormat'
+import { FullBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
 import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
 import { absoluteRoutes } from '@/config/routes'
-import { useDeepMemo } from '@/hooks/useDeepMemo'
-import { useMsTimestamp } from '@/hooks/useMsTimestamp'
-import { EnglishTimerState } from '@/hooks/useNftState'
 import { NftSaleType } from '@/joystream-lib/types'
-import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
-import { formatDateTime, formatDurationShort, formatTime } from '@/utils/time'
 
 import { NftHistory, NftHistoryEntry } from './NftHistory'
-import { NftInfoItem, NftTimerItem } from './NftInfoItem'
 import {
-  ButtonGrid,
+  CollapsibleButtonWrapper,
+  CollapsibleElement,
+  CollapsibleWrapper,
   Container,
   Content,
   NftOwnerContainer,
@@ -31,67 +21,34 @@ import {
   OwnerHandle,
   OwnerLabel,
   Size,
-  TopBidderContainer,
-  TopBidderTokenContainer,
+  StatusContainer,
+  StatusMark,
+  StyledSvgActionChevronT,
 } from './NftWidget.styles'
-
-export type Auction = {
-  status: 'auction'
-  type: 'open' | 'english'
-  startingPrice: BN
-  buyNowPrice: BN | undefined
-  topBid: BasicBidFieldsFragment | undefined
-  topBidAmount: BN | undefined
-  topBidderHandle: string | undefined
-  topBidderAvatarUri: string | null | undefined
-  isUserTopBidder: boolean | undefined
-  userBidAmount: BN | undefined
-  userBidUnlockDate: Date | undefined
-  canWithdrawBid: boolean | undefined
-  canChangeBid: boolean | undefined
-  hasTimersLoaded: boolean | undefined
-  englishTimerState: EnglishTimerState | undefined
-  auctionPlannedEndDate: Date | undefined
-  startsAtDate: Date | undefined
-  plannedEndAtBlock: number | null | undefined
-  startsAtBlock: number | null | undefined
-  auctionBeginsInDays: number
-  auctionBeginsInSeconds: number
-  isUserWhitelisted: boolean | undefined
-}
+import { NftWidgetStatus } from './NftWidget.types'
+import { NftWidgetContent } from './NftWidgetContent'
 
 export type NftWidgetProps = {
   ownerHandle: string | null | undefined
-  ownerAvatar: string | null | undefined
+  ownerAvatarUrls: string[] | null | undefined
   creatorId?: string
-  isOwner: boolean | undefined
-  needsSettling: boolean | undefined
-  bidFromPreviousAuction: FullBidFieldsFragment | undefined
   saleType: NftSaleType | null
-  nftStatus?:
-    | {
-        status: 'idle'
-        lastSalePrice: BN | undefined
-        lastSaleDate: Date | undefined
-      }
-    | {
-        status: 'buy-now'
-        buyNowPrice: BN
-      }
-    | Auction
-    | undefined
+  isOwnedByChannel?: boolean
+  nftStatus: NftWidgetStatus | undefined
   nftHistory: NftHistoryEntry[]
+  isOwner: boolean | undefined
+  needsSettling: boolean | undefined
+  onNftPutOnSale?: () => void
+  onNftAcceptBid?: () => void
   onNftPurchase?: () => void
+  onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
+  bidFromPreviousAuction: FullBidFieldsFragment | undefined
   onNftSettlement?: () => void
   onNftBuyNow?: () => void
-  onNftPutOnSale?: () => void
-  onNftAcceptBid?: () => void
   onNftCancelSale?: () => void
   onNftChangePrice?: () => void
-  onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
   userBidCreatedAt?: Date
   userBidAmount?: BN
-  isOwnedByChannel?: boolean
 }
 
 const SMALL_VARIANT_MAXIMUM_SIZE = 416
@@ -99,11 +56,12 @@ const SMALL_VARIANT_MAXIMUM_SIZE = 416
 export const NftWidget: FC<NftWidgetProps> = ({
   ownerHandle,
   creatorId,
-  isOwner,
   nftStatus,
   nftHistory,
+  isOwnedByChannel,
+  ownerAvatarUrls,
+  isOwner,
   needsSettling,
-  ownerAvatar,
   onNftPutOnSale,
   onNftAcceptBid,
   onWithdrawBid,
@@ -115,494 +73,28 @@ export const NftWidget: FC<NftWidgetProps> = ({
   onNftBuyNow,
   userBidCreatedAt,
   userBidAmount,
-  isOwnedByChannel,
 }) => {
-  const timestamp = useMsTimestamp()
   const { ref, width = SMALL_VARIANT_MAXIMUM_SIZE + 1 } = useResizeObserver({
     box: 'border-box',
   })
 
-  const size: Size = width > SMALL_VARIANT_MAXIMUM_SIZE ? 'medium' : 'small'
-  const { convertHapiToUSD, isLoadingPrice } = useTokenPrice()
-
-  const content = useDeepMemo(() => {
-    if (!nftStatus) {
-      return
-    }
-    const contentTextVariant = size === 'small' ? 'h400' : 'h600'
-    const buttonSize = size === 'small' ? 'medium' : 'large'
-    const buttonColumnSpan = size === 'small' ? 1 : 2
-    const timerColumnSpan = size === 'small' ? 1 : 2
-
-    const BuyNow = memo(({ buyNowPrice }: { buyNowPrice?: BN }) => {
-      const buyNowPriceInUsd = buyNowPrice && convertHapiToUSD(buyNowPrice)
-      return buyNowPrice?.gtn(0) ? (
-        <NftInfoItem
-          size={size}
-          label="Buy now"
-          disableSecondary={buyNowPriceInUsd === null}
-          content={
-            <>
-              <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-              <NumberFormat as="span" value={buyNowPrice} format="short" variant={contentTextVariant} />
-            </>
-          }
-          secondaryText={
-            buyNowPriceInUsd && <NumberFormat as="span" color="colorText" format="dollar" value={buyNowPriceInUsd} />
-          }
-        />
-      ) : null
-    })
-    BuyNow.displayName = 'BuyNow'
-    const InfoBanner = ({ title, description }: { title: string; description: string }) => (
-      <GridItem colSpan={buttonColumnSpan}>
-        <Banner icon={<SvgAlertsInformative24 />} {...{ title, description }} />
-      </GridItem>
-    )
-
-    const WithdrawBidFromPreviousAuction = ({ secondary }: { secondary?: boolean }) =>
-      bidFromPreviousAuction ? (
-        <>
-          <GridItem colSpan={buttonColumnSpan}>
-            <Button
-              variant={secondary ? 'secondary' : undefined}
-              fullWidth
-              size={buttonSize}
-              onClick={() =>
-                onWithdrawBid?.(new BN(bidFromPreviousAuction.amount), new Date(bidFromPreviousAuction.createdAt))
-              }
-            >
-              Withdraw last bid
-            </Button>
-            <Text as="p" margin={{ top: 2 }} variant="t100" color="colorText" align="center">
-              You bid{' '}
-              <NumberFormat
-                as="span"
-                value={new BN(bidFromPreviousAuction?.amount)}
-                format="short"
-                variant="t100"
-                color="colorText"
-                withToken
-              />{' '}
-              on {formatDateTime(new Date(bidFromPreviousAuction.createdAt))}
-            </Text>
-          </GridItem>
-        </>
-      ) : null
+  const location = useLocation()
+  const [isCollapsed, setIsCollapsed] = useState(true)
 
-    const BidPlacingInfoText = () => (
-      <Text as="p" variant="t100" color="colorText" align="center">
-        Placing a bid will withdraw your last bid
-      </Text>
-    )
+  const shouldCollapse = location.state?.shouldCollapse === undefined ? true : location.state?.shouldCollapse
 
-    switch (nftStatus.status) {
-      case 'idle':
-        return (
-          <>
-            {nftStatus.lastSalePrice ? (
-              <NftInfoItem
-                size={size}
-                label="Last price"
-                content={
-                  <>
-                    <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-                    <NumberFormat
-                      as="span"
-                      value={nftStatus.lastSalePrice}
-                      format="short"
-                      variant={contentTextVariant}
-                      color="colorText"
-                    />
-                  </>
-                }
-                secondaryText={nftStatus.lastSaleDate && formatDateTime(nftStatus.lastSaleDate)}
-              />
-            ) : (
-              <NftInfoItem
-                size={size}
-                label="status"
-                content={
-                  <Text as="span" variant={contentTextVariant} color="colorText">
-                    Not for sale
-                  </Text>
-                }
-              />
-            )}
-            {bidFromPreviousAuction && (
-              <>
-                <InfoBanner
-                  title="Withdraw your bid"
-                  description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
-                />
-                <WithdrawBidFromPreviousAuction />
-              </>
-            )}
-            {isOwner && (
-              <GridItem colSpan={buttonColumnSpan}>
-                <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPutOnSale}>
-                  Start sale of this NFT
-                </Button>
-              </GridItem>
-            )}
-          </>
-        )
-      case 'buy-now':
-        return (
-          <>
-            <BuyNow buyNowPrice={nftStatus.buyNowPrice} />
+  useEffect(() => {
+    setIsCollapsed(shouldCollapse)
+  }, [shouldCollapse])
 
-            <GridItem colSpan={buttonColumnSpan}>
-              <ButtonGrid data-size={size}>
-                {isOwner ? (
-                  <>
-                    <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftChangePrice}>
-                      Change price
-                    </Button>
-                    <Button fullWidth variant="destructive" size={buttonSize} onClick={onNftCancelSale}>
-                      Remove from sale
-                    </Button>
-                  </>
-                ) : (
-                  <GridItem colSpan={buttonColumnSpan}>
-                    <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
-                      Buy now
-                    </Button>
-                  </GridItem>
-                )}
-                {bidFromPreviousAuction && (
-                  <>
-                    <InfoBanner
-                      title="Withdraw your bid"
-                      description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
-                    />
-                    <WithdrawBidFromPreviousAuction secondary />
-                  </>
-                )}
-              </ButtonGrid>
-            </GridItem>
-          </>
-        )
-      case 'auction': {
-        const getInfoBannerProps = () => {
-          const hasBids = !nftStatus.topBid?.isCanceled && nftStatus.topBidAmount?.gtn(0)
-          if (nftStatus.type === 'open' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid to participate',
-              description:
-                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
-            }
-          }
-
-          if (nftStatus.englishTimerState === 'expired' && isOwner && !hasBids) {
-            return {
-              title: 'Auction ended',
-              description: 'This auction has ended and no one placed a bid. You can now remove this NFT from sale.',
-            }
-          }
-          if (nftStatus.englishTimerState === 'expired' && !bidFromPreviousAuction && !hasBids && !isOwner) {
-            return {
-              title: 'Auction ended',
-              description:
-                "This auction has ended and no one placed a bid. We're waiting for the NFT owner to remove this NFT from sale.",
-            }
-          }
-          if (
-            nftStatus.englishTimerState === 'expired' &&
-            !bidFromPreviousAuction &&
-            hasBids &&
-            !isOwner &&
-            !nftStatus.isUserTopBidder
-          ) {
-            return {
-              title: 'Auction ended',
-              description:
-                'We are waiting for this auction to be settled by the auction winner or the current NFT owner.',
-            }
-          }
-          if (nftStatus.englishTimerState === 'expired' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid',
-              description: 'You placed a bid in a previous auction that you can now withdraw.',
-            }
-          }
-
-          if (nftStatus.englishTimerState === 'running' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid to participate',
-              description:
-                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
-            }
-          }
-
-          if (nftStatus.englishTimerState === 'upcoming' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid to participate',
-              description:
-                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this upcoming auction.',
-            }
-          }
-
-          if (nftStatus.isUserWhitelisted === false) {
-            return {
-              title: "You're not on the whitelist",
-              description: `This sale is available only to members whitelisted by ${ownerHandle}.`,
-            }
-          }
-
-          return null
-        }
-        const infoBannerProps = getInfoBannerProps()
-
-        const infoTextNode = !!nftStatus.userBidAmount?.gtn(0) && nftStatus.userBidUnlockDate && (
-          <GridItem colSpan={buttonColumnSpan}>
-            {nftStatus.type === 'english' ? (
-              <BidPlacingInfoText />
-            ) : (
-              <Text as="p" variant="t100" color="colorText" align="center">
-                {nftStatus.canWithdrawBid ? `Your last bid: ` : `Your last bid (`}
-                <NumberFormat as="span" value={nftStatus.userBidAmount} format="short" withToken />
-                {nftStatus.canWithdrawBid
-                  ? ''
-                  : `) becomes withdrawable on ${formatDateTime(nftStatus.userBidUnlockDate)}`}
-              </Text>
-            )}
-          </GridItem>
-        )
-
-        const topBidAmountInUsd = nftStatus.topBidAmount && convertHapiToUSD(nftStatus.topBidAmount)
-        const startingPriceInUsd = convertHapiToUSD(nftStatus.startingPrice)
-
-        return (
-          <>
-            {nftStatus.topBidAmount?.gtn(0) && !nftStatus.topBid?.isCanceled ? (
-              <NftInfoItem
-                size={size}
-                label="Top bid"
-                content={
-                  <>
-                    <TopBidderContainer>
-                      <Avatar assetUrl={nftStatus.topBidderAvatarUri} size={24} />
-                      <TopBidderTokenContainer data-size={size}>
-                        <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-                      </TopBidderTokenContainer>
-                    </TopBidderContainer>
-                    <NumberFormat
-                      as="span"
-                      format="short"
-                      value={nftStatus.topBidAmount}
-                      variant={contentTextVariant}
-                    />
-                  </>
-                }
-                secondaryText={
-                  !isLoadingPrice && nftStatus.topBidderHandle ? (
-                    <>
-                      {topBidAmountInUsd ? (
-                        <NumberFormat as="span" color="colorText" format="dollar" value={topBidAmountInUsd} />
-                      ) : null}{' '}
-                      from{' '}
-                      <OwnerHandle to={absoluteRoutes.viewer.member(nftStatus.topBidderHandle)}>
-                        <Text as="span" variant="t100">
-                          {nftStatus.isUserTopBidder ? 'you' : nftStatus.topBidderHandle}
-                        </Text>
-                      </OwnerHandle>
-                    </>
-                  ) : null
-                }
-              />
-            ) : (
-              <NftInfoItem
-                size={size}
-                label="Starting Price"
-                content={
-                  <>
-                    <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-                    <NumberFormat
-                      as="span"
-                      format="short"
-                      value={nftStatus.startingPrice}
-                      variant={contentTextVariant}
-                    />
-                  </>
-                }
-                disableSecondary={startingPriceInUsd === null}
-                secondaryText={
-                  startingPriceInUsd && (
-                    <NumberFormat as="span" color="colorText" format="dollar" value={startingPriceInUsd ?? 0} />
-                  )
-                }
-              />
-            )}
-            <BuyNow buyNowPrice={nftStatus.buyNowPrice} />
-
-            {nftStatus.englishTimerState === 'expired' && (
-              <GridItem colSpan={timerColumnSpan}>
-                <NftInfoItem
-                  size={size}
-                  label="Auction ended on"
-                  loading={!nftStatus.auctionPlannedEndDate}
-                  content={
-                    nftStatus.auctionPlannedEndDate && (
-                      <Text as="span" variant={contentTextVariant} color="colorText">
-                        {formatDateTime(nftStatus.auctionPlannedEndDate)}
-                      </Text>
-                    )
-                  }
-                />
-              </GridItem>
-            )}
-            {nftStatus.englishTimerState === 'running' && nftStatus?.auctionPlannedEndDate && (
-              <GridItem colSpan={timerColumnSpan}>
-                <NftTimerItem size={size} time={nftStatus.auctionPlannedEndDate} />
-              </GridItem>
-            )}
-            {nftStatus.startsAtBlock && nftStatus.auctionBeginsInSeconds >= 0 && (
-              <GridItem colSpan={timerColumnSpan}>
-                <NftInfoItem
-                  size={size}
-                  label="Auction begins on"
-                  loading={!nftStatus.startsAtDate}
-                  content={
-                    nftStatus.startsAtDate && (
-                      <Text as="span" variant={contentTextVariant} color="colorText">
-                        {nftStatus.auctionBeginsInDays > 1 && formatDateTime(nftStatus.startsAtDate)}
-                        {nftStatus.auctionBeginsInDays === 1 && `Tomorrow at ${formatTime(nftStatus.startsAtDate)}`}
-                        {nftStatus.auctionBeginsInDays < 1 &&
-                          formatDurationShort(differenceInSeconds(nftStatus.startsAtDate, timestamp))}
-                      </Text>
-                    )
-                  }
-                />
-              </GridItem>
-            )}
-
-            {nftStatus.hasTimersLoaded && infoBannerProps && <InfoBanner {...infoBannerProps} />}
-
-            {nftStatus.hasTimersLoaded && needsSettling && (nftStatus.isUserTopBidder || isOwner) && (
-              <GridItem colSpan={buttonColumnSpan}>
-                <Button fullWidth size={buttonSize} onClick={onNftSettlement}>
-                  Settle auction
-                </Button>
-              </GridItem>
-            )}
-
-            {nftStatus.hasTimersLoaded && bidFromPreviousAuction && <WithdrawBidFromPreviousAuction />}
-
-            {nftStatus.hasTimersLoaded &&
-              !needsSettling &&
-              !bidFromPreviousAuction &&
-              (isOwner
-                ? (nftStatus.type === 'open' ||
-                    // english auction with no bids
-                    !nftStatus.topBidAmount ||
-                    nftStatus.topBid?.isCanceled) && (
-                    <GridItem colSpan={buttonColumnSpan}>
-                      <ButtonGrid data-size={size}>
-                        {nftStatus.type === 'open' && nftStatus.topBid && !nftStatus.topBid?.isCanceled && (
-                          <Button fullWidth size={buttonSize} onClick={onNftAcceptBid}>
-                            Review and accept bid
-                          </Button>
-                        )}
-                        <Button
-                          fullWidth
-                          onClick={onNftCancelSale}
-                          variant={
-                            nftStatus.type === 'open' && !nftStatus.topBid?.isCanceled
-                              ? 'destructive-secondary'
-                              : 'destructive'
-                          }
-                          size={buttonSize}
-                        >
-                          Remove from sale
-                        </Button>
-                      </ButtonGrid>
-                    </GridItem>
-                  )
-                : nftStatus.englishTimerState === 'running' &&
-                  nftStatus.isUserWhitelisted !== false &&
-                  (nftStatus.buyNowPrice?.gtn(0) ? (
-                    <GridItem colSpan={buttonColumnSpan}>
-                      <ButtonGrid data-size={size} data-two-columns={size === 'medium'}>
-                        <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPurchase}>
-                          {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
-                        </Button>
-                        <Button fullWidth size={buttonSize} onClick={onNftBuyNow}>
-                          Buy now
-                        </Button>
-                        {/* second row button */}
-                        {nftStatus.canWithdrawBid && (
-                          <GridItem colSpan={buttonColumnSpan}>
-                            <Button
-                              fullWidth
-                              size={buttonSize}
-                              variant="destructive-secondary"
-                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
-                            >
-                              Withdraw bid
-                            </Button>
-                          </GridItem>
-                        )}
-
-                        {infoTextNode}
-                      </ButtonGrid>
-                    </GridItem>
-                  ) : (
-                    <GridItem colSpan={buttonColumnSpan}>
-                      <ButtonGrid data-size={size}>
-                        <GridItem colSpan={buttonColumnSpan}>
-                          <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
-                            {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
-                          </Button>
-                        </GridItem>
-                        {nftStatus.canWithdrawBid && (
-                          <GridItem colSpan={buttonColumnSpan}>
-                            <Button
-                              fullWidth
-                              size={buttonSize}
-                              variant="destructive-secondary"
-                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
-                            >
-                              Withdraw bid
-                            </Button>
-                          </GridItem>
-                        )}
-                        {infoTextNode}
-                      </ButtonGrid>
-                    </GridItem>
-                  )))}
-          </>
-        )
-      }
-    }
-  }, [
-    nftStatus,
-    size,
-    convertHapiToUSD,
-    bidFromPreviousAuction,
-    onWithdrawBid,
-    isOwner,
-    onNftPutOnSale,
-    onNftChangePrice,
-    onNftCancelSale,
-    onNftPurchase,
-    isLoadingPrice,
-    timestamp,
-    needsSettling,
-    onNftSettlement,
-    onNftAcceptBid,
-    onNftBuyNow,
-    ownerHandle,
-    userBidAmount,
-    userBidCreatedAt,
-  ])
+  const size: Size = width > SMALL_VARIANT_MAXIMUM_SIZE ? 'medium' : 'small'
 
   if (!nftStatus) return null
 
   return (
     <Container ref={ref}>
       <NftOwnerContainer data-size={size}>
-        <OwnerAvatar assetUrl={ownerAvatar} size={40} />
+        <OwnerAvatar assetUrls={ownerAvatarUrls} size={40} />
         <OwnerLabel as="span" variant="t100" color="colorText">
           This NFT is owned by
         </OwnerLabel>
@@ -619,10 +111,48 @@ export const NftWidget: FC<NftWidgetProps> = ({
             {ownerHandle}
           </Text>
         </OwnerHandle>
+        <CollapsibleButtonWrapper>
+          <Button
+            icon={<StyledSvgActionChevronT isCollapsed={isCollapsed} />}
+            variant="tertiary"
+            size="small"
+            onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}
+          />
+        </CollapsibleButtonWrapper>
       </NftOwnerContainer>
-      <Content data-size={size}>{content}</Content>
-
-      <NftHistory size={size} width={width} historyItems={nftHistory} />
+      {nftStatus.status !== 'idle' && isCollapsed && (
+        <StatusContainer>
+          <StatusMark />
+          <Text variant="t100" as="p">
+            Purchasable
+          </Text>
+        </StatusContainer>
+      )}
+      <CollapsibleWrapper collapsed={isCollapsed}>
+        <CollapsibleElement>
+          <Content data-size={size}>
+            <NftWidgetContent
+              ownerHandle={ownerHandle}
+              size={size}
+              nftStatus={nftStatus}
+              isOwner={isOwner}
+              needsSettling={needsSettling}
+              onNftPutOnSale={onNftPutOnSale}
+              onNftAcceptBid={onNftAcceptBid}
+              onWithdrawBid={onWithdrawBid}
+              bidFromPreviousAuction={bidFromPreviousAuction}
+              onNftCancelSale={onNftCancelSale}
+              onNftChangePrice={onNftChangePrice}
+              onNftPurchase={onNftPurchase}
+              onNftSettlement={onNftSettlement}
+              onNftBuyNow={onNftBuyNow}
+              userBidCreatedAt={userBidCreatedAt}
+              userBidAmount={userBidAmount}
+            />
+          </Content>
+          <NftHistory size={size} width={width} historyItems={nftHistory} />
+        </CollapsibleElement>
+      </CollapsibleWrapper>
     </Container>
   )
 }

+ 41 - 0
packages/atlas/src/components/_nft/NftWidget/NftWidget.types.ts

@@ -0,0 +1,41 @@
+import BN from 'bn.js'
+
+import { BasicBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
+import { EnglishTimerState } from '@/hooks/useNftState'
+
+export type Auction = {
+  status: 'auction'
+  type: 'open' | 'english'
+  startingPrice: BN
+  buyNowPrice: BN | undefined
+  topBid: BasicBidFieldsFragment | undefined
+  topBidAmount: BN | undefined
+  topBidderHandle: string | undefined
+  topBidderAvatarUris: string[] | null | undefined
+  isUserTopBidder: boolean | undefined
+  userBidAmount: BN | undefined
+  userBidUnlockDate: Date | undefined
+  canWithdrawBid: boolean | undefined
+  canChangeBid: boolean | undefined
+  hasTimersLoaded: boolean | undefined
+  englishTimerState: EnglishTimerState | undefined
+  auctionPlannedEndDate: Date | undefined
+  startsAtDate: Date | undefined
+  plannedEndAtBlock: number | null | undefined
+  startsAtBlock: number | null | undefined
+  auctionBeginsInDays: number
+  auctionBeginsInSeconds: number
+  isUserWhitelisted: boolean | undefined
+}
+
+export type NftWidgetStatus =
+  | {
+      status: 'idle'
+      lastSalePrice: BN | undefined
+      lastSaleDate: Date | undefined
+    }
+  | {
+      status: 'buy-now'
+      buyNowPrice: BN
+    }
+  | Auction

+ 54 - 0
packages/atlas/src/components/_nft/NftWidget/NftWidgetContent.styles.ts

@@ -0,0 +1,54 @@
+import styled from '@emotion/styled'
+
+import { cVar, sizes, zIndex } from '@/styles'
+
+export const sizeObj = { small: 'small', medium: 'medium' } as const
+export type Size = keyof typeof sizeObj
+
+export type SizeProps = { 'data-size': keyof typeof sizeObj }
+
+export const ButtonGrid = styled.div<SizeProps & { 'data-two-columns'?: boolean }>`
+  display: grid;
+  gap: ${sizes(4)};
+
+  &[data-size=${sizeObj.small}] {
+    gap: ${sizes(2)};
+  }
+
+  &[data-two-columns='true'] {
+    grid-template-columns: 1fr 1fr;
+  }
+`
+
+export const TopBidderContainer = styled.div`
+  display: flex;
+`
+
+export const TopBidderTokenContainer = styled.div<SizeProps>`
+  display: flex;
+  align-items: center;
+  position: relative;
+  left: -4px;
+  z-index: ${zIndex.overlay};
+
+  &::before {
+    display: inline-block;
+    position: absolute;
+    content: '';
+    width: 28px;
+    height: 28px;
+    background: ${cVar('colorBackgroundMuted')};
+    border-radius: 100%;
+    left: -2px;
+    top: -2px;
+  }
+
+  &[data-size=${sizeObj.small}] {
+    &::before {
+      width: 21px;
+      height: 21px;
+      left: -2.5px;
+      top: 1.5px;
+    }
+  }
+`

+ 542 - 0
packages/atlas/src/components/_nft/NftWidget/NftWidgetContent.tsx

@@ -0,0 +1,542 @@
+import BN from 'bn.js'
+import { differenceInSeconds } from 'date-fns'
+import { FC, memo } from 'react'
+
+import { FullBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
+import { SvgAlertsInformative24 } from '@/assets/icons'
+import { Avatar } from '@/components/Avatar'
+import { Banner } from '@/components/Banner'
+import { JoyTokenIcon } from '@/components/JoyTokenIcon'
+import { GridItem } from '@/components/LayoutGrid'
+import { NumberFormat } from '@/components/NumberFormat'
+import { Text } from '@/components/Text'
+import { Button } from '@/components/_buttons/Button'
+import { absoluteRoutes } from '@/config/routes'
+import { useMsTimestamp } from '@/hooks/useMsTimestamp'
+import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
+import { formatDateTime, formatDurationShort, formatTime } from '@/utils/time'
+
+import { NftInfoItem, NftTimerItem } from './NftInfoItem'
+import { OwnerHandle } from './NftWidget.styles'
+import { NftWidgetStatus } from './NftWidget.types'
+import { ButtonGrid, TopBidderContainer, TopBidderTokenContainer } from './NftWidgetContent.styles'
+
+type Size = 'small' | 'medium'
+
+type NftWidgetContentProps = {
+  nftStatus: NftWidgetStatus | undefined
+  ownerHandle: string | null | undefined
+  isOwner: boolean | undefined
+  needsSettling: boolean | undefined
+  onNftPutOnSale?: () => void
+  onNftAcceptBid?: () => void
+  onNftPurchase?: () => void
+  onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
+  bidFromPreviousAuction: FullBidFieldsFragment | undefined
+  onNftSettlement?: () => void
+  onNftBuyNow?: () => void
+  onNftCancelSale?: () => void
+  onNftChangePrice?: () => void
+  userBidCreatedAt?: Date
+  userBidAmount?: BN
+  size: Size
+}
+
+export const NftWidgetContent: FC<NftWidgetContentProps> = memo(
+  ({
+    nftStatus,
+    isOwner,
+    ownerHandle,
+    needsSettling,
+    onNftPutOnSale,
+    onNftAcceptBid,
+    onNftPurchase,
+    onWithdrawBid,
+    bidFromPreviousAuction,
+    onNftSettlement,
+    onNftBuyNow,
+    onNftCancelSale,
+    onNftChangePrice,
+    userBidCreatedAt,
+    size,
+    userBidAmount,
+  }) => {
+    const { convertHapiToUSD, isLoadingPrice } = useTokenPrice()
+    const shouldIgnoreTimestamp = nftStatus?.status === 'auction' && !nftStatus.startsAtDate
+    const timestamp = useMsTimestamp({
+      shouldStop: shouldIgnoreTimestamp,
+    })
+    if (!nftStatus) {
+      return null
+    }
+
+    const contentTextVariant = size === 'small' ? 'h400' : 'h600'
+    const buttonSize = size === 'small' ? 'medium' : 'large'
+    const buttonColumnSpan = size === 'small' ? 1 : 2
+    const timerColumnSpan = size === 'small' ? 1 : 2
+
+    switch (nftStatus.status) {
+      case 'idle':
+        return (
+          <>
+            {nftStatus.lastSalePrice ? (
+              <NftInfoItem
+                size={size}
+                label="Last price"
+                content={
+                  <NumberFormat
+                    as="span"
+                    value={nftStatus.lastSalePrice}
+                    format="short"
+                    variant={contentTextVariant}
+                    icon={<JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />}
+                    color="colorText"
+                    withDenomination
+                  />
+                }
+                secondaryText={nftStatus.lastSaleDate && formatDateTime(nftStatus.lastSaleDate)}
+              />
+            ) : (
+              <NftInfoItem
+                size={size}
+                label="status"
+                content={
+                  <Text as="span" variant={contentTextVariant} color="colorText">
+                    Not for sale
+                  </Text>
+                }
+              />
+            )}
+            {bidFromPreviousAuction && (
+              <>
+                <InfoBanner
+                  size={size}
+                  title="Withdraw your bid"
+                  description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
+                />
+                <WithdrawBidFromPreviousAuction size={size} bidFromPreviousAuction={bidFromPreviousAuction} />
+              </>
+            )}
+            {isOwner && (
+              <GridItem colSpan={buttonColumnSpan}>
+                <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPutOnSale}>
+                  Start sale of this NFT
+                </Button>
+              </GridItem>
+            )}
+          </>
+        )
+      case 'buy-now':
+        return (
+          <>
+            <BuyNow buyNowPrice={nftStatus.buyNowPrice} size={size} />
+
+            <GridItem colSpan={buttonColumnSpan}>
+              <ButtonGrid data-size={size}>
+                {isOwner ? (
+                  <>
+                    <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftChangePrice}>
+                      Change price
+                    </Button>
+                    <Button fullWidth variant="destructive" size={buttonSize} onClick={onNftCancelSale}>
+                      Remove from sale
+                    </Button>
+                  </>
+                ) : (
+                  <GridItem colSpan={buttonColumnSpan}>
+                    <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
+                      Buy now
+                    </Button>
+                  </GridItem>
+                )}
+                {bidFromPreviousAuction && (
+                  <>
+                    <InfoBanner
+                      size={size}
+                      title="Withdraw your bid"
+                      description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
+                    />
+                    <WithdrawBidFromPreviousAuction
+                      secondary
+                      size={size}
+                      bidFromPreviousAuction={bidFromPreviousAuction}
+                    />
+                  </>
+                )}
+              </ButtonGrid>
+            </GridItem>
+          </>
+        )
+      case 'auction': {
+        const getInfoBannerProps = () => {
+          const hasBids = !nftStatus.topBid?.isCanceled && nftStatus.topBidAmount?.gtn(0)
+          if (nftStatus.type === 'open' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid to participate',
+              description:
+                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
+            }
+          }
+
+          if (nftStatus.englishTimerState === 'expired' && isOwner && !hasBids) {
+            return {
+              title: 'Auction ended',
+              description: 'This auction has ended and no one placed a bid. You can now remove this NFT from sale.',
+            }
+          }
+          if (nftStatus.englishTimerState === 'expired' && !bidFromPreviousAuction && !hasBids && !isOwner) {
+            return {
+              title: 'Auction ended',
+              description:
+                "This auction has ended and no one placed a bid. We're waiting for the NFT owner to remove this NFT from sale.",
+            }
+          }
+          if (
+            nftStatus.englishTimerState === 'expired' &&
+            !bidFromPreviousAuction &&
+            hasBids &&
+            !isOwner &&
+            !nftStatus.isUserTopBidder
+          ) {
+            return {
+              title: 'Auction ended',
+              description:
+                'We are waiting for this auction to be settled by the auction winner or the current NFT owner.',
+            }
+          }
+          if (nftStatus.englishTimerState === 'expired' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid',
+              description: 'You placed a bid in a previous auction that you can now withdraw.',
+            }
+          }
+
+          if (nftStatus.englishTimerState === 'running' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid to participate',
+              description:
+                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
+            }
+          }
+
+          if (nftStatus.englishTimerState === 'upcoming' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid to participate',
+              description:
+                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this upcoming auction.',
+            }
+          }
+
+          if (nftStatus.isUserWhitelisted === false) {
+            return {
+              title: "You're not on the whitelist",
+              description: `This sale is available only to members whitelisted by ${ownerHandle}.`,
+            }
+          }
+
+          return null
+        }
+        const infoBannerProps = getInfoBannerProps()
+
+        const infoTextNode = !!nftStatus.userBidAmount?.gtn(0) && nftStatus.userBidUnlockDate && (
+          <GridItem colSpan={buttonColumnSpan}>
+            {nftStatus.type === 'english' ? (
+              <BidPlacingInfoText />
+            ) : (
+              <Text as="p" variant="t100" color="colorText" align="center">
+                {nftStatus.canWithdrawBid ? `Your last bid: ` : `Your last bid (`}
+                <NumberFormat as="span" value={nftStatus.userBidAmount} format="short" withToken />
+                {nftStatus.canWithdrawBid
+                  ? ''
+                  : `) becomes withdrawable on ${formatDateTime(nftStatus.userBidUnlockDate)}`}
+              </Text>
+            )}
+          </GridItem>
+        )
+
+        const topBidAmountInUsd = nftStatus.topBidAmount && convertHapiToUSD(nftStatus.topBidAmount)
+
+        return (
+          <>
+            {nftStatus.topBidAmount?.gtn(0) && !nftStatus.topBid?.isCanceled ? (
+              <NftInfoItem
+                size={size}
+                label="Top bid"
+                content={
+                  <>
+                    <TopBidderContainer>
+                      <Avatar assetUrls={nftStatus.topBidderAvatarUris} size={24} />
+                      <TopBidderTokenContainer data-size={size}>
+                        <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
+                      </TopBidderTokenContainer>
+                    </TopBidderContainer>
+                    <NumberFormat
+                      as="span"
+                      format="short"
+                      value={nftStatus.topBidAmount}
+                      variant={contentTextVariant}
+                    />
+                  </>
+                }
+                secondaryText={
+                  !isLoadingPrice && nftStatus.topBidderHandle ? (
+                    <>
+                      {topBidAmountInUsd ? (
+                        <NumberFormat as="span" color="colorText" format="dollar" value={topBidAmountInUsd} />
+                      ) : null}{' '}
+                      from{' '}
+                      <OwnerHandle to={absoluteRoutes.viewer.member(nftStatus.topBidderHandle)}>
+                        <Text as="span" variant="t100">
+                          {nftStatus.isUserTopBidder ? 'you' : nftStatus.topBidderHandle}
+                        </Text>
+                      </OwnerHandle>
+                    </>
+                  ) : null
+                }
+              />
+            ) : (
+              <NftInfoItem
+                size={size}
+                label="Starting Price"
+                content={
+                  <NumberFormat
+                    as="span"
+                    format="short"
+                    value={nftStatus.startingPrice}
+                    variant={contentTextVariant}
+                    icon={<JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />}
+                    withDenomination
+                  />
+                }
+              />
+            )}
+            <BuyNow buyNowPrice={nftStatus.buyNowPrice} size={size} />
+
+            {nftStatus.englishTimerState === 'expired' && (
+              <GridItem colSpan={timerColumnSpan}>
+                <NftInfoItem
+                  size={size}
+                  label="Auction ended on"
+                  loading={!nftStatus.auctionPlannedEndDate}
+                  content={
+                    nftStatus.auctionPlannedEndDate && (
+                      <Text as="span" variant={contentTextVariant} color="colorText">
+                        {formatDateTime(nftStatus.auctionPlannedEndDate)}
+                      </Text>
+                    )
+                  }
+                />
+              </GridItem>
+            )}
+            {nftStatus.englishTimerState === 'running' && nftStatus?.auctionPlannedEndDate && (
+              <GridItem colSpan={timerColumnSpan}>
+                <NftTimerItem size={size} time={nftStatus.auctionPlannedEndDate} />
+              </GridItem>
+            )}
+            {nftStatus.startsAtBlock && nftStatus.auctionBeginsInSeconds >= 0 && (
+              <GridItem colSpan={timerColumnSpan}>
+                <NftInfoItem
+                  size={size}
+                  label="Auction begins on"
+                  loading={!nftStatus.startsAtDate}
+                  content={
+                    nftStatus.startsAtDate && (
+                      <Text as="span" variant={contentTextVariant} color="colorText">
+                        {nftStatus.auctionBeginsInDays > 1 && formatDateTime(nftStatus.startsAtDate)}
+                        {nftStatus.auctionBeginsInDays === 1 && `Tomorrow at ${formatTime(nftStatus.startsAtDate)}`}
+                        {nftStatus.auctionBeginsInDays < 1 &&
+                          formatDurationShort(differenceInSeconds(nftStatus.startsAtDate, timestamp))}
+                      </Text>
+                    )
+                  }
+                />
+              </GridItem>
+            )}
+
+            {nftStatus.hasTimersLoaded && infoBannerProps && <InfoBanner size={size} {...infoBannerProps} />}
+
+            {nftStatus.hasTimersLoaded && needsSettling && (nftStatus.isUserTopBidder || isOwner) && (
+              <GridItem colSpan={buttonColumnSpan}>
+                <Button fullWidth size={buttonSize} onClick={onNftSettlement}>
+                  Settle auction
+                </Button>
+              </GridItem>
+            )}
+
+            {nftStatus.hasTimersLoaded && bidFromPreviousAuction && (
+              <WithdrawBidFromPreviousAuction size={size} bidFromPreviousAuction={bidFromPreviousAuction} />
+            )}
+
+            {nftStatus.hasTimersLoaded &&
+              !needsSettling &&
+              !bidFromPreviousAuction &&
+              (isOwner
+                ? (nftStatus.type === 'open' ||
+                    // english auction with no bids
+                    !nftStatus.topBidAmount ||
+                    nftStatus.topBid?.isCanceled) && (
+                    <GridItem colSpan={buttonColumnSpan}>
+                      <ButtonGrid data-size={size}>
+                        {nftStatus.type === 'open' && nftStatus.topBid && !nftStatus.topBid?.isCanceled && (
+                          <Button fullWidth size={buttonSize} onClick={onNftAcceptBid}>
+                            Review and accept bid
+                          </Button>
+                        )}
+                        <Button
+                          fullWidth
+                          onClick={onNftCancelSale}
+                          variant={
+                            nftStatus.type === 'open' && !nftStatus.topBid?.isCanceled
+                              ? 'destructive-secondary'
+                              : 'destructive'
+                          }
+                          size={buttonSize}
+                        >
+                          Remove from sale
+                        </Button>
+                      </ButtonGrid>
+                    </GridItem>
+                  )
+                : nftStatus.englishTimerState === 'running' &&
+                  nftStatus.isUserWhitelisted !== false &&
+                  (nftStatus.buyNowPrice?.gtn(0) ? (
+                    <GridItem colSpan={buttonColumnSpan}>
+                      <ButtonGrid data-size={size} data-two-columns={size === 'medium'}>
+                        <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPurchase}>
+                          {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
+                        </Button>
+                        <Button fullWidth size={buttonSize} onClick={onNftBuyNow}>
+                          Buy now
+                        </Button>
+                        {/* second row button */}
+                        {nftStatus.canWithdrawBid && (
+                          <GridItem colSpan={buttonColumnSpan}>
+                            <Button
+                              fullWidth
+                              size={buttonSize}
+                              variant="destructive-secondary"
+                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
+                            >
+                              Withdraw bid
+                            </Button>
+                          </GridItem>
+                        )}
+
+                        {infoTextNode}
+                      </ButtonGrid>
+                    </GridItem>
+                  ) : (
+                    <GridItem colSpan={buttonColumnSpan}>
+                      <ButtonGrid data-size={size}>
+                        <GridItem colSpan={buttonColumnSpan}>
+                          <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
+                            {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
+                          </Button>
+                        </GridItem>
+                        {nftStatus.canWithdrawBid && (
+                          <GridItem colSpan={buttonColumnSpan}>
+                            <Button
+                              fullWidth
+                              size={buttonSize}
+                              variant="destructive-secondary"
+                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
+                            >
+                              Withdraw bid
+                            </Button>
+                          </GridItem>
+                        )}
+                        {infoTextNode}
+                      </ButtonGrid>
+                    </GridItem>
+                  )))}
+          </>
+        )
+      }
+    }
+  }
+)
+
+NftWidgetContent.displayName = 'NftWidgetContent'
+
+const BidPlacingInfoText = () => (
+  <Text as="p" variant="t100" color="colorText" align="center">
+    Placing a bid will withdraw your last bid
+  </Text>
+)
+
+export const BuyNow = memo(({ buyNowPrice, size }: { buyNowPrice?: BN; size: Size }) => {
+  const contentTextVariant = size === 'small' ? 'h400' : 'h600'
+  return buyNowPrice?.gtn(0) ? (
+    <NftInfoItem
+      size={size}
+      label="Buy now"
+      content={
+        <NumberFormat
+          icon={<JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />}
+          withDenomination
+          as="span"
+          value={buyNowPrice}
+          format="short"
+          variant={contentTextVariant}
+        />
+      }
+    />
+  ) : null
+})
+BuyNow.displayName = 'BuyNow'
+
+const InfoBanner = ({ title, description, size }: { title: string; description: string; size: Size }) => {
+  const buttonColumnSpan = size === 'small' ? 1 : 2
+  return (
+    <GridItem colSpan={buttonColumnSpan}>
+      <Banner icon={<SvgAlertsInformative24 />} {...{ title, description }} />
+    </GridItem>
+  )
+}
+
+const WithdrawBidFromPreviousAuction = memo(
+  ({
+    secondary,
+    bidFromPreviousAuction,
+    size,
+    onWithdrawBid,
+  }: {
+    size: Size
+    secondary?: boolean
+    bidFromPreviousAuction: FullBidFieldsFragment | undefined
+    onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
+  }) => {
+    const buttonColumnSpan = size === 'small' ? 1 : 2
+    const buttonSize = size === 'small' ? 'medium' : 'large'
+    return bidFromPreviousAuction ? (
+      <>
+        <GridItem colSpan={buttonColumnSpan}>
+          <Button
+            variant={secondary ? 'secondary' : undefined}
+            fullWidth
+            size={buttonSize}
+            onClick={() =>
+              onWithdrawBid?.(new BN(bidFromPreviousAuction.amount), new Date(bidFromPreviousAuction.createdAt))
+            }
+          >
+            Withdraw last bid
+          </Button>
+          <Text as="p" margin={{ top: 2 }} variant="t100" color="colorText" align="center">
+            You bid{' '}
+            <NumberFormat
+              as="span"
+              value={new BN(bidFromPreviousAuction?.amount)}
+              format="short"
+              variant="t100"
+              color="colorText"
+              withToken
+            />{' '}
+            on {formatDateTime(new Date(bidFromPreviousAuction.createdAt))}
+          </Text>
+        </GridItem>
+      </>
+    ) : null
+  }
+)
+
+WithdrawBidFromPreviousAuction.displayName = 'WithdrawBidFromPreviousAuction'

+ 3 - 3
packages/atlas/src/components/_notifications/NotificationTile/NotificationTile.tsx

@@ -81,7 +81,7 @@ export const NotificationTile: FC<NotificationProps> = ({
   className,
 }) => {
   const { date, video, member, read } = notification
-  const { url: avatarUrl, isLoadingAsset: isLoadingAvatar } = getMemberAvatar(member)
+  const { urls: avatarUrls, isLoadingAsset: isLoadingAvatar } = getMemberAvatar(member)
 
   const formattedDate = useMemo(() => {
     const differenceDays = differenceInDays(new Date(), date)
@@ -111,7 +111,7 @@ export const NotificationTile: FC<NotificationProps> = ({
           variant="compact"
           nodeStart={
             member ? (
-              <Avatar size={32} assetUrl={avatarUrl} loading={isLoadingAvatar || loading} />
+              <Avatar size={32} assetUrls={avatarUrls} loading={isLoadingAvatar || loading} />
             ) : (
               <NoActorNotificationAvatar size="small" />
             )
@@ -155,7 +155,7 @@ export const NotificationTile: FC<NotificationProps> = ({
       )}
       <AvatarWrapper>
         {member ? (
-          <Avatar size={40} assetUrl={avatarUrl} loading={isLoadingAvatar || loading} />
+          <Avatar size={40} assetUrls={avatarUrls} loading={isLoadingAvatar || loading} />
         ) : (
           <NoActorNotificationAvatar size="regular" />
         )}

+ 14 - 14
packages/atlas/src/components/_overlays/AcceptBidDialog/AcceptBidList.tsx

@@ -7,12 +7,11 @@ import { NumberFormat } from '@/components/NumberFormat'
 import { Text } from '@/components/Text'
 import { RadioInput } from '@/components/_inputs/RadioInput'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
-import { hapiBnToTokenNumber } from '@/joystream-lib/utils'
 import { getMemberAvatar } from '@/providers/assets/assets.helpers'
 import { formatDateTime } from '@/utils/time'
 
 import { Bid, SelectedBid } from './AcceptBidDialog.types'
-import { BidRowWrapper, Price, TokenPrice } from './AcceptBidList.styles'
+import { BidRowWrapper, Price } from './AcceptBidList.styles'
 
 type BidRowProps = {
   selectedBid?: SelectedBid
@@ -43,10 +42,10 @@ export const AcceptBidList: FC<AcceptBidListProps> = ({ items, onSelect, selecte
   )
 }
 
-export const BidRow: FC<BidRowProps> = ({ bidder, createdAt, amount, amountUSD, selectedBid, onSelect }) => {
+export const BidRow: FC<BidRowProps> = ({ bidder, createdAt, amount, selectedBid, onSelect }) => {
   const xsMatch = useMediaMatch('xs')
   const selected = selectedBid?.bidderId === bidder.id
-  const { url, isLoadingAsset } = getMemberAvatar(bidder)
+  const { urls, isLoadingAsset } = getMemberAvatar(bidder)
   return (
     <BidRowWrapper selected={selected} onClick={() => onSelect?.(bidder.id, amount)}>
       <RadioInput
@@ -54,7 +53,7 @@ export const BidRow: FC<BidRowProps> = ({ bidder, createdAt, amount, amountUSD,
         value={bidder.id}
         onChange={() => onSelect?.(bidder.id, amount)}
       />
-      {xsMatch && <Avatar assetUrl={url} loading={isLoadingAsset} size={40} />}
+      {xsMatch && <Avatar assetUrls={urls} loading={isLoadingAsset} size={40} />}
       <div>
         <Text as="p" variant="h300" color={!selected ? 'colorText' : undefined} margin={{ bottom: 1 }}>
           {bidder?.handle}
@@ -64,15 +63,16 @@ export const BidRow: FC<BidRowProps> = ({ bidder, createdAt, amount, amountUSD,
         </Text>
       </div>
       <Price>
-        <TokenPrice>
-          <JoyTokenIcon variant={selected ? 'regular' : 'gray'} />
-          <Text as="p" variant="h300" margin={{ left: 1 }} color={!selected ? 'colorText' : undefined}>
-            {hapiBnToTokenNumber(amount)}
-          </Text>
-        </TokenPrice>
-        {amountUSD !== null && (
-          <NumberFormat value={amountUSD} format="dollar" as="p" variant="t100" color="colorText" />
-        )}
+        <NumberFormat
+          value={amount}
+          icon={<JoyTokenIcon variant={selected ? 'regular' : 'gray'} />}
+          format="short"
+          as="p"
+          variant="h300"
+          color={!selected ? 'colorText' : undefined}
+          withDenomination
+          denominationAlign="right"
+        />
       </Price>
     </BidRowWrapper>
   )

+ 1 - 1
packages/atlas/src/components/_overlays/ImageCropModal/ImageCropModal.stories.tsx

@@ -85,7 +85,7 @@ const RegularTemplate: StoryFn<ImageCropModalProps> = () => {
   return (
     <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'start', gap: '24px' }}>
       <Avatar
-        assetUrl={avatarImage?.url}
+        assetUrls={avatarImage?.url ? [avatarImage.url] : undefined}
         editable
         onClick={() => avatarDialogRef.current?.open(avatarImage?.originalBlob, avatarImage?.cropData, true)}
         size={88}

+ 6 - 3
packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdown.tsx

@@ -7,6 +7,7 @@ import { useTransition } from 'react-spring'
 import useResizeObserver from 'use-resize-observer'
 
 import { absoluteRoutes } from '@/config/routes'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { getMemberAvatar } from '@/providers/assets/assets.helpers'
 import { useSubscribeAccountBalance } from '@/providers/joystream/joystream.hooks'
 import { useUser } from '@/providers/user/user.hooks'
@@ -30,11 +31,12 @@ export const MemberDropdown = forwardRef<HTMLDivElement, MemberDropdownProps>(
     const navigate = useNavigate()
     const { channelId, activeMembership, memberships, signOut, setActiveUser, setSignInModalOpen, membershipsLoading } =
       useUser()
+    const { identifyUser } = useSegmentAnalytics()
     const [showWithdrawDialog, setShowWithdrawDialog] = useState(false)
     const [disableScrollDuringAnimation, setDisableScrollDuringAnimation] = useState(false)
 
     const [showSendDialog, setShowSendDialog] = useState(false)
-    const { url: memberAvatarUrl } = getMemberAvatar(activeMembership)
+    const { urls: memberAvatarUrls } = getMemberAvatar(activeMembership)
     const selectedChannel = activeMembership?.channels.find((chanel) => chanel.id === channelId)
 
     const memoizedChannelStateBloatBond = useMemo(() => {
@@ -77,13 +79,14 @@ export const MemberDropdown = forwardRef<HTMLDivElement, MemberDropdownProps>(
     const handleMemberChange = useCallback(
       (memberId: string, accountId: string, channelId: string | null) => {
         setActiveUser({ accountId, memberId, channelId })
+        identifyUser(memberId)
         setIsList(false)
 
         if (publisher) {
           navigate(absoluteRoutes.studio.index())
         }
       },
-      [navigate, publisher, setActiveUser]
+      [identifyUser, navigate, publisher, setActiveUser]
     )
 
     const handleAddNewChannel = useCallback(() => {
@@ -144,7 +147,7 @@ export const MemberDropdown = forwardRef<HTMLDivElement, MemberDropdownProps>(
     return (
       <>
         <WithdrawFundsDialog
-          avatarUrl={memberAvatarUrl}
+          avatarUrls={memberAvatarUrls}
           activeMembership={activeMembership}
           show={showWithdrawDialog}
           onExitClick={toggleWithdrawDialog}

+ 3 - 3
packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownList.tsx

@@ -68,7 +68,7 @@ export const MemberDropdownList: FC<MemberDropdownListProps> = ({
               <ListItem
                 key={channel.id}
                 onClick={() => onChannelChange?.(channel.id)}
-                nodeStart={<Avatar assetUrl={channel.avatarPhoto?.resolvedUrl} size={32} />}
+                nodeStart={<Avatar assetUrls={channel.avatarPhoto?.resolvedUrls} size={32} />}
                 label={channel?.title ?? ''}
                 caption={channel ? `${channel?.followsNum} followers` : undefined}
                 selected={channel.id === channelId}
@@ -97,11 +97,11 @@ type MemberListItemProps = {
   onClick: () => void
 }
 const MemberListItem: FC<MemberListItemProps> = ({ member, selected, onClick }) => {
-  const { url, isLoadingAsset } = getMemberAvatar(member)
+  const { urls, isLoadingAsset } = getMemberAvatar(member)
   return (
     <ListItem
       onClick={onClick}
-      nodeStart={<Avatar assetUrl={url} loading={isLoadingAsset} />}
+      nodeStart={<Avatar assetUrls={urls} loading={isLoadingAsset} />}
       label={member.handle ?? ''}
       selected={selected}
       asButton

+ 22 - 10
packages/atlas/src/components/_overlays/MemberDropdown/MemberDropdownNav.tsx

@@ -93,8 +93,8 @@ export const MemberDropdownNav: FC<MemberDropdownNavProps> = ({
 }) => {
   const navigate = useNavigate()
   const selectedChannel = activeMembership?.channels.find((chanel) => chanel.id === channelId)
-  const { url: memberAvatarUrl, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(activeMembership)
-  const channelAvatarUrl = selectedChannel?.avatarPhoto?.resolvedUrl
+  const { urls: memberAvatarUrls, isLoadingAsset: memberAvatarLoading } = getMemberAvatar(activeMembership)
+  const channelAvatarUrls = selectedChannel?.avatarPhoto?.resolvedUrls
   const setSignInModalOpen = useUserStore((state) => state.actions.setSignInModalOpen)
   const memberAvatarWrapperRef = useRef<HTMLButtonElement>(null)
   const channelAvatarWrapperRef = useRef<HTMLButtonElement>(null)
@@ -148,7 +148,7 @@ export const MemberDropdownNav: FC<MemberDropdownNavProps> = ({
                 clickable={false}
                 isDisabled={type === 'channel'}
                 size={40}
-                assetUrl={memberAvatarUrl}
+                assetUrls={memberAvatarUrls}
                 loading={memberAvatarLoading}
               />
               <StyledIconWrapper size="small" icon={<SvgActionMember />} />
@@ -172,7 +172,7 @@ export const MemberDropdownNav: FC<MemberDropdownNavProps> = ({
                 clickable={false}
                 isDisabled={type === 'member'}
                 size={40}
-                assetUrl={channelAvatarUrl}
+                assetUrls={channelAvatarUrls}
                 loading={membershipLoading}
               >
                 {!hasAtLeastOneChannel ? (
@@ -193,10 +193,14 @@ export const MemberDropdownNav: FC<MemberDropdownNavProps> = ({
                       {selectedChannel?.title}
                     </MemberHandleText>
                     {channelBalance !== undefined ? (
-                      <UserBalance>
-                        <JoyTokenIcon size={16} variant="regular" />
-                        <NumberFormat as="span" variant="t200-strong" value={channelBalance} format="short" />
-                      </UserBalance>
+                      <NumberFormat
+                        icon={<JoyTokenIcon size={16} variant="regular" />}
+                        as="span"
+                        variant="t200-strong"
+                        value={channelBalance}
+                        format="short"
+                        withDenomination
+                      />
                     ) : (
                       <SkeletonLoader width={30} height={20} />
                     )}
@@ -213,14 +217,22 @@ export const MemberDropdownNav: FC<MemberDropdownNavProps> = ({
                     >
                       {accountBalance !== undefined ? (
                         <UserBalance>
-                          <JoyTokenIcon isNegative={isInDebt} size={16} variant="regular" withoutInformationTooltip />
                           <NumberFormat
+                            icon={
+                              <JoyTokenIcon
+                                isNegative={isInDebt}
+                                size={16}
+                                variant="regular"
+                                withoutInformationTooltip
+                              />
+                            }
                             withTooltip={false}
                             as="span"
                             variant="t200-strong"
                             value={accountBalance}
                             format="short"
                             isNegative={isInDebt}
+                            withDenomination
                           />
                         </UserBalance>
                       ) : (
@@ -264,7 +276,7 @@ export const MemberDropdownNav: FC<MemberDropdownNavProps> = ({
             </TextLink>
           </BalanceContainer>
         </MemberInfoContainer>
-        <BlurredBG memberUrl={memberAvatarUrl} channelUrl={channelAvatarUrl} isChannel={type === 'channel'}>
+        <BlurredBG memberUrl={memberAvatarUrls?.[0]} channelUrl={channelAvatarUrls?.[0]} isChannel={type === 'channel'}>
           <Filter />
         </BlurredBG>
       </MemberInfoAndBgWrapper>

+ 2 - 2
packages/atlas/src/components/_overlays/SendTransferDialogs/SendFundsDialog.tsx

@@ -247,10 +247,10 @@ type ResolvedAvatarProps = {
   member: BasicMembershipFieldsFragment
 }
 const ResolvedAvatar: FC<ResolvedAvatarProps> = ({ member }) => {
-  const { url, isLoadingAsset } = getMemberAvatar(member)
+  const { urls, isLoadingAsset } = getMemberAvatar(member)
   return (
     <Tooltip text={member?.handle} placement="top">
-      <Avatar assetUrl={url} loading={isLoadingAsset} size={24} />
+      <Avatar assetUrls={urls} loading={isLoadingAsset} size={24} />
     </Tooltip>
   )
 }

+ 3 - 3
packages/atlas/src/components/_overlays/SendTransferDialogs/WithdrawFundsDialog.tsx

@@ -32,7 +32,7 @@ type WithdrawFundsDialogProps = {
   show: boolean
   totalBalance?: BN
   channelBalance?: BN
-  avatarUrl?: string | null
+  avatarUrls?: string[] | null
   channelId?: string | null
 }
 
@@ -42,7 +42,7 @@ export const WithdrawFundsDialog: FC<WithdrawFundsDialogProps> = ({
   onExitClick,
   activeMembership,
   show,
-  avatarUrl,
+  avatarUrls,
   totalBalance = new BN(0),
   channelBalance = new BN(0),
   channelId,
@@ -186,7 +186,7 @@ export const WithdrawFundsDialog: FC<WithdrawFundsDialogProps> = ({
             Destination account
           </Text>
           <VerticallyCenteredDiv>
-            <StyledAvatar assetUrl={avatarUrl} />
+            <StyledAvatar assetUrls={avatarUrls} />
             <Text as="span" variant="t100" margin={{ left: 2, right: 1 }}>
               {activeMembership?.handle}
             </Text>

+ 4 - 2
packages/atlas/src/components/_video/BackgroundVideoPlayer/BackgroundVideoPlayer.styles.ts

@@ -3,6 +3,8 @@ import { css } from '@emotion/react'
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
+import { AssetImage } from '@/components/AssetImage'
+import { AssetVideo } from '@/components/AssetVideo/AssetVideo'
 import { cVar, media, sizes, zIndex } from '@/styles'
 
 type VideoWrapperProps = {
@@ -13,7 +15,7 @@ export const VideoWrapper = styled.div<VideoWrapperProps>`
   overflow: hidden;
 `
 
-export const VideoPoster = styled.img`
+export const VideoPoster = styled(AssetImage)`
   position: absolute;
   object-fit: cover;
   max-height: 100%;
@@ -29,7 +31,7 @@ export const VideoPoster = styled.img`
   }
 `
 
-export const StyledVideo = styled.video`
+export const StyledVideo = styled(AssetVideo)`
   position: absolute;
   object-fit: cover;
   max-height: 100%;

+ 6 - 4
packages/atlas/src/components/_video/BackgroundVideoPlayer/BackgroundVideoPlayer.tsx

@@ -17,7 +17,9 @@ type BackgroundVideoPlayerProps = {
   videoPlaytime?: number
   videoId?: string
   withFade?: boolean
-} & VideoHTMLAttributes<HTMLVideoElement>
+  src: string[]
+  poster: string[]
+} & Omit<VideoHTMLAttributes<HTMLVideoElement>, 'src' | 'poster'>
 
 export const BackgroundVideoPlayer: FC<BackgroundVideoPlayerProps> = ({
   autoPlay,
@@ -110,13 +112,13 @@ export const BackgroundVideoPlayer: FC<BackgroundVideoPlayerProps> = ({
       {playing && <VideoProgress video={videoRef.current} isPlaying={isPlaying} tick={10} limit={videoPlaytime} />}
       <StyledLink withFade={withFade} to={videoId ? absoluteRoutes.viewer.video(videoId) : ''}>
         <StyledVideo
-          src={src}
+          resolvedVideoUrls={src}
           autoPlay={autoPlay}
           playsInline={playsInline}
           ref={videoRef}
           onEnded={handleEnded}
           onPlay={handlePlay}
-          poster={poster}
+          resolvedPosterUrls={poster}
           {...props}
           muted={handleActions ? isMuted : props.muted}
         />
@@ -128,7 +130,7 @@ export const BackgroundVideoPlayer: FC<BackgroundVideoPlayerProps> = ({
             classNames={transitions.names.fade}
             timeout={300}
           >
-            <VideoPoster src={poster} alt="" />
+            <VideoPoster resolvedUrls={poster} alt="" />
           </CSSTransition>
         )}
       </StyledLink>

+ 2 - 2
packages/atlas/src/components/_video/VideoHero/VideoHero.tsx

@@ -125,9 +125,9 @@ export const VideoHero: FC<VideoHeroProps> = ({
             muted={soundMuted}
             autoPlay
             onTimeUpdate={onTimeUpdate}
-            poster={videoHeroData.heroPosterUrl ?? undefined}
+            poster={[videoHeroData.heroPosterUrl ?? '']}
             onEnded={handleEnded}
-            src={videoHeroData?.heroVideoCutUrl}
+            src={[videoHeroData?.heroVideoCutUrl]}
           />
         )}
         <GradientOverlay withSolidOverlay />

+ 3 - 3
packages/atlas/src/components/_video/VideoPlayer/VideoOverlay.tsx

@@ -16,7 +16,7 @@ type VideoOverlayProps = {
   playerState: PlayerState
   onPlayAgain: () => void
   channelId?: string
-  currentThumbnailUrl?: string | null
+  currentThumbnailUrls?: string[] | null
   videoId?: string
   isFullScreen?: boolean
   isPlayNextDisabled?: boolean
@@ -29,7 +29,7 @@ export const VideoOverlay: FC<VideoOverlayProps> = ({
   playerState,
   onPlayAgain,
   channelId,
-  currentThumbnailUrl,
+  currentThumbnailUrls,
   videoId,
   isFullScreen,
   isPlayNextDisabled,
@@ -87,7 +87,7 @@ export const VideoOverlay: FC<VideoOverlayProps> = ({
               isPlayNextDisabled={isPlayNextDisabled}
               onPlayAgain={onPlayAgain}
               channelId={channelId}
-              currentThumbnailUrl={currentThumbnailUrl}
+              currentThumbnailUrls={currentThumbnailUrls}
               randomNextVideo={playRandomVideoOnEnded ? randomNextVideo : undefined}
             />
           )}

+ 2 - 1
packages/atlas/src/components/_video/VideoPlayer/VideoOverlays/EndingOverlay.styles.ts

@@ -1,5 +1,6 @@
 import styled from '@emotion/styled'
 
+import { AssetImage } from '@/components/AssetImage'
 import { CircularProgress } from '@/components/CircularProgress'
 import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
@@ -163,7 +164,7 @@ export const RestartButton = styled(Button)`
   }
 `
 
-export const VideoThumbnail = styled.img`
+export const VideoThumbnail = styled(AssetImage)`
   width: 320px;
   display: none;
 

+ 8 - 8
packages/atlas/src/components/_video/VideoPlayer/VideoOverlays/EndingOverlay.tsx

@@ -25,7 +25,7 @@ import {
 
 type EndingOverlayProps = {
   channelId?: string
-  currentThumbnailUrl?: string | null
+  currentThumbnailUrls?: string[] | null
   isFullScreen?: boolean
   onPlayAgain?: () => void
   randomNextVideo?: BasicVideoFieldsFragment | null
@@ -39,7 +39,7 @@ export const EndingOverlay: FC<EndingOverlayProps> = ({
   onPlayAgain,
   isFullScreen,
   channelId,
-  currentThumbnailUrl,
+  currentThumbnailUrls,
   randomNextVideo,
   isEnded,
   isPlayNextDisabled,
@@ -49,7 +49,7 @@ export const EndingOverlay: FC<EndingOverlayProps> = ({
   const [isCountDownStarted, setIsCountDownStarted] = useState(false)
   const mdMatch = useMediaMatch('md')
 
-  const randomNextVideoThumbnailUrl = randomNextVideo?.thumbnailPhoto?.resolvedUrl
+  const randomNextVideoThumbnailUrls = randomNextVideo?.thumbnailPhoto?.resolvedUrls
 
   useEffect(() => {
     if (!randomNextVideo || !isEnded) {
@@ -103,13 +103,13 @@ export const EndingOverlay: FC<EndingOverlayProps> = ({
     }
   }
 
-  const thumbnailUrl = useMemo(() => {
+  const thumbnailUrls = useMemo(() => {
     if (randomNextVideo) {
-      return randomNextVideoThumbnailUrl || ''
+      return randomNextVideoThumbnailUrls
     } else {
-      return currentThumbnailUrl || ''
+      return currentThumbnailUrls
     }
-  }, [currentThumbnailUrl, randomNextVideo, randomNextVideoThumbnailUrl])
+  }, [currentThumbnailUrls, randomNextVideo, randomNextVideoThumbnailUrls])
 
   const stopPropagationx = (e: MouseEvent) => {
     e.stopPropagation()
@@ -124,7 +124,7 @@ export const EndingOverlay: FC<EndingOverlayProps> = ({
               navigate(absoluteRoutes.viewer.video(randomNextVideo.id))
             }}
           >
-            <VideoThumbnail src={thumbnailUrl} />
+            <VideoThumbnail resolvedUrls={thumbnailUrls} />
             <VideoInfo>
               <Text as="span" variant={mdMatch ? 't300' : 't200'} color="colorText">
                 Up next

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