From 726d424dad25bca319efdc4289e20d7ae9fe34c9 Mon Sep 17 00:00:00 2001
From: Kevin Szuchet <31735779+kevinszuchet@users.noreply.github.com>
Date: Thu, 27 Jul 2023 14:10:45 +0200
Subject: [PATCH] feat: Fetch sw public content (#1946)
* feat: Fetch item content to take the video from builder api
* fix: Use asset itemid only
* fix: Visual fixes in the showcase modal
* feat: Add EOL in css file
---
...SmartWearableVideoShowcaseModal.module.css | 5 +-
.../SmartWearableVideoShowcaseModal.tsx | 12 ++-
webapp/src/lib/asset.spec.ts | 98 +++++++++++--------
webapp/src/lib/asset.ts | 28 ++++--
.../vendor/decentraland/builder/api.ts | 7 ++
5 files changed, 94 insertions(+), 56 deletions(-)
diff --git a/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.module.css b/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.module.css
index 3e6664a9c..359e41dcd 100644
--- a/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.module.css
+++ b/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.module.css
@@ -1,5 +1,8 @@
.video {
width: 100%;
- height: 100%;
object-fit: contain;
}
+
+.content {
+ min-height: 440px;
+}
diff --git a/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.tsx b/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.tsx
index 2c9a83ac6..9218a1bd3 100644
--- a/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.tsx
+++ b/webapp/src/components/Modals/SmartWearableVideoShowcaseModal/SmartWearableVideoShowcaseModal.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Modal } from 'decentraland-dapps/dist/containers'
-import { ModalNavigation } from 'decentraland-ui'
+import { Loader, ModalNavigation } from 'decentraland-ui'
import { builderAPI } from '../../../modules/vendor/decentraland/builder/api'
import { getSmartWearableVideoShowcase } from '../../../lib/asset'
import { VIDEO_TEST_ID } from './constants'
@@ -18,9 +18,9 @@ const SmartWearableVideoShowcaseModal = (props: Props) => {
const fetchVideoSrc = useCallback(async () => {
if (!asset?.urn) return
- const videoHash = await getSmartWearableVideoShowcase(asset.urn)
+ const videoHash = await getSmartWearableVideoShowcase(asset)
if (videoHash) setVideoSrc(builderAPI.contentUrl(videoHash))
- }, [asset.urn])
+ }, [asset])
useEffect(() => {
fetchVideoSrc()
@@ -39,13 +39,15 @@ const SmartWearableVideoShowcaseModal = (props: Props) => {
className={styles.video}
autoPlay
controls
- loop
muted
playsInline
data-testid={VIDEO_TEST_ID}
height={364}
+ preload="auto"
/>
- ) : null}
+ ) : (
+
+ )}
)
diff --git a/webapp/src/lib/asset.spec.ts b/webapp/src/lib/asset.spec.ts
index 4eadf4ba0..5bb94ef17 100644
--- a/webapp/src/lib/asset.spec.ts
+++ b/webapp/src/lib/asset.spec.ts
@@ -1,11 +1,24 @@
import * as contentClient from 'dcl-catalyst-client/dist/client/ContentClient'
+import { builderAPI } from '../modules/vendor/decentraland/builder/api'
+import { Asset } from '../modules/asset/types'
import {
getSmartWearableRequiredPermissions,
getSmartWearableSceneContent,
getSmartWearableVideoShowcase
} from './asset'
+jest.mock('../modules/vendor/decentraland/builder/api', () => ({
+ builderAPI: {
+ fetchItemContent: jest.fn()
+ }
+}))
+
const anSWUrn = 'aUrn'
+const smartWearable = {
+ contractAddress: '0xcontractAddress',
+ itemId: 'itemId',
+ urn: anSWUrn
+} as Asset
const entity = [{ content: [{ file: 'scene.json', hash: 'aHash' }] }]
let SWSceneContent = {
main: 'bin/game.js',
@@ -15,18 +28,20 @@ let SWSceneContent = {
},
requiredPermissions: ['USE_WEB3_API', 'OPEN_EXTERNAL_LINK']
}
-let mockClient: jest.SpyInstance
+let clientMock: jest.SpyInstance
+let fetchItemContentMock: jest.Mock
let SWSceneContentBuffer: ArrayBuffer
beforeEach(() => {
- mockClient = jest.spyOn(contentClient, 'createContentClient')
+ clientMock = jest.spyOn(contentClient, 'createContentClient')
+ fetchItemContentMock = builderAPI.fetchItemContent as jest.Mock
SWSceneContentBuffer = Buffer.from(JSON.stringify(SWSceneContent))
})
describe('when getting a smart wearable scene content', () => {
describe('and the smart wearable does not have an entity', () => {
beforeEach(() => {
- mockClient = mockClient.mockReturnValueOnce({
+ clientMock.mockReturnValueOnce({
fetchEntitiesByPointers: jest.fn().mockResolvedValueOnce([])
})
})
@@ -38,7 +53,7 @@ describe('when getting a smart wearable scene content', () => {
describe('and the smart wearable does not have a valid entity', () => {
beforeEach(() => {
- mockClient.mockReturnValueOnce({
+ clientMock.mockReturnValueOnce({
fetchEntitiesByPointers: jest
.fn()
.mockResolvedValueOnce([{ id: 'anId' }])
@@ -52,7 +67,7 @@ describe('when getting a smart wearable scene content', () => {
describe('and the smart wearable have a valid entity', () => {
beforeEach(() => {
- mockClient.mockReturnValueOnce({
+ clientMock.mockReturnValueOnce({
fetchEntitiesByPointers: jest.fn().mockResolvedValueOnce(entity),
downloadContent: jest.fn().mockResolvedValueOnce(SWSceneContentBuffer)
})
@@ -71,7 +86,7 @@ describe('when getting a smart wearable required permissions', () => {
beforeEach(() => {
SWSceneContent = { ...SWSceneContent, requiredPermissions: [] }
SWSceneContentBuffer = Buffer.from(JSON.stringify(SWSceneContent))
- mockClient.mockReturnValueOnce({
+ clientMock.mockReturnValueOnce({
fetchEntitiesByPointers: jest.fn().mockResolvedValueOnce(entity),
downloadContent: jest.fn().mockResolvedValueOnce(SWSceneContentBuffer)
})
@@ -86,7 +101,7 @@ describe('when getting a smart wearable required permissions', () => {
describe('and the smart wearable have required permissions', () => {
beforeEach(() => {
- mockClient.mockReturnValueOnce({
+ clientMock.mockReturnValueOnce({
fetchEntitiesByPointers: jest.fn().mockResolvedValueOnce(entity),
downloadContent: jest.fn().mockResolvedValueOnce(SWSceneContentBuffer)
})
@@ -101,52 +116,51 @@ describe('when getting a smart wearable required permissions', () => {
})
describe('when getting a smart wearable video showcase', () => {
- describe('and the smart wearable does not have an entity', () => {
+ describe.each([null, undefined])('and the asset itemId is %s', itemId => {
+ it('should return undefined', async () => {
+ expect(
+ await getSmartWearableVideoShowcase({
+ ...smartWearable,
+ itemId
+ } as Asset)
+ ).toBe(undefined)
+ })
+ })
+
+ describe('and the builder api fails', () => {
beforeEach(() => {
- mockClient = mockClient.mockReturnValueOnce({
- fetchEntitiesByPointers: jest.fn().mockResolvedValueOnce([])
- })
+ fetchItemContentMock.mockRejectedValueOnce(new Error('aError'))
})
it('should return undefined', async () => {
- expect(await getSmartWearableVideoShowcase(anSWUrn)).toBe(undefined)
+ expect(await getSmartWearableVideoShowcase(smartWearable)).toBe(undefined)
})
})
- describe('and the smart wearable has an entity', () => {
- describe('and the smart wearable does not have the video in its content', () => {
- beforeEach(() => {
- mockClient.mockReturnValueOnce({
- fetchEntitiesByPointers: jest.fn().mockResolvedValueOnce(entity)
- })
- })
+ describe('and the smart wearable does not have a video in the content', () => {
+ beforeEach(() => {
+ fetchItemContentMock.mockResolvedValueOnce({})
+ })
- it('should return undefined', async () => {
- expect(await getSmartWearableVideoShowcase(anSWUrn)).toBeUndefined()
- })
+ it('should return undefined', async () => {
+ expect(await getSmartWearableVideoShowcase(smartWearable)).toBe(undefined)
})
+ })
- describe('and the smart wearable have video showcase', () => {
- const entityWithVideo = [
- {
- content: [
- { file: 'scene.json', hash: 'aHash' },
- { file: 'video.mp4', hash: 'aVideoHash' }
- ]
- }
- ]
-
- beforeEach(() => {
- mockClient.mockReturnValueOnce({
- fetchEntitiesByPointers: jest
- .fn()
- .mockResolvedValueOnce(entityWithVideo)
- })
- })
+ describe('and the smart wearable has a video', () => {
+ const content = {
+ 'scene.json': 'aHash',
+ 'video.mp4': 'aVideoHash'
+ }
- it('should return the video hash', async () => {
- expect(await getSmartWearableVideoShowcase(anSWUrn)).toBe('aVideoHash')
- })
+ beforeEach(() => {
+ fetchItemContentMock.mockResolvedValueOnce(content)
+ })
+
+ it('should return the video hash', async () => {
+ expect(await getSmartWearableVideoShowcase(smartWearable)).toBe(
+ 'aVideoHash'
+ )
})
})
})
diff --git a/webapp/src/lib/asset.ts b/webapp/src/lib/asset.ts
index 71ec94835..563c63431 100644
--- a/webapp/src/lib/asset.ts
+++ b/webapp/src/lib/asset.ts
@@ -1,7 +1,12 @@
import { createContentClient } from 'dcl-catalyst-client/dist/client/ContentClient'
import { createFetchComponent } from '@well-known-components/fetch-component'
+import { Asset } from '../modules/asset/types'
+import { builderAPI } from '../modules/vendor/decentraland/builder/api'
import { peerUrl } from './environment'
+export const SCENE_PATH = 'scene.json'
+export const VIDEO_PATH = 'video.mp4'
+
const getContentClient = () =>
createContentClient({
url: `${peerUrl}/content`,
@@ -16,7 +21,7 @@ export const getSmartWearableSceneContent = async (
if (wearableEntity.length > 0) {
const scene = wearableEntity[0].content?.find(entity =>
- entity.file.endsWith('scene.json')
+ entity.file.endsWith(SCENE_PATH)
)
if (scene) {
@@ -39,14 +44,21 @@ export const getSmartWearableRequiredPermissions = async (
}
export const getSmartWearableVideoShowcase = async (
- urn: string
+ asset: Asset
): Promise => {
- const contentClient = getContentClient()
- const wearableEntity = await contentClient.fetchEntitiesByPointers([urn])
+ try {
+ const { contractAddress, itemId } = asset
- const video = wearableEntity[0]?.content?.find(entity =>
- entity.file.endsWith('video.mp4')
- )
+ if (!itemId) return undefined
- return video ? video.hash : undefined
+ const contents = await builderAPI.fetchItemContent(contractAddress, itemId)
+
+ const videoContentKey = Object.keys(contents).find(key =>
+ key.endsWith(VIDEO_PATH)
+ )
+
+ return videoContentKey ? contents[videoContentKey] : undefined
+ } catch (error) {
+ return undefined
+ }
}
diff --git a/webapp/src/modules/vendor/decentraland/builder/api.ts b/webapp/src/modules/vendor/decentraland/builder/api.ts
index 2dfe2fd67..efe8b1fec 100644
--- a/webapp/src/modules/vendor/decentraland/builder/api.ts
+++ b/webapp/src/modules/vendor/decentraland/builder/api.ts
@@ -18,6 +18,13 @@ class BuilderAPI extends BaseAPI {
contentUrl(hash: string) {
return `${this.url}/storage/contents/${hash}`
}
+
+ fetchItemContent = async (
+ collectionAddress: string,
+ itemId: string
+ ): Promise> => {
+ return this.request('get', `/items/${collectionAddress}/${itemId}/contents`)
+ }
}
export const builderAPI = new BuilderAPI(BUILDER_SERVER_URL, retryParams)