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)