diff --git a/packages/shared/components/MediaDisplay.svelte b/packages/shared/components/MediaDisplay.svelte index 8df73db2c8a..d08e9111b20 100644 --- a/packages/shared/components/MediaDisplay.svelte +++ b/packages/shared/components/MediaDisplay.svelte @@ -31,7 +31,7 @@ </script> <div class="h-full w-full object-cover"> - {#if htmlTag === ParentMimeType.Image} + {#if htmlTag === ParentMimeType.Image || htmlTag === ParentMimeType.Text} <img {src} {alt} loading="lazy" class="w-full h-full object-cover" /> {:else if htmlTag === ParentMimeType.Video} <video diff --git a/packages/shared/components/NftImageOrIconBox.svelte b/packages/shared/components/NftImageOrIconBox.svelte index c821731dc24..a1c9b5dd1ad 100644 --- a/packages/shared/components/NftImageOrIconBox.svelte +++ b/packages/shared/components/NftImageOrIconBox.svelte @@ -1,8 +1,7 @@ <script lang="ts"> - import { INft } from '@core/nfts' - import { NftSize } from 'shared/components/enums' + import { INft, ParentMimeType } from '@core/nfts' import { MediaPlaceholder, NftMedia } from 'shared/components' - import { ParentMimeType } from '@core/nfts' + import { NftSize } from 'shared/components/enums' export let nft: INft | null = null export let size: NftSize = NftSize.Medium @@ -18,7 +17,7 @@ class:medium={size === NftSize.Medium} class:large={size === NftSize.Large} > - {#if parentType === ParentMimeType.Image && nft} + {#if (parentType === ParentMimeType.Image && nft) || (parentType === ParentMimeType.Text && nft)} <NftMedia {nft} {useCaching}> <placeholder-wrapper slot="placeholder" diff --git a/packages/shared/lib/core/nfts/actions/downloadNextNftInQueue.ts b/packages/shared/lib/core/nfts/actions/downloadNextNftInQueue.ts index e67fa150a36..33a18312c13 100644 --- a/packages/shared/lib/core/nfts/actions/downloadNextNftInQueue.ts +++ b/packages/shared/lib/core/nfts/actions/downloadNextNftInQueue.ts @@ -1,6 +1,7 @@ import { Platform } from '@core/app' import { get } from 'svelte/store' import { downloadingNftId, nftDownloadQueue, removeNftFromDownloadQueue } from '../stores' +import { getIpfsUri, getIPFSHash } from '../utils' export async function downloadNextNftInQueue(): Promise<void> { const nextDownload = get(nftDownloadQueue)?.[0] @@ -9,8 +10,17 @@ export async function downloadNextNftInQueue(): Promise<void> { } try { - const { downloadUrl, path, nft, accountIndex } = nextDownload + // eslint-disable-next-line prefer-const + let { downloadUrl, path, nft, accountIndex } = nextDownload downloadingNftId.set(nft.id) + const ipfsHash = getIPFSHash(downloadUrl) + if (ipfsHash) { + const ipfsUri = await getIpfsUri({ hash: ipfsHash }) + if (ipfsUri) { + downloadUrl = ipfsUri + } + } + await Platform.downloadNft(downloadUrl, path, nft.id, accountIndex) } catch (error) { downloadingNftId.set(undefined) diff --git a/packages/shared/lib/core/nfts/actions/updateNftInAllAccountNfts.ts b/packages/shared/lib/core/nfts/actions/updateNftInAllAccountNfts.ts index 62f63dd2936..bd4214c2655 100644 --- a/packages/shared/lib/core/nfts/actions/updateNftInAllAccountNfts.ts +++ b/packages/shared/lib/core/nfts/actions/updateNftInAllAccountNfts.ts @@ -1,5 +1,6 @@ -import { allAccountNfts } from '../stores' import { INft } from '../interfaces' +import { allAccountNfts } from '../stores' +import { getIpfsUri, getIPFSHash } from '../utils' export function updateNftInAllAccountNfts(accountIndex: number, nftId: string, partialNft: Partial<INft>): void { allAccountNfts.update((state) => { @@ -8,6 +9,16 @@ export function updateNftInAllAccountNfts(accountIndex: number, nftId: string, p } const nft = state[accountIndex].find((_nft) => _nft.id === nftId) if (nft) { + const downloadUrl = nft.downloadUrl + const ipfsHash = getIPFSHash(downloadUrl) + if (ipfsHash) { + void getIpfsUri({ hash: ipfsHash }).then((ipfsUri) => { + if (ipfsUri) { + nft.downloadUrl = ipfsUri + nft.composedUrl = ipfsUri + } + }) + } Object.assign(nft, { ...nft, ...partialNft }) } return state diff --git a/packages/shared/lib/core/nfts/utils/getIpfsHash.ts b/packages/shared/lib/core/nfts/utils/getIpfsHash.ts new file mode 100644 index 00000000000..44997e4cca8 --- /dev/null +++ b/packages/shared/lib/core/nfts/utils/getIpfsHash.ts @@ -0,0 +1,7 @@ +export function getIPFSHash(url?: string): string | undefined { + const ipfsPrefix = 'https://ipfs.io' + + if (url?.includes(ipfsPrefix)) { + return url.slice(ipfsPrefix.length) + } +} diff --git a/packages/shared/lib/core/nfts/utils/getIpfsUri.ts b/packages/shared/lib/core/nfts/utils/getIpfsUri.ts new file mode 100644 index 00000000000..3d094ea6a87 --- /dev/null +++ b/packages/shared/lib/core/nfts/utils/getIpfsUri.ts @@ -0,0 +1,152 @@ +interface IIpfsLink { + Hash: string + Name: string + Size: number + Target: string + Type: number + Mode?: string + Mtime?: number + MtimeNsecs?: number +} + +interface IIPfsEntry { + readonly type: 'dir' | 'file' + readonly cid: string + readonly name: string + readonly path: string + mode?: number + mtime?: { + secs: number + nsecs?: number + } + size: number +} + +interface IIpfsObject { + Hash: string + Links: IIpfsLink[] +} + +enum typeOfLink { + Dir = 'dir', + File = 'file', +} + +const IPFS_ENDPOINT = 'https://ipfs.io' +const IPFS_PATH = '/api/v0/ls' +const IPFS_PREFIX = '/ipfs/' + +export async function getIpfsUri(link: { path?: string; hash: string }): Promise<string | undefined> { + let ipfsLink = `${link.hash}${link.path ?? ''}` + try { + const ipfsEntry = await ls(ipfsLink) + + if (ipfsEntry) { + if (ipfsEntry.type === 'dir') { + const path = `${link.path ?? ''}/${ipfsEntry.name}` + return await getIpfsUri({ hash: link.hash, path }) + } + ipfsLink = `${ipfsLink}/${encodeURIComponent(ipfsEntry.name)}` + } + } catch (error) { + console.error('error', error) + } + + return `${IPFS_ENDPOINT}${ipfsLink}` +} + +async function ls(path: string): Promise<IIPfsEntry | undefined> { + let ipfsEntry: IIPfsEntry | undefined + + try { + const baseUrl = IPFS_ENDPOINT + const method = 'get' + const payload = undefined + let headers = {} + const timeout = undefined + + headers ??= {} + + let controller: AbortController | undefined + let timerId: NodeJS.Timeout | undefined + + if (timeout !== undefined) { + controller = new AbortController() + timerId = setTimeout(() => { + if (controller) { + controller.abort() + } + }, timeout) + } + + try { + if (path.includes('ipfs')) { + const response = await fetch(`${baseUrl}${IPFS_PATH}?arg=/${path}`, { + method, + headers, + body: payload ? JSON.stringify(payload) : undefined, + signal: controller ? controller.signal : undefined, + }) + const lsResponse = (await response.json()) as { Objects: IIpfsObject[] } + const result = lsResponse.Objects[0] + if (result) { + const links = result.Links + if (links.length > 0) { + ipfsEntry = mapLinkToIpfsEntry(links[0], path) + } + } + } + } catch (error) { + console.error('error', error) + } finally { + if (timerId) { + clearTimeout(timerId) + } + } + } catch (error) { + console.error('error', error) + } + + return ipfsEntry +} + +function mapLinkToIpfsEntry(link: IIpfsLink, path: string): IIPfsEntry { + const hash = link.Hash.startsWith(IPFS_PREFIX) ? link.Hash.slice(IPFS_PREFIX.length) : link.Hash + const entry: IIPfsEntry = { + name: link.Name, + path: path + (link.Name ? `/${link.Name}` : ''), + size: link.Size, + cid: hash, + type: typeOf(link), + } + if (link.Mode) { + entry.mode = Number.parseInt(link.Mode, 8) + } + + if (link.Mtime !== undefined && link.Mtime !== null) { + entry.mtime = { + secs: link.Mtime, + } + + if (link.MtimeNsecs !== undefined && link.MtimeNsecs !== null) { + entry.mtime.nsecs = link.MtimeNsecs + } + } + + return entry +} + +function typeOf(link: IIpfsLink): typeOfLink { + switch (link.Type) { + case 1: + case 5: { + return typeOfLink.Dir + } + case 2: { + return typeOfLink.File + } + default: { + return typeOfLink.File + } + } +} diff --git a/packages/shared/lib/core/nfts/utils/index.ts b/packages/shared/lib/core/nfts/utils/index.ts index ea8bd888dc2..717cbfe65c5 100644 --- a/packages/shared/lib/core/nfts/utils/index.ts +++ b/packages/shared/lib/core/nfts/utils/index.ts @@ -2,9 +2,11 @@ export * from './buildNftFromNftOutput' export * from './checkIfNftShouldBeDownloaded' export * from './composeUrlFromNftUri' export * from './convertAndFormatNftMetadata' -export * from './getSpendableStatusFromUnspentNftOutput' export * from './fetchWithTimeout' +export * from './getIpfsHash' +export * from './getIpfsUri' +export * from './getParentMimeType' +export * from './getSpendableStatusFromUnspentNftOutput' export * from './isNftOwnedByAnyAccount' export * from './parseNftMetadata' export * from './rewriteIpfsUri' -export * from './getParentMimeType'