diff --git a/src/components/CollectionDetailPage/CollectionDetailPage.tsx b/src/components/CollectionDetailPage/CollectionDetailPage.tsx index 9a19cf1b2..5da6cb47a 100644 --- a/src/components/CollectionDetailPage/CollectionDetailPage.tsx +++ b/src/components/CollectionDetailPage/CollectionDetailPage.tsx @@ -16,7 +16,7 @@ import { isOwner } from 'modules/collection/utils' import { CollectionType } from 'modules/collection/types' -import { isSmart } from 'modules/item/utils' +import { isEmote, isSmart, isWearable } from 'modules/item/utils' import { Item, ItemType, SyncStatus, VIDEO_PATH } from 'modules/item/types' import CollectionProvider from 'components/CollectionProvider' import LoggedInDetailPage from 'components/LoggedInDetailPage' @@ -239,16 +239,14 @@ export default function CollectionDetailPage({ const canMint = canMintCollectionItems(collection, wallet.address) const isLocked = isCollectionLocked(collection) const isOnSale = isCollectionOnSale(collection, wallet) - const hasEmotes = items.some(item => item.type === ItemType.EMOTE) - const hasWearables = items.some(item => item.type === ItemType.WEARABLE) - const isEmoteMissingPrice = hasEmotes ? items.some(item => item.type === ItemType.EMOTE && !item.price) : false - const isWearableMissingPrice = hasWearables ? items.some(item => item.type === ItemType.WEARABLE && !item.price) : false + const hasEmotes = items.some(isEmote) + const hasWearables = items.some(isWearable) + const isEmoteMissingPrice = hasEmotes ? items.some(item => isEmote(item) && !item.price) : false + const isWearableMissingPrice = hasWearables ? items.some(item => isWearable(item) && !item.price) : false const isSmartWearableMissingVideo = hasWearables && items.some(item => isSmart(item) && !(VIDEO_PATH in item.contents)) const hasOnlyEmotes = hasEmotes && !hasWearables const hasOnlyWearables = hasWearables && !hasEmotes - const filteredItems = items.filter(item => - hasOnlyWearables ? item.type === ItemType.WEARABLE : hasOnlyEmotes ? item.type === ItemType.EMOTE : item.type === tab - ) + const filteredItems = items.filter(item => (hasOnlyWearables ? isWearable(item) : hasOnlyEmotes ? isEmote(item) : item.type === tab)) const showShowTabs = hasEmotes && hasWearables return ( diff --git a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx index 8369dabfa..308ae407f 100644 --- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx +++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx @@ -9,9 +9,9 @@ import { Link, useHistory } from 'react-router-dom' import { locations } from 'routing/locations' import { preventDefault } from 'lib/event' import { extractThirdPartyTokenId, extractTokenId, isThirdParty } from 'lib/urn' -import { isComplete, isFree, canManageItem, getMaxSupply, isSmart } from 'modules/item/utils' +import { isComplete, isFree, canManageItem, getMaxSupply, isSmart, isEmote } from 'modules/item/utils' import { isLocked } from 'modules/collection/utils' -import { isEmoteData, ItemType, SyncStatus, VIDEO_PATH, WearableData } from 'modules/item/types' +import { isEmoteData, SyncStatus, VIDEO_PATH, WearableData } from 'modules/item/types' import { FromParam } from 'modules/location/types' import ItemStatus from 'components/ItemStatus' import ItemBadge from 'components/ItemBadge' @@ -176,7 +176,7 @@ export default function CollectionItem({ onOpenModal, onSetItems, item, collecti {item.rarity && data.category ? : null} {data.category ?
{t(`${item.type}.category.${data.category}`)}
: null}
- {item.type === ItemType.EMOTE && isEmoteData(data) ? ( + {isEmote(item) && isEmoteData(data) ? ( {data.category ?
{t(`emote.play_mode.${data.loop ? 'loop' : 'simple'}.text`)}
: null}
diff --git a/src/components/ItemDetailPage/ItemDetailPage.tsx b/src/components/ItemDetailPage/ItemDetailPage.tsx index bb81e9900..40efc569e 100644 --- a/src/components/ItemDetailPage/ItemDetailPage.tsx +++ b/src/components/ItemDetailPage/ItemDetailPage.tsx @@ -6,7 +6,7 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { Network } from '@dcl/schemas' import { locations } from 'routing/locations' import { Collection } from 'modules/collection/types' -import { getMaxSupply, getMissingBodyShapeType, isFree, resizeImage, getThumbnailURL, isSmart } from 'modules/item/utils' +import { getMaxSupply, getMissingBodyShapeType, isFree, resizeImage, getThumbnailURL, isSmart, isWearable } from 'modules/item/utils' import { getCollectionType, isLocked as isCollectionLocked } from 'modules/collection/utils' import { dataURLToBlob } from 'modules/media/utils' import { computeHashes } from 'modules/deployment/contentUtils' @@ -347,7 +347,7 @@ export default function ItemDetailPage(props: Props) { - {item.type === ItemType.WEARABLE && !isSmart(item) ? ( + {isWearable(item) && !isSmart(item) ? (
{t('item_detail_page.representations.title')}
diff --git a/src/components/ItemEditorPage/CenterPanel/CenterPanel.container.ts b/src/components/ItemEditorPage/CenterPanel/CenterPanel.container.ts index 8a6760dff..80a7bd4a0 100644 --- a/src/components/ItemEditorPage/CenterPanel/CenterPanel.container.ts +++ b/src/components/ItemEditorPage/CenterPanel/CenterPanel.container.ts @@ -29,7 +29,7 @@ import { } from 'modules/editor/selectors' import { fetchCollectionItemsRequest, fetchItemsRequest } from 'modules/item/actions' import { getEmotes, getItem, hasUserOrphanItems } from 'modules/item/selectors' -import { ItemType } from 'modules/item/types' +import { isEmote } from 'modules/item/utils' import { getSelectedCollectionId, getSelectedItemId } from 'modules/location/selectors' import { MapStateProps, MapDispatchProps, MapDispatch } from './CenterPanel.types' import CenterPanel from './CenterPanel' @@ -53,7 +53,7 @@ const mapState = (state: RootState): MapStateProps => { const selectedBaseWearablesByBodyShape = getSelectedBaseWearablesByBodyShape(state) const visibleItems = getVisibleItems(state) const emote = getEmote(state) - const isPLayingIdleEmote = !visibleItems.some(item => item.type === ItemType.EMOTE) && emote === PreviewEmote.IDLE + const isPLayingIdleEmote = !visibleItems.some(isEmote) && emote === PreviewEmote.IDLE /* The library react-dropzone doesn't work as expected when an Iframe is present in the current view. This way, we're getting when the CreateSingleItemModal is open to disable the drag and drop events in the Iframe and the library react-dropzone works as expected in the CreateSingleItemModal. diff --git a/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx b/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx index 05a2ee95a..6ea0f56f8 100644 --- a/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx +++ b/src/components/ItemEditorPage/CenterPanel/CenterPanel.tsx @@ -20,6 +20,7 @@ import { isDevelopment } from 'lib/environment' import { extractThirdPartyTokenId, extractTokenId, isThirdParty } from 'lib/urn' import { isTPCollection } from 'modules/collection/utils' import { ItemType } from 'modules/item/types' +import { isEmote } from 'modules/item/utils' import { toBase64, toHex } from 'modules/editor/utils' import { getSkinColors, getEyeColors, getHairColors } from 'modules/editor/avatar' import BuilderIcon from 'components/Icon' @@ -272,7 +273,7 @@ export default class CenterPanel extends React.PureComponent { wearableController } = this.props const { isShowingAvatarAttributes, showSceneBoundaries, isLoading } = this.state - const isRenderingAnEmote = visibleItems.some(item => item.type === ItemType.EMOTE) && selectedItem?.type === ItemType.EMOTE + const isRenderingAnEmote = visibleItems.some(isEmote) && selectedItem?.type === ItemType.EMOTE const zoom = emote === PreviewEmote.JUMP ? 1 : undefined return ( diff --git a/src/components/ItemEditorPage/LeftPanel/Items/Items.tsx b/src/components/ItemEditorPage/LeftPanel/Items/Items.tsx index 170c423a3..2cbaff5c7 100644 --- a/src/components/ItemEditorPage/LeftPanel/Items/Items.tsx +++ b/src/components/ItemEditorPage/LeftPanel/Items/Items.tsx @@ -16,7 +16,7 @@ import { } from 'decentraland-ui' import { extractThirdPartyTokenId, extractTokenId, isThirdParty } from 'lib/urn' import { Item, ItemType } from 'modules/item/types' -import { hasBodyShape } from 'modules/item/utils' +import { hasBodyShape, isEmote, isWearable } from 'modules/item/utils' import { TP_TRESHOLD_TO_REVIEW } from 'modules/collection/constants' import { LEFT_PANEL_PAGE_SIZE } from '../../constants' import Collapsable from 'components/Collapsable' @@ -77,7 +77,7 @@ export default class Items extends React.PureComponent { let newVisibleItemIds = visibleItems.filter(_item => _item.id !== item.id) - if (item.type === ItemType.EMOTE) { + if (isEmote(item)) { if (this.isVisible(item)) { if (isPlayingEmote) { wearableController?.emote.pause() as void @@ -165,8 +165,8 @@ export default class Items extends React.PureComponent { } renderSidebarCategory = (items: Item[]) => { - const wearableItems = items.filter(item => item.type === ItemType.WEARABLE) - const emoteItems = items.filter(item => item.type === ItemType.EMOTE) + const wearableItems = items.filter(isWearable) + const emoteItems = items.filter(isEmote) if (wearableItems.length === 0 || emoteItems.length === 0) { return items.map(this.renderSidebarItem) diff --git a/src/components/ItemEditorPage/LeftPanel/Items/SidebarItem/SidebarItem.tsx b/src/components/ItemEditorPage/LeftPanel/Items/SidebarItem/SidebarItem.tsx index 10a1d109e..2ad9efa73 100644 --- a/src/components/ItemEditorPage/LeftPanel/Items/SidebarItem/SidebarItem.tsx +++ b/src/components/ItemEditorPage/LeftPanel/Items/SidebarItem/SidebarItem.tsx @@ -4,8 +4,7 @@ import { Icon, Popup } from 'decentraland-ui' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import ItemImage from 'components/ItemImage' import ItemStatus from 'components/ItemStatus' -import { ItemType } from 'modules/item/types' -import { getMissingBodyShapeType, hasBodyShape } from 'modules/item/utils' +import { getMissingBodyShapeType, hasBodyShape, isEmote } from 'modules/item/utils' import { locations } from 'routing/locations' import { Props } from './SidebarItem.types' import './SidebarItem.css' @@ -18,7 +17,7 @@ class SidebarItem extends React.PureComponent { renderToggleItem() { const { item, isVisible, isPlayingEmote } = this.props - if (item.type === ItemType.EMOTE) { + if (isEmote(item)) { return } else { return
diff --git a/src/components/ItemEditorPage/RightPanel/RightPanel.tsx b/src/components/ItemEditorPage/RightPanel/RightPanel.tsx index e6d17a283..f15a25583 100644 --- a/src/components/ItemEditorPage/RightPanel/RightPanel.tsx +++ b/src/components/ItemEditorPage/RightPanel/RightPanel.tsx @@ -16,7 +16,8 @@ import { getHideableBodyPartCategories, getHideableWearableCategories, isSmart, - hasVideo + hasVideo, + isWearable } from 'modules/item/utils' import { isLocked } from 'modules/collection/utils' import { computeHashes } from 'modules/deployment/contentUtils' @@ -83,7 +84,7 @@ export default class RightPanel extends React.PureComponent { setItem(item: Item) { const data = item.data - if (item.type === ItemType.WEARABLE && data.replaces?.length) { + if (isWearable(item) && data.replaces?.length) { // Move all items that are in replaces array to hides array data.hides = data.hides.concat(data.replaces) data.replaces = [] @@ -525,7 +526,7 @@ export default class RightPanel extends React.PureComponent { const isItemLocked = collection && isLocked(collection) const canEditItemMetadata = this.canEditItemMetadata(item) - const categories = item ? (item.type === ItemType.WEARABLE ? getWearableCategories(item.contents) : getEmoteCategories()) : [] + const categories = item ? (isWearable(item) ? getWearableCategories(item.contents) : getEmoteCategories()) : [] return isLoading ? ( diff --git a/src/components/Modals/CreateAndEditMultipleItemsModal/CreateAndEditMultipleItemsModal.tsx b/src/components/Modals/CreateAndEditMultipleItemsModal/CreateAndEditMultipleItemsModal.tsx index ceec6c901..38c0ff844 100644 --- a/src/components/Modals/CreateAndEditMultipleItemsModal/CreateAndEditMultipleItemsModal.tsx +++ b/src/components/Modals/CreateAndEditMultipleItemsModal/CreateAndEditMultipleItemsModal.tsx @@ -23,7 +23,16 @@ import { T, t } from 'decentraland-dapps/dist/modules/translation/utils' import { config } from 'config' import { EngineType, getModelData } from 'lib/getModelData' import { getExtension, toMB } from 'lib/file' -import { buildThirdPartyURN, DecodedURN, decodeURN, URNType } from 'lib/urn' +import { + buildThirdPartyURN, + buildThirdPartyV2URN, + decodedCollectionsUrnAreEqual, + DecodedURN, + decodeURN, + isThirdPartyCollectionDecodedUrn, + isThirdPartyV2CollectionDecodedUrn, + URNType +} from 'lib/urn' import { convertImageIntoWearableThumbnail, dataURLToBlob, getImageType } from 'modules/media/utils' import { ImageType } from 'modules/media/types' import { MultipleItemsSaveState } from 'modules/ui/createMultipleItems/reducer' @@ -165,33 +174,41 @@ export default class CreateAndEditMultipleItemsModal extends React.PureComponent // Generate or set the correct URN for the items taking into consideration the selected collection const decodedCollectionUrn: DecodedURN | null = collection?.urn ? decodeURN(collection.urn) : null + // Check if the collection is a third party collection if ( decodedCollectionUrn && - decodedCollectionUrn.type === URNType.COLLECTIONS_THIRDPARTY && - decodedCollectionUrn.thirdPartyCollectionId + (isThirdPartyCollectionDecodedUrn(decodedCollectionUrn) || isThirdPartyV2CollectionDecodedUrn(decodedCollectionUrn)) ) { const decodedUrn: DecodedURN | null = loadedFile.wearable.id ? decodeURN(loadedFile.wearable.id) : null - if (loadedFile.wearable.id && decodedUrn && decodedUrn.type === URNType.COLLECTIONS_THIRDPARTY) { - const { thirdPartyName, thirdPartyCollectionId } = decodedUrn - - if ( - (thirdPartyCollectionId && thirdPartyCollectionId !== decodedCollectionUrn.thirdPartyCollectionId) || - (thirdPartyName && thirdPartyName !== decodedCollectionUrn.thirdPartyName) - ) { - throw new Error(t('create_and_edit_multiple_items_modal.invalid_urn')) - } - if (decodedUrn.thirdPartyTokenId) { - itemFactory.withUrn( - buildThirdPartyURN( - decodedCollectionUrn.thirdPartyName, - decodedCollectionUrn.thirdPartyCollectionId, - decodedUrn.thirdPartyTokenId - ) + const thirdPartyTokenId = + loadedFile.wearable.id && + decodedUrn && + (decodedUrn.type === URNType.COLLECTIONS_THIRDPARTY || decodedUrn.type === URNType.COLLECTIONS_THIRDPARTY_V2) + ? decodedUrn.thirdPartyTokenId ?? null + : null + + // Check if the decoded collections match a the collection level + if (decodedUrn && !decodedCollectionsUrnAreEqual(decodedCollectionUrn, decodedUrn)) { + throw new Error(t('create_and_edit_multiple_items_modal.invalid_urn')) + } + + // Build the third party item URN in accordance ot the collection URN + if (isThirdPartyCollectionDecodedUrn(decodedCollectionUrn)) { + itemFactory.withUrn( + buildThirdPartyURN( + decodedCollectionUrn.thirdPartyName, + decodedCollectionUrn.thirdPartyCollectionId, + thirdPartyTokenId ?? uuid.v4() ) - } + ) } else { itemFactory.withUrn( - buildThirdPartyURN(decodedCollectionUrn.thirdPartyName, decodedCollectionUrn.thirdPartyCollectionId, uuid.v4()) + buildThirdPartyV2URN( + decodedCollectionUrn.thirdPartyLinkedCollectionName, + decodedCollectionUrn.linkedCollectionNetwork, + decodedCollectionUrn.linkedCollectionAddress, + thirdPartyTokenId ?? uuid.v4() + ) ) } } diff --git a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx index af5f9969b..3194dd173 100644 --- a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx +++ b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx @@ -1,6 +1,14 @@ import * as React from 'react' import uuid from 'uuid' +import { ethers } from 'ethers' import { BodyPartCategory, BodyShape, EmoteCategory, EmoteDataADR74, Rarity, PreviewProjection, WearableCategory } from '@dcl/schemas' +import { + MAX_EMOTE_FILE_SIZE, + MAX_SKIN_FILE_SIZE, + MAX_THUMBNAIL_FILE_SIZE, + MAX_WEARABLE_FILE_SIZE, + MAX_SMART_WEARABLE_FILE_SIZE +} from '@dcl/builder-client/dist/files/constants' import { ModalNavigation, Row, @@ -46,16 +54,29 @@ import { getEmoteCategories, getEmotePlayModes, getBodyShapeTypeFromContents, - isSmart + isSmart, + isWearable } from 'modules/item/utils' import { EngineType, getItemData, getModelData } from 'lib/getModelData' import { getExtension, toMB } from 'lib/file' -import { buildThirdPartyURN, DecodedURN, decodeURN, isThirdParty, URNType } from 'lib/urn' +import { + buildThirdPartyURN, + buildThirdPartyV2URN, + DecodedURN, + decodeURN, + isThirdParty, + isThirdPartyCollectionDecodedUrn, + isThirdPartyV2CollectionDecodedUrn +} from 'lib/urn' import ItemDropdown from 'components/ItemDropdown' import Icon from 'components/Icon' import ItemVideo from 'components/ItemVideo' import ItemRequiredPermission from 'components/ItemRequiredPermission' import ItemProperties from 'components/ItemProperties' +import { calculateFileSize, calculateModelFinalSize } from 'modules/item/export' +import { MAX_THUMBNAIL_SIZE } from 'modules/assetPack/utils' +import { Authorization } from 'lib/api/auth' +import { BUILDER_SERVER_URL, BuilderAPI } from 'lib/api/builder' import EditPriceAndBeneficiaryModal from '../EditPriceAndBeneficiaryModal' import ImportStep from './ImportStep/ImportStep' import EditThumbnailStep from './EditThumbnailStep/EditThumbnailStep' @@ -72,18 +93,6 @@ import { ITEM_LOADED_CHECK_DELAY } from './CreateSingleItemModal.types' import './CreateSingleItemModal.css' -import { calculateFileSize, calculateModelFinalSize } from 'modules/item/export' -import { MAX_THUMBNAIL_SIZE } from 'modules/assetPack/utils' -import { Authorization } from 'lib/api/auth' -import { BUILDER_SERVER_URL, BuilderAPI } from 'lib/api/builder' -import { - MAX_EMOTE_FILE_SIZE, - MAX_SKIN_FILE_SIZE, - MAX_THUMBNAIL_FILE_SIZE, - MAX_WEARABLE_FILE_SIZE, - MAX_SMART_WEARABLE_FILE_SIZE -} from '@dcl/builder-client/dist/files/constants' - export default class CreateSingleItemModal extends React.PureComponent { state: State = this.getInitialState() thumbnailInput = React.createRef() @@ -223,7 +232,7 @@ export default class CreateSingleItemModal extends React.PureComponent { - const { address, collection } = this.props + const { address, collection, onSave } = this.props const { id, name, @@ -244,12 +253,15 @@ export default class CreateSingleItemModal extends React.PureComponent | null = collection?.urn ? decodeURN(collection.urn) : null let urn: string | undefined - if ( - decodedCollectionUrn && - decodedCollectionUrn.type === URNType.COLLECTIONS_THIRDPARTY && - decodedCollectionUrn.thirdPartyCollectionId - ) { + if (decodedCollectionUrn && isThirdPartyCollectionDecodedUrn(decodedCollectionUrn)) { urn = buildThirdPartyURN(decodedCollectionUrn.thirdPartyName, decodedCollectionUrn.thirdPartyCollectionId, uuid.v4()) + } else if (decodedCollectionUrn && isThirdPartyV2CollectionDecodedUrn(decodedCollectionUrn)) { + urn = buildThirdPartyV2URN( + decodedCollectionUrn.thirdPartyLinkedCollectionName, + decodedCollectionUrn.linkedCollectionNetwork, + decodedCollectionUrn.linkedCollectionContractAddress, + uuid.v4() + ) } // create item to save @@ -278,7 +290,7 @@ export default class CreateSingleItemModal extends React.PureComponent = { id, name, urn, @@ -303,8 +315,15 @@ export default class CreateSingleItemModal extends React.PureComponent { @@ -416,7 +437,6 @@ export default class CreateSingleItemModal extends React.PureComponent } filter={this.filterItemsByBodyShape} onChange={this.handleItemChange} isDisabled={isAddingRepresentation} @@ -1180,8 +1200,9 @@ export default class CreateSingleItemModal extends React.PureComponent diff --git a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.types.ts b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.types.ts index 86a598869..9e95f37ab 100644 --- a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.types.ts +++ b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.types.ts @@ -40,7 +40,7 @@ export type StateData = { metrics: Metrics contents: Record isRepresentation: boolean - item: Item + item: Item collectionId: string isLoading: boolean error: string diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx index 26358f7b5..1bf5e0f68 100644 --- a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx @@ -22,7 +22,7 @@ import { toFixedMANAValue } from 'decentraland-dapps/dist/lib/mana' import Info from 'components/Info' import { isValid } from 'lib/address' -import { Item } from 'modules/item/types' +import { Item, ItemType } from 'modules/item/types' import { Props, State } from './EditPriceAndBeneficiaryModal.types' import './EditPriceAndBeneficiaryModal.css' @@ -80,13 +80,13 @@ export default class EditPriceAndBeneficiaryModal extends React.PureComponent = { ...item, price: priceInWei, beneficiary } // Send itemSortedContents if this modal was opened from CreateSingleItem modal. - onSave(newItem, itemSortedContents ?? {}) + onSave(newItem as Item, itemSortedContents ?? {}) } } diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts index 679a8c716..8a0c47fc4 100644 --- a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts @@ -1,6 +1,6 @@ import { Dispatch } from 'redux' import { ModalProps } from 'decentraland-dapps/dist/providers/ModalProvider/ModalProvider.types' -import { Item } from 'modules/item/types' +import { Item, ItemType } from 'modules/item/types' import { saveItemRequest, SaveItemRequestAction, @@ -9,7 +9,7 @@ import { } from 'modules/item/actions' export type Props = ModalProps & { - item: Item + item: Item error: string | null isLoading: boolean metadata: EditPriceAndBeneficiaryModalMetadata diff --git a/src/components/ThirdPartyCollectionDetailPage/CollectionItem/CollectionItem.tsx b/src/components/ThirdPartyCollectionDetailPage/CollectionItem/CollectionItem.tsx index 983d399a8..ef530bb58 100644 --- a/src/components/ThirdPartyCollectionDetailPage/CollectionItem/CollectionItem.tsx +++ b/src/components/ThirdPartyCollectionDetailPage/CollectionItem/CollectionItem.tsx @@ -4,7 +4,7 @@ import { Grid, Dropdown, Icon, Button, Checkbox, CheckboxProps, Popup } from 'de import { Link, useHistory } from 'react-router-dom' import { locations } from 'routing/locations' import { preventDefault } from 'lib/event' -import { decodeURN, URNType } from 'lib/urn' +import { decodeURN, isThirdPartyCollectionDecodedUrn, isThirdPartyV2CollectionDecodedUrn } from 'lib/urn' import ItemStatus from 'components/ItemStatus' import { SyncStatus } from 'modules/item/types' import { FromParam } from 'modules/location/types' @@ -47,8 +47,14 @@ export default function CollectionItem({ item, status, selected, onSelect, onOpe return '' } - const decodedURN = decodeURN(item.urn) - return decodedURN.type === URNType.COLLECTIONS_THIRDPARTY ? decodedURN.thirdPartyTokenId : '' + try { + const decodedURN = decodeURN(item.urn) + return isThirdPartyCollectionDecodedUrn(decodedURN) || isThirdPartyV2CollectionDecodedUrn(decodedURN) + ? decodedURN.thirdPartyTokenId ?? '' + : '' + } catch (error) { + return '' + } }, [item]) const statusIcon = useMemo(() => { diff --git a/src/modules/editor/utils.ts b/src/modules/editor/utils.ts index fa5a2d2f5..03ae7bc45 100644 --- a/src/modules/editor/utils.ts +++ b/src/modules/editor/utils.ts @@ -8,7 +8,7 @@ import { getContentsStorageUrl } from 'lib/api/builder' import { capitalize } from 'lib/text' import { Color4 } from 'lib/colors' import { Vector3 } from 'modules/models/types' -import { getSkinHiddenCategories } from 'modules/item/utils' +import { getSkinHiddenCategories, isEmote } from 'modules/item/utils' import { EntityDefinition, ComponentDefinition, ComponentType, SceneSDK6 } from 'modules/scene/types' import { injectScript } from 'routing/utils' import { base64ArrayBuffer } from './base64' @@ -342,7 +342,7 @@ export function toEmote(item: Item): EmoteDefinition { * @param item - an Item */ export function toBase64(item: Item | Item): string { - const wearable = item.type === ItemType.EMOTE ? toEmote(item as Item) : toWearable(item as Item) + const wearable = isEmote(item) ? toEmote(item) : toWearable(item) const stringified = JSON.stringify(wearable) const sanitized = stringified.replace(/[^\x20-\x7F]/g, '') return btoa(sanitized) diff --git a/src/modules/item/reducer.ts b/src/modules/item/reducer.ts index 3fc9966b9..58001ae11 100644 --- a/src/modules/item/reducer.ts +++ b/src/modules/item/reducer.ts @@ -110,7 +110,15 @@ import { } from 'modules/thirdParty/actions' import { toItemObject } from './utils' import { Item, BlockchainRarity } from './types' -import { buildCatalystItemURN, buildThirdPartyURN, decodeURN, URNType } from 'lib/urn' +import { + buildCatalystItemURN, + buildThirdPartyURN, + buildThirdPartyV2URN, + decodeURN, + isThirdPartyCollectionDecodedUrn, + isThirdPartyV2CollectionDecodedUrn, + URNType +} from 'lib/urn' import { CLOSE_MODAL, CloseModalAction } from 'decentraland-dapps/dist/modules/modal/actions' export type ItemPaginationData = { @@ -509,15 +517,18 @@ export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReduce if (item.collectionId === collection.id && item.urn) { let newItemURN: string const itemURN = decodeURN(item.urn) - if (collectionURN.type === URNType.COLLECTIONS_THIRDPARTY) { - if (itemURN.type !== URNType.COLLECTIONS_THIRDPARTY) { + if (isThirdPartyCollectionDecodedUrn(collectionURN) || isThirdPartyV2CollectionDecodedUrn(collectionURN)) { + if (!isThirdPartyCollectionDecodedUrn(itemURN) && !isThirdPartyV2CollectionDecodedUrn(itemURN)) { throw new Error(`The item ${item.id} is not part of a third-party collection but it should be`) } - newItemURN = buildThirdPartyURN( - collectionURN.thirdPartyName, - collectionURN.thirdPartyCollectionId!, - itemURN.thirdPartyTokenId - ) + newItemURN = isThirdPartyCollectionDecodedUrn(collectionURN) + ? buildThirdPartyURN(collectionURN.thirdPartyName, collectionURN.thirdPartyCollectionId, itemURN.thirdPartyTokenId) + : buildThirdPartyV2URN( + collectionURN.thirdPartyLinkedCollectionName, + collectionURN.linkedCollectionNetwork, + collectionURN.linkedCollectionContractAddress, + itemURN.thirdPartyTokenId + ) } else if (collectionURN.type === URNType.COLLECTIONS_V2) { if (itemURN.type !== URNType.COLLECTIONS_V2) { throw new Error(`The item ${item.id} is not part of a decentraland collection but it should be`) diff --git a/src/modules/item/sagas.ts b/src/modules/item/sagas.ts index 1453f5d74..d7b2bbefd 100644 --- a/src/modules/item/sagas.ts +++ b/src/modules/item/sagas.ts @@ -131,17 +131,7 @@ import { getCatalystContentUrl } from 'lib/api/peer' import { downloadZip } from 'lib/zip' import { isErrorWithCode } from 'lib/error' import { calculateModelFinalSize, calculateFileSize, reHashOlderContents } from './export' -import { - Item, - BlockchainRarity, - CatalystItem, - BodyShapeType, - IMAGE_PATH, - THUMBNAIL_PATH, - WearableData, - ItemType, - VIDEO_PATH -} from './types' +import { Item, BlockchainRarity, CatalystItem, BodyShapeType, IMAGE_PATH, THUMBNAIL_PATH, WearableData, VIDEO_PATH } from './types' import { getData as getItemsById, getItems, getEntityByItemId, getCollectionItems, getItem, getPaginationData } from './selectors' import { ItemEmoteTooBigError, @@ -151,7 +141,17 @@ import { ThumbnailFileTooBigError, VideoFileTooBigError } from './errors' -import { buildZipContents, getMetadata, groupsOf, isValidText, generateCatalystImage, MAX_VIDEO_FILE_SIZE, isSmart } from './utils' +import { + buildZipContents, + getMetadata, + groupsOf, + isValidText, + generateCatalystImage, + MAX_VIDEO_FILE_SIZE, + isSmart, + isWearable, + isEmote +} from './utils' import { ItemPaginationData } from './reducer' import { getSuccessfulDeletedItemToast, getSuccessfulMoveItemToAnotherCollectionToast } from './toasts' @@ -362,10 +362,7 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien const oldItem: Item | undefined = yield select(getItem, actionItem.id) const rarityChanged = oldItem && oldItem.rarity !== item.rarity const shouldValidateCategoryChanged = - !!oldItem && - oldItem.type === ItemType.WEARABLE && - oldItem.data.category !== item.data.category && - oldItem.data.category === WearableCategory.SKIN + !!oldItem && isWearable(oldItem) && oldItem.data.category !== item.data.category && oldItem.data.category === WearableCategory.SKIN if (!isValidText(item.name) || !isValidText(item.description)) { throw new Error(t('sagas.item.invalid_character')) @@ -428,11 +425,11 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien } } - const isEmote = item.type === ItemType.EMOTE - const isSkin = !isEmote && item.data.category === WearableCategory.SKIN + const isEmoteItem = isEmote(item) + const isSkin = !isEmoteItem && item.data.category === WearableCategory.SKIN const isSmartWearable = isSmart(item) - if (isEmote && finalModelSize > MAX_EMOTE_FILE_SIZE) { + if (isEmoteItem && finalModelSize > MAX_EMOTE_FILE_SIZE) { throw new ItemEmoteTooBigError() } @@ -444,7 +441,7 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien throw new ItemSmartWearableTooBigError() } - if (!isSkin && !isSmartWearable && !isEmote && finalModelSize > MAX_WEARABLE_FILE_SIZE) { + if (!isSkin && !isSmartWearable && !isEmoteItem && finalModelSize > MAX_WEARABLE_FILE_SIZE) { throw new ItemWearableTooBigError() } } @@ -489,7 +486,7 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien if (ItemModals.some(modal => openModals[modal])) { yield put(closeAllModals()) } else if (openModals['CreateSingleItemModal']) { - if (location.pathname === locations.collectionDetail(collectionId) && item.type === ItemType.EMOTE) { + if (location.pathname === locations.collectionDetail(collectionId) && isEmote(item)) { // Redirect to the item editor yield put(setItems([item])) history.push(locations.itemEditor({ collectionId, itemId: item.id, newItem: item.name }), { fromParam: FromParam.COLLECTIONS }) diff --git a/src/modules/item/selectors.ts b/src/modules/item/selectors.ts index 145b957e0..99a767361 100644 --- a/src/modules/item/selectors.ts +++ b/src/modules/item/selectors.ts @@ -18,8 +18,8 @@ import { isEqual } from 'lib/address' import { buildCatalystItemURN, isThirdParty } from '../../lib/urn' import { DOWNLOAD_ITEM_REQUEST } from './actions' import { ItemState } from './reducer' -import { Item, SyncStatus, BlockchainRarity, CatalystItem, ItemType, VIDEO_PATH } from './types' -import { areSynced, canSeeItem, isOwner, isSmart } from './utils' +import { Item, SyncStatus, BlockchainRarity, CatalystItem, VIDEO_PATH } from './types' +import { areSynced, canSeeItem, isEmote, isOwner, isSmart, isWearable } from './utils' import { getSearch } from 'connected-react-router' export const getState = (state: RootState) => state.item @@ -84,11 +84,9 @@ export const getRarities = (state: RootState): BlockchainRarity[] => { return getState(state).rarities } -export const getWearables = createSelector(getItems, items => - items.filter(item => item.type === ItemType.WEARABLE) -) +export const getWearables = createSelector(getItems, items => items.filter(isWearable)) -export const getEmotes = createSelector(getItems, items => items.filter(item => item.type === ItemType.EMOTE)) +export const getEmotes = createSelector(getItems, items => items.filter(isEmote)) export const getItemsByURN = createSelector, Record>( state => getItems(state),