Skip to content

Commit

Permalink
feat: Fetch sw public content (#1946)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kevinszuchet authored and cyaiox committed Jul 28, 2023
1 parent fd92b4c commit 726d424
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.video {
width: 100%;
height: 100%;
object-fit: contain;
}

.content {
min-height: 440px;
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand All @@ -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}
) : (
<Loader size="big" />
)}
</Modal.Content>
</Modal>
)
Expand Down
98 changes: 56 additions & 42 deletions webapp/src/lib/asset.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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([])
})
})
Expand All @@ -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' }])
Expand All @@ -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)
})
Expand All @@ -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)
})
Expand All @@ -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)
})
Expand All @@ -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'
)
})
})
})
28 changes: 20 additions & 8 deletions webapp/src/lib/asset.ts
Original file line number Diff line number Diff line change
@@ -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`,
Expand All @@ -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) {
Expand All @@ -39,14 +44,21 @@ export const getSmartWearableRequiredPermissions = async (
}

export const getSmartWearableVideoShowcase = async (
urn: string
asset: Asset
): Promise<string | undefined> => {
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
}
}
7 changes: 7 additions & 0 deletions webapp/src/modules/vendor/decentraland/builder/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class BuilderAPI extends BaseAPI {
contentUrl(hash: string) {
return `${this.url}/storage/contents/${hash}`
}

fetchItemContent = async (
collectionAddress: string,
itemId: string
): Promise<Record<string, string>> => {
return this.request('get', `/items/${collectionAddress}/${itemId}/contents`)
}
}

export const builderAPI = new BuilderAPI(BUILDER_SERVER_URL, retryParams)

0 comments on commit 726d424

Please sign in to comment.