diff --git a/src/components/CollectionDetailPage/CollectionDetailPage.container.ts b/src/components/CollectionDetailPage/CollectionDetailPage.container.ts index 32878417e..b2d27cef2 100644 --- a/src/components/CollectionDetailPage/CollectionDetailPage.container.ts +++ b/src/components/CollectionDetailPage/CollectionDetailPage.container.ts @@ -5,10 +5,11 @@ import { getData as getWallet } from 'decentraland-dapps/dist/modules/wallet/sel import { RootState } from 'modules/common/types' import { getCollectionId } from 'modules/location/selectors' import { getCollection, isOnSaleLoading, getLoading as getLoadingCollection, getStatusByCollectionId } from 'modules/collection/selectors' -import { DELETE_COLLECTION_REQUEST } from 'modules/collection/actions' +import { DELETE_COLLECTION_REQUEST, SET_COLLECTION_MINTERS_REQUEST } from 'modules/collection/actions' import { openModal } from 'modules/modal/actions' import { getCollectionItems } from 'modules/item/selectors' import { ItemType } from 'modules/item/types' +import { fetchCollectionForumPostReplyRequest, FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST } from 'modules/forum/actions' import { MapStateProps, MapDispatchProps, MapDispatch } from './CollectionDetailPage.types' import CollectionDetailPage from './CollectionDetailPage' @@ -22,16 +23,19 @@ const mapState = (state: RootState): MapStateProps => { tab: tab ? (tab as ItemType) : undefined, wallet: getWallet(state)!, collection, - isOnSaleLoading: isOnSaleLoading(state), + isOnSaleLoading: isOnSaleLoading(state) || isLoadingType(getLoadingCollection(state), SET_COLLECTION_MINTERS_REQUEST), items: getCollectionItems(state, collectionId), status: statusByCollectionId[collectionId], - isLoading: isLoadingType(getLoadingCollection(state), DELETE_COLLECTION_REQUEST) + isLoading: + isLoadingType(getLoadingCollection(state), DELETE_COLLECTION_REQUEST) || + isLoadingType(getLoadingCollection(state), FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST) } } const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ onNavigate: (path, locationState) => dispatch(push(path, locationState)), - onOpenModal: (name, metadata) => dispatch(openModal(name, metadata)) + onOpenModal: (name, metadata) => dispatch(openModal(name, metadata)), + onFetchCollectionForumPostReply: id => dispatch(fetchCollectionForumPostReplyRequest(id)) }) export default connect(mapState, mapDispatch)(CollectionDetailPage) diff --git a/src/components/CollectionDetailPage/CollectionDetailPage.css b/src/components/CollectionDetailPage/CollectionDetailPage.css index 47f2346e1..1f05dc146 100644 --- a/src/components/CollectionDetailPage/CollectionDetailPage.css +++ b/src/components/CollectionDetailPage/CollectionDetailPage.css @@ -1,3 +1,7 @@ +.CollectionDetailPage > .ui.container > .dcl.section { + margin-bottom: 48px; +} + .CollectionDetailPage .header-column { flex: none; width: calc(100% - 300px); @@ -24,6 +28,14 @@ display: inline-block; } +.CollectionDetailPage .ui.circular.label.badge-on-sale { + margin-left: 1em; + padding: 0.5em 0.667em !important; + color: var(--text); + background-color: var(--primary); + text-transform: uppercase; +} + .CollectionDetailPage .section:not(.is-published) .header-row:hover { cursor: pointer; } @@ -255,10 +267,17 @@ padding: 7px 20px; } -.CollectionDetailPage .dcl.tabs .secondary-actions.tab .button span { +.CollectionDetailPage .dcl.tabs .secondary-actions.tab .button span, +.CollectionDetailPage .dcl.narrow .secondary-actions .button span { color: var(--primary); } +.CollectionDetailPage .badge-forum-unread-posts { + margin-left: 8px; + color: var(--text); + background-color: var(--primary); +} + .unsynced-collection.container { display: flex; padding: 24px 30px; @@ -324,3 +343,8 @@ height: 48px; width: 48px; } + +.CollectionDetailPage.popup-mint, +.CollectionDetailPage.popup-mint:before { + background-color: var(--smart-grey) !important; +} diff --git a/src/components/CollectionDetailPage/CollectionDetailPage.tsx b/src/components/CollectionDetailPage/CollectionDetailPage.tsx index bcc773972..258fd20dc 100644 --- a/src/components/CollectionDetailPage/CollectionDetailPage.tsx +++ b/src/components/CollectionDetailPage/CollectionDetailPage.tsx @@ -1,10 +1,9 @@ import * as React from 'react' import classNames from 'classnames' -import { Link } from 'react-router-dom' import { Network } from '@dcl/schemas' -import { Section, Row, Narrow, Column, Header, Button, Popup, Tabs, Table } from 'decentraland-ui' +import { Section, Row, Narrow, Column, Header, Button, Popup, Tabs, Table, Label } from 'decentraland-ui' import { NetworkCheck } from 'decentraland-dapps/dist/containers' -import { t, T } from 'decentraland-dapps/dist/modules/translation/utils' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { locations } from 'routing/locations' import { FromParam } from 'modules/location/types' import { @@ -20,7 +19,6 @@ import { CollectionType } from 'modules/collection/types' import CollectionProvider from 'components/CollectionProvider' import { Item, ItemType, SyncStatus } from 'modules/item/types' import LoggedInDetailPage from 'components/LoggedInDetailPage' -import Notice from 'components/Notice' import NotFound from 'components/NotFound' import BuilderIcon from 'components/Icon' import Back from 'components/Back' @@ -32,13 +30,33 @@ import CollectionItem from './CollectionItem' import './CollectionDetailPage.css' -const STORAGE_KEY = 'dcl-collection-notice' - export default class CollectionDetailPage extends React.PureComponent { state: State = { tab: this.props.tab || ItemType.WEARABLE } + componentDidMount(): void { + const { collection } = this.props + if (collection) { + this.fetchCollectionForumPostReply() + } + } + + componentDidUpdate(prevProps: Props): void { + const { collection } = this.props + if (!prevProps.collection && collection) { + this.fetchCollectionForumPostReply() + } + } + + fetchCollectionForumPostReply() { + const { collection, onFetchCollectionForumPostReply } = this.props + // Only fetch the forum post replies if the collection has a forum link and there's no other fetch process in progress + if (collection && collection.isPublished && !collection.isApproved && collection.forumLink) { + onFetchCollectionForumPostReply(collection.id) + } + } + handleMintItems = () => { const { collection, onOpenModal } = this.props onOpenModal('MintItemsModal', { collectionId: collection!.id }) @@ -58,9 +76,8 @@ export default class CollectionDetailPage extends React.PureComponent { const { collection, wallet, onOpenModal } = this.props - const toggleIsOnSale = !isCollectionOnSale(collection!, wallet) if (collection) { - onOpenModal('SellCollectionModal', { collectionId: collection.id, isOnSale: toggleIsOnSale }) + onOpenModal('SellCollectionModal', { collectionId: collection.id, isOnSale: isCollectionOnSale(collection, wallet) }) } } @@ -164,7 +181,7 @@ export default class CollectionDetailPage extends React.PureComponent - {isOnSale ? t('collection_detail_page.remove_from_marketplace') : t('collection_detail_page.on_sale')} + {isOnSale ? t('collection_detail_page.remove_from_marketplace') : t('collection_detail_page.put_for_sale')} )} @@ -172,6 +189,21 @@ export default class CollectionDetailPage extends React.PureComponent + {unreadPosts} + + ) + } + + return null + } + renderPage(items: Item[]) { const { tab } = this.state const { status, wallet } = this.props @@ -179,6 +211,7 @@ export default class CollectionDetailPage extends React.PureComponent 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 @@ -209,21 +242,39 @@ export default class CollectionDetailPage extends React.PureComponent + {isOnSale ? ( + + ) : null} )} {collection.isPublished && collection.isApproved ? ( - + + {t('collection_detail_page.mint_items')} + + } + on="hover" + inverted + flowing + /> + ) : null} + {collection.isPublished && collection.forumLink && !collection.isApproved ? ( + <> + + {collection?.forumPostReply ? this.renderForumRepliesBadge() : null} + ) : null} - {collection.isPublished && collection.forumLink && !collection.isApproved && ( - - )} {!(collection.isPublished && collection.isApproved) && status !== SyncStatus.UNSYNCED ? ( ) : null} @@ -238,15 +289,6 @@ export default class CollectionDetailPage extends React.PureComponent - - {t('global.click_here')} - }} - /> - - {status === SyncStatus.UNSYNCED ? (
diff --git a/src/components/CollectionDetailPage/CollectionDetailPage.types.ts b/src/components/CollectionDetailPage/CollectionDetailPage.types.ts index e24bb2069..a49848b4d 100644 --- a/src/components/CollectionDetailPage/CollectionDetailPage.types.ts +++ b/src/components/CollectionDetailPage/CollectionDetailPage.types.ts @@ -4,6 +4,7 @@ import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' import { openModal, OpenModalAction } from 'modules/modal/actions' import { Collection } from 'modules/collection/types' import { Item, ItemType, SyncStatus } from 'modules/item/types' +import { fetchCollectionForumPostReplyRequest, FetchCollectionForumPostReplyRequestAction } from 'modules/forum/actions' export type Props = { tab?: ItemType @@ -15,6 +16,7 @@ export type Props = { status: SyncStatus onNavigate: (path: string, prop?: CollectionDetailLocationState) => void onOpenModal: typeof openModal + onFetchCollectionForumPostReply: typeof fetchCollectionForumPostReplyRequest } export type CollectionDetailLocationState = { @@ -26,5 +28,5 @@ export type State = { } export type MapStateProps = Pick -export type MapDispatchProps = Pick -export type MapDispatch = Dispatch +export type MapDispatchProps = Pick +export type MapDispatch = Dispatch diff --git a/src/components/CollectionDetailPage/CollectionPublishButton/CollectionPublishButton.tsx b/src/components/CollectionDetailPage/CollectionPublishButton/CollectionPublishButton.tsx index 0d1e6b854..ec78c4e6c 100644 --- a/src/components/CollectionDetailPage/CollectionPublishButton/CollectionPublishButton.tsx +++ b/src/components/CollectionDetailPage/CollectionPublishButton/CollectionPublishButton.tsx @@ -70,7 +70,7 @@ const CollectionPublishButton = (props: Props) => { } else { const publishButton = ( - {t('global.publish')} + {t('collection_detail_page.publish')} ) diff --git a/src/components/CollectionImage/CollectionImage.css b/src/components/CollectionImage/CollectionImage.css index f60853ef7..602cd8f6b 100644 --- a/src/components/CollectionImage/CollectionImage.css +++ b/src/components/CollectionImage/CollectionImage.css @@ -11,15 +11,16 @@ height: 100%; } -.CollectionImage .item-row.empty { +.CollectionImage > .item-row.empty { display: flex; flex-direction: column; justify-content: center; height: 100%; color: var(--secondary-text); - border: 1px solid var(--divider); - border-radius: 5px; + border: 1px solid var(--secondary-text); text-transform: uppercase; + margin-top: 0; + padding: 0; } .CollectionImage .item { diff --git a/src/components/CollectionsPage/CollectionsPage.css b/src/components/CollectionsPage/CollectionsPage.css index 891d87260..4fa66873a 100644 --- a/src/components/CollectionsPage/CollectionsPage.css +++ b/src/components/CollectionsPage/CollectionsPage.css @@ -89,7 +89,12 @@ } .CollectionsPage .filters .dcl.row.actions .ui.button.open-editor { - padding: 6px 13px; + display: flex; + align-items: center; +} + +.CollectionsPage .filters .dcl.row.actions .ui.button.open-editor .Icon { + margin-right: 6px; } .CollectionsPage .filters .dcl.row.actions .ui.button.open-editor:hover { diff --git a/src/components/CollectionsPage/CollectionsPage.tsx b/src/components/CollectionsPage/CollectionsPage.tsx index 6429f5073..13d86ab5e 100644 --- a/src/components/CollectionsPage/CollectionsPage.tsx +++ b/src/components/CollectionsPage/CollectionsPage.tsx @@ -36,7 +36,7 @@ const PAGE_SIZE = 20 export default class CollectionsPage extends React.PureComponent { state = { currentTab: TABS.COLLECTIONS, - sort: CurationSortOptions.MOST_RELEVANT, + sort: CurationSortOptions.CREATED_AT_DESC, page: 1 } @@ -44,7 +44,7 @@ export default class CollectionsPage extends React.PureComponent { const { address, hasUserOrphanItems, onFetchCollections, onFetchOrphanItem } = this.props // fetch if already connected if (address) { - onFetchCollections(address, { page: 1, limit: PAGE_SIZE, sort: CurationSortOptions.MOST_RELEVANT }) + onFetchCollections(address, { page: 1, limit: PAGE_SIZE, sort: CurationSortOptions.CREATED_AT_DESC }) // TODO: Remove this call when there are no users with orphan items if (hasUserOrphanItems === undefined) { onFetchOrphanItem(address) @@ -186,31 +186,18 @@ export default class CollectionsPage extends React.PureComponent { return ( - {this.isCollectionTabActive() && ( - <> - - - - } - inline - direction="left" - > - - <> - - {isThirdPartyManager && ( - - )} - - - - + {isThirdPartyManager && ( + )} - + ) diff --git a/src/components/CurationPage/CurationPage.tsx b/src/components/CurationPage/CurationPage.tsx index 3ee58bb70..09925c9bd 100644 --- a/src/components/CurationPage/CurationPage.tsx +++ b/src/components/CurationPage/CurationPage.tsx @@ -42,7 +42,7 @@ const CAMPAIGN_TAG = 'HolidaySeason' export default class CurationPage extends React.PureComponent { state: State = { - sortBy: CurationSortOptions.MOST_RELEVANT, + sortBy: CurationSortOptions.CREATED_AT_DESC, filterByStatus: CurationExtraStatuses.ALL_STATUS, filterByTags: [], filterByType: CollectionExtraTypes.ALL_TYPES, diff --git a/src/components/Modals/PublishWizardCollectionModal/CongratulationsStep/CongratulationsStep.css b/src/components/Modals/PublishWizardCollectionModal/CongratulationsStep/CongratulationsStep.css index 50ae7e8e7..b0aeb9b7f 100644 --- a/src/components/Modals/PublishWizardCollectionModal/CongratulationsStep/CongratulationsStep.css +++ b/src/components/Modals/PublishWizardCollectionModal/CongratulationsStep/CongratulationsStep.css @@ -14,7 +14,7 @@ height: 128px; width: 128px; margin: 50px auto; - background-image: url('../../../../images/sparkles.svg'); + background-image: url('../../../../images/sparkles_colors.svg'); background-repeat: no-repeat; background-size: contain; } diff --git a/src/components/Modals/SellCollectionModal/SellCollectionModal.css b/src/components/Modals/SellCollectionModal/SellCollectionModal.css index c67bf7fe7..dff570840 100644 --- a/src/components/Modals/SellCollectionModal/SellCollectionModal.css +++ b/src/components/Modals/SellCollectionModal/SellCollectionModal.css @@ -1,3 +1,7 @@ +.SellCollectionModal.ui.modal .dcl.modal-navigation-title { + text-align: center; +} + .SellCollectionModal.ui.modal > .content { text-align: center; margin-left: 60px; diff --git a/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx b/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx index 2457f688e..7c2f4c383 100644 --- a/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx +++ b/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx @@ -8,34 +8,24 @@ import { Props } from './SellCollectionModal.types' import './SellCollectionModal.css' export default class SellCollectionModal extends React.PureComponent { - handleSell = () => { + handleToggleOnSale = () => { const { collection, wallet, metadata, onSetMinters } = this.props - onSetMinters(collection, setOnSale(collection, wallet, metadata.isOnSale)) + onSetMinters(collection, setOnSale(collection, wallet, !metadata.isOnSale)) } render() { const { metadata, isLoading, hasUnsyncedItems, onClose } = this.props + const tKey = metadata.isOnSale ? 'remove_from_marketplace' : 'put_for_sale' return ( - + {hasUnsyncedItems &&

{t('sell_collection_modal.unsynced_warning')}

} - {metadata.isOnSale ? ( - <> - {t('sell_collection_modal.turn_on_description')} - - - ) : ( - <> - {t('sell_collection_modal.turn_off_description')} - - - )} - +
diff --git a/src/config/env/dev.json b/src/config/env/dev.json index cac98cd28..04927a78a 100644 --- a/src/config/env/dev.json +++ b/src/config/env/dev.json @@ -30,5 +30,6 @@ "ACCOUNT_URL": "https://account.decentraland.zone", "DISCORD_URL": "https://dcl.gg/discord", "WEARABLES_ZIP_INFRA_URL": "http://a-test-url-com", - "MIN_SALE_VALUE_IN_WEI": "1000000000000000000" + "MIN_SALE_VALUE_IN_WEI": "1000000000000000000", + "FORUM_URL": "https://forum.decentraland.org" } diff --git a/src/config/env/prod.json b/src/config/env/prod.json index 7464e7c10..2b9a4e52a 100644 --- a/src/config/env/prod.json +++ b/src/config/env/prod.json @@ -30,5 +30,6 @@ "ACCOUNT_URL": "https://account.decentraland.org", "DISCORD_URL": "https://dcl.gg/discord", "WEARABLES_ZIP_INFRA_URL": "https://docs.decentraland.org/decentraland/linked-wearables/#creating-linked-wearables-in-bulk", - "MIN_SALE_VALUE_IN_WEI": "1000000000000000000" + "MIN_SALE_VALUE_IN_WEI": "1000000000000000000", + "FORUM_URL": "https://forum.decentraland.org" } diff --git a/src/config/env/stg.json b/src/config/env/stg.json index 1845508a3..8e4e0c75b 100644 --- a/src/config/env/stg.json +++ b/src/config/env/stg.json @@ -30,5 +30,6 @@ "ACCOUNT_URL": "https://account.decentraland.today", "DISCORD_URL": "https://dcl.gg/discord", "WEARABLES_ZIP_INFRA_URL": "https://docs.decentraland.org/decentraland/linked-wearables/#creating-linked-wearables-in-bulk", - "MIN_SALE_VALUE_IN_WEI": "1000000000000000000" + "MIN_SALE_VALUE_IN_WEI": "1000000000000000000", + "FORUM_URL": "https://forum.decentraland.org" } diff --git a/src/images/sparkles_colors.svg b/src/images/sparkles_colors.svg new file mode 100644 index 000000000..451869ba3 --- /dev/null +++ b/src/images/sparkles_colors.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/lib/api/builder.ts b/src/lib/api/builder.ts index 4456a39f5..03913f82f 100644 --- a/src/lib/api/builder.ts +++ b/src/lib/api/builder.ts @@ -18,7 +18,7 @@ import { Account } from 'modules/committee/types' import { Collection, CollectionType } from 'modules/collection/types' import { Cheque, ThirdParty } from 'modules/thirdParty/types' import { PreviewType } from 'modules/editor/types' -import { ForumPost } from 'modules/forum/types' +import { ForumPost, ForumPostReply } from 'modules/forum/types' import { ModelMetrics } from 'modules/models/types' import { CollectionCuration } from 'modules/curations/collectionCuration/types' import { CurationSortOptions, CurationStatus } from 'modules/curations/types' @@ -27,6 +27,7 @@ import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, PaginatedResource } from './pagination import { Authorization } from './auth' export const BUILDER_SERVER_URL = config.get('BUILDER_SERVER_URL', '') +export const FORUM_URL = config.get('FORUM_URL', '') export const getContentsStorageUrl = (hash = '') => `${BUILDER_SERVER_URL}/storage/contents/${hash}` export const getAssetPackStorageUrl = (hash = '') => `${BUILDER_SERVER_URL}/storage/assetPacks/${hash}` @@ -834,6 +835,20 @@ export class BuilderAPI extends BaseAPI { return this.request('post', `/collections/${collection.id}/post`, { forumPost }) as Promise } + /* Getting the forum post replies in the front to utilize the forum session + * and get the latest read messages by the creator. + */ + async getCollectionForumPostReply(topicId: string): Promise { + const response = await fetch(`${FORUM_URL}/t/${topicId}.json`) + const data = await response.json() + return { + topic_id: data.id, + highest_post_number: data.highest_post_number, + show_read_indicator: data.show_read_indicator, + last_read_post_number: data?.last_read_post_number + } + } + async createCollectionNewAssigneeForumPost(collection: Collection, forumPost: ForumPost): Promise { return this.request('post', `/collections/${collection.id}/curation/post`, { forumPost }) as Promise } diff --git a/src/modules/collection/reducer.spec.ts b/src/modules/collection/reducer.spec.ts index 8a5b8b22a..40c257fd1 100644 --- a/src/modules/collection/reducer.spec.ts +++ b/src/modules/collection/reducer.spec.ts @@ -1,6 +1,12 @@ import { fetchTransactionSuccess } from 'decentraland-dapps/dist/modules/transaction/actions' import { FetchCollectionsParams } from 'lib/api/builder' import { PaginationStats } from 'lib/api/pagination' +import { + fetchCollectionForumPostReplyFailure, + fetchCollectionForumPostReplyRequest, + fetchCollectionForumPostReplySuccess +} from 'modules/forum/actions' +import { ForumPostReply } from 'modules/forum/types' import { closeAllModals, closeModal } from 'modules/modal/actions' import { fetchCollectionsRequest, fetchCollectionsSuccess, PUBLISH_COLLECTION_SUCCESS } from './actions' import { collectionReducer as reducer, CollectionState } from './reducer' @@ -186,3 +192,89 @@ describe('when all modals are closed', () => { expect(reducer(initialState, closeAllModals()).error).toBe(null) }) }) + +describe('when an action of type FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST is called', () => { + let initialState: CollectionState + let collection: Collection + let topicId: string + beforeEach(() => { + collection = { id: 'anExistingCollectionId' } as Collection + topicId = '1234' + initialState = { + data: { + [collection.id]: { + id: collection.id, + forumLink: `https://forum.decentraland.org/t/collection-cucos-created-by-MrCuco-is-ready-for-review/${topicId}` + } + } + } as CollectionState + }) + + it('should set the loading state', () => { + const state = reducer(initialState, fetchCollectionForumPostReplyRequest(collection.id)) + expect(reducer(initialState, fetchCollectionForumPostReplyRequest(collection.id))).toEqual({ + ...state, + loading: [fetchCollectionForumPostReplyRequest(collection.id)] + }) + }) +}) + +describe('when an action of type FETCH_COLLECTION_FORUM_POST_REPLY_SUCCESS is called', () => { + let initialState: CollectionState + let collection: Collection + let topicId: string + beforeEach(() => { + collection = { id: 'anExistingCollectionId' } as Collection + topicId = '1234' + initialState = { + data: { + [collection.id]: { + id: collection.id, + forumLink: `https://forum.decentraland.org.decentraland.org/t/collection-cucos-created-by-MrCuco-is-ready-for-review/${topicId}` + } + } + } as CollectionState + }) + + it('should set the forumPostReply state', () => { + const forumPostReply: ForumPostReply = { + topic_id: topicId, + highest_post_number: 1, + show_read_indicator: true, + last_read_post_number: 0 + } + const state = reducer(initialState, fetchCollectionForumPostReplySuccess(collection, forumPostReply)) + expect(reducer(initialState, fetchCollectionForumPostReplySuccess(collection, forumPostReply))).toEqual({ + ...state, + data: { + ...initialState.data, + [collection.id]: { ...initialState.data[collection.id], forumPostReply } + }, + loading: [] + }) + }) +}) + +describe('when an action of type FETCH_COLLECTION_FORUM_POST_REPLY_FAILURE is called', () => { + let initialState: CollectionState + let collection: Collection + let topicId: number + beforeEach(() => { + collection = { id: 'anExistingCollectionId' } as Collection + topicId = 1234 + initialState = { + data: { + [collection.id]: { + id: collection.id, + forumLink: `https://forum.decentraland.org/t/collection-cucos-created-by-MrCuco-is-ready-for-review/${topicId}` + } + } + } as CollectionState + }) + + it('should set the error state', () => { + const error = 'anError' + const state = reducer(initialState, fetchCollectionForumPostReplyFailure(collection, error)) + expect(reducer(initialState, fetchCollectionForumPostReplyFailure(collection, error))).toEqual({ ...state, error, loading: [] }) + }) +}) diff --git a/src/modules/collection/reducer.ts b/src/modules/collection/reducer.ts index a50fc4b7b..a0d469998 100644 --- a/src/modules/collection/reducer.ts +++ b/src/modules/collection/reducer.ts @@ -10,7 +10,13 @@ import { CreateCollectionForumPostFailureAction, CREATE_COLLECTION_FORUM_POST_REQUEST, CREATE_COLLECTION_FORUM_POST_SUCCESS, - CREATE_COLLECTION_FORUM_POST_FAILURE + CREATE_COLLECTION_FORUM_POST_FAILURE, + FetchCollectionForumPostReplyRequestAction, + FetchCollectionForumPostReplySuccessAction, + FetchCollectionForumPostReplyFailureAction, + FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST, + FETCH_COLLECTION_FORUM_POST_REPLY_SUCCESS, + FETCH_COLLECTION_FORUM_POST_REPLY_FAILURE } from 'modules/forum/actions' import { PublishThirdPartyItemsSuccessAction, PUBLISH_THIRD_PARTY_ITEMS_SUCCESS } from 'modules/thirdParty/actions' import { @@ -138,6 +144,9 @@ type CollectionReducerAction = | CreateCollectionForumPostRequestAction | CreateCollectionForumPostSuccessAction | CreateCollectionForumPostFailureAction + | FetchCollectionForumPostReplyRequestAction + | FetchCollectionForumPostReplySuccessAction + | FetchCollectionForumPostReplyFailureAction | PublishThirdPartyItemsSuccessAction export function collectionReducer(state: CollectionState = INITIAL_STATE, action: CollectionReducerAction): CollectionState { @@ -163,6 +172,7 @@ export function collectionReducer(state: CollectionState = INITIAL_STATE, action case MINT_COLLECTION_ITEMS_REQUEST: case MINT_COLLECTION_ITEMS_SUCCESS: case CREATE_COLLECTION_FORUM_POST_REQUEST: + case FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST: case APPROVE_COLLECTION_SUCCESS: { return { ...state, @@ -277,6 +287,20 @@ export function collectionReducer(state: CollectionState = INITIAL_STATE, action loading: loadingReducer(state.loading, action) } } + case FETCH_COLLECTION_FORUM_POST_REPLY_SUCCESS: { + const { collection, forumPostReply } = action.payload + return { + ...state, + data: { + ...state.data, + [collection.id]: { + ...state.data[collection.id], + forumPostReply + } + }, + loading: loadingReducer(state.loading, action) + } + } case FETCH_COLLECTIONS_FAILURE: case FETCH_COLLECTION_FAILURE: case SAVE_COLLECTION_FAILURE: @@ -287,7 +311,8 @@ export function collectionReducer(state: CollectionState = INITIAL_STATE, action case SET_COLLECTION_MINTERS_FAILURE: case SET_COLLECTION_MANAGERS_FAILURE: case MINT_COLLECTION_ITEMS_FAILURE: - case CREATE_COLLECTION_FORUM_POST_FAILURE: { + case CREATE_COLLECTION_FORUM_POST_FAILURE: + case FETCH_COLLECTION_FORUM_POST_REPLY_FAILURE: { return { ...state, loading: loadingReducer(state.loading, action), diff --git a/src/modules/collection/types.ts b/src/modules/collection/types.ts index cf4a1636a..dc49696ea 100644 --- a/src/modules/collection/types.ts +++ b/src/modules/collection/types.ts @@ -1,4 +1,5 @@ import { Item } from 'modules/item/types' +import { ForumPostReply } from 'modules/forum/types' export type Collection = { id: string @@ -13,6 +14,7 @@ export type Collection = { minters: string[] managers: string[] forumLink?: string + forumPostReply?: ForumPostReply lock?: number reviewedAt?: number createdAt: number diff --git a/src/modules/curations/types.ts b/src/modules/curations/types.ts index 8eff77ab4..1b5568f8f 100644 --- a/src/modules/curations/types.ts +++ b/src/modules/curations/types.ts @@ -9,7 +9,6 @@ export enum CurationStatus { export enum CurationSortOptions { MOST_RELEVANT = 'MOST_RELEVANT', - NEWEST = 'NEWEST', NAME_DESC = 'NAME_DESC', NAME_ASC = 'NAME_ASC', CREATED_AT_DESC = 'CREATED_AT_DESC', diff --git a/src/modules/forum/actions.ts b/src/modules/forum/actions.ts index 0cf6151b9..6790f40c7 100644 --- a/src/modules/forum/actions.ts +++ b/src/modules/forum/actions.ts @@ -1,7 +1,7 @@ import { action } from 'typesafe-actions' import { CollectionCuration } from 'modules/curations/collectionCuration/types' import { Collection } from 'modules/collection/types' -import { ForumPost } from './types' +import { ForumPost, ForumPostReply } from './types' // Create collection forum post @@ -23,6 +23,23 @@ export type CreateCollectionForumPostRequestAction = ReturnType export type CreateCollectionForumPostFailureAction = ReturnType +// Get collection forum post reply + +export const FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST = '[Request] Fetch collection forum post reply' +export const FETCH_COLLECTION_FORUM_POST_REPLY_SUCCESS = '[Success] Fetch collection forum post reply' +export const FETCH_COLLECTION_FORUM_POST_REPLY_FAILURE = '[Failure] Fetch collection forum post reply' + +export const fetchCollectionForumPostReplyRequest = (collectionId: Collection['id']) => + action(FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST, { collectionId }) +export const fetchCollectionForumPostReplySuccess = (collection: Collection, forumPostReply: ForumPostReply) => + action(FETCH_COLLECTION_FORUM_POST_REPLY_SUCCESS, { collection, forumPostReply }) +export const fetchCollectionForumPostReplyFailure = (collection: Collection, error: string) => + action(FETCH_COLLECTION_FORUM_POST_REPLY_FAILURE, { collection, error }) + +export type FetchCollectionForumPostReplyRequestAction = ReturnType +export type FetchCollectionForumPostReplySuccessAction = ReturnType +export type FetchCollectionForumPostReplyFailureAction = ReturnType + // Create collection assignee event forum post export const CREATE_COLLECTION_ASSIGNEE_FORUM_POST_REQUEST = '[Request] Create collection assignee forum post' diff --git a/src/modules/forum/sagas.spec.ts b/src/modules/forum/sagas.spec.ts index d4172794b..34ad4d0f4 100644 --- a/src/modules/forum/sagas.spec.ts +++ b/src/modules/forum/sagas.spec.ts @@ -11,16 +11,22 @@ import { throwError } from 'redux-saga-test-plan/providers' import { createCollectionAssigneeForumPostRequest, createCollectionAssigneeForumPostFailure, - createCollectionAssigneeForumPostSuccess + createCollectionAssigneeForumPostSuccess, + fetchCollectionForumPostReplySuccess, + fetchCollectionForumPostReplyRequest, + fetchCollectionForumPostReplyFailure } from './actions' import { forumSaga } from './sagas' -import { ForumPost } from './types' +import { ForumPost, ForumPostReply } from './types' import { buildCollectionNewAssigneePostBody } from './utils' +const FORUM_URL = 'https://forum.decentraland.org' + const mockErrorMessage = 'Some Error' const mockBuilder = { - createCollectionNewAssigneeForumPost: jest.fn() + createCollectionNewAssigneeForumPost: jest.fn(), + getCollectionForumPostReply: jest.fn() } as any as BuilderAPI afterEach(() => { @@ -34,7 +40,7 @@ describe('when setting a new curation assignee successfully', () => { beforeEach(() => { mockedCollection = { id: 'anId', - forumLink: 'https://forum.decentraland.org/t/collection-cucos-created-by-MrCuco-is-ready-for-review/10713' + forumLink: `${FORUM_URL}/t/collection-cucos-created-by-MrCuco-is-ready-for-review/10713` } as Collection mockedCuration = { id: 'curationId', @@ -64,7 +70,7 @@ describe('when creating the new assignee forum post', () => { beforeEach(() => { mockedCollection = { id: 'anId', - forumLink: 'https://forum.decentraland.org/t/collection-cucos-created-by-MrCuco-is-ready-for-review/10713' + forumLink: `${FORUM_URL}/t/collection-cucos-created-by-MrCuco-is-ready-for-review/10713` } as Collection mockedCuration = { id: 'curationId', @@ -111,3 +117,52 @@ describe('when creating the new assignee forum post', () => { }) }) }) + +describe('when fetching collection forum post reply', () => { + let mockedCollection: Collection + let topicId: string + + beforeEach(() => { + topicId = '1234' + mockedCollection = { + id: 'anId', + forumLink: `${FORUM_URL}/t/collection-cucos-created-by-MrCuco-is-ready-for-review/${topicId}` + } as Collection + }) + + describe('and the collection has a valid forum link', () => { + it('should put forum post reply success action', () => { + const forumPostReply: ForumPostReply = { + topic_id: topicId, + highest_post_number: 1, + show_read_indicator: true, + last_read_post_number: 0 + } + return expectSaga(forumSaga, mockBuilder) + .provide([ + [select(getCollection, mockedCollection.id), mockedCollection], + [call([mockBuilder, mockBuilder.getCollectionForumPostReply], topicId), forumPostReply] + ]) + .dispatch(fetchCollectionForumPostReplyRequest(mockedCollection.id)) + .put(fetchCollectionForumPostReplySuccess(mockedCollection, forumPostReply)) + .run({ silenceTimeout: true }) + }) + }) + + describe('and the collection does not has a valid forum link', () => { + beforeEach(() => { + mockedCollection = { + ...mockedCollection, + forumLink: `${FORUM_URL}/t/collection-cucos-created-by-MrCuco-is-ready-for-review/` + } + }) + it('should put forum post reply failure action', () => { + const error = `Invalid forum topic id for the collection id: ${mockedCollection.id}` + return expectSaga(forumSaga, mockBuilder) + .provide([[select(getCollection, mockedCollection.id), mockedCollection]]) + .dispatch(fetchCollectionForumPostReplyRequest(mockedCollection.id)) + .put(fetchCollectionForumPostReplyFailure(mockedCollection, error)) + .run({ silenceTimeout: true }) + }) + }) +}) diff --git a/src/modules/forum/sagas.ts b/src/modules/forum/sagas.ts index f99f53a16..19c503cef 100644 --- a/src/modules/forum/sagas.ts +++ b/src/modules/forum/sagas.ts @@ -22,8 +22,13 @@ import { createCollectionForumPostSuccess, CREATE_COLLECTION_ASSIGNEE_FORUM_POST_REQUEST, CREATE_COLLECTION_FORUM_POST_FAILURE, - CREATE_COLLECTION_FORUM_POST_REQUEST + CREATE_COLLECTION_FORUM_POST_REQUEST, + fetchCollectionForumPostReplyFailure, + FetchCollectionForumPostReplyRequestAction, + fetchCollectionForumPostReplySuccess, + FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST } from './actions' +import { ForumPostReply } from './types' const RETRY_DELAY = 5000 @@ -32,6 +37,7 @@ export function* forumSaga(builder: BuilderAPI) { yield takeEvery(CREATE_COLLECTION_FORUM_POST_FAILURE, handleCreateForumPostFailure) yield takeEvery(SET_COLLECTION_CURATION_ASSIGNEE_SUCCESS, handleSetAssigneeSuccess) yield takeEvery(CREATE_COLLECTION_ASSIGNEE_FORUM_POST_REQUEST, handleCreateCollectionAssigneeForumPost) + yield takeEvery(FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST, handleFetchCollectionForumPostReply) function* handleCreateForumPostRequest(action: CreateCollectionForumPostRequestAction) { const { collection, forumPost } = action.payload @@ -70,6 +76,22 @@ export function* forumSaga(builder: BuilderAPI) { yield put(createCollectionAssigneeForumPostRequest(collectionId, curation)) } + function* handleFetchCollectionForumPostReply(action: FetchCollectionForumPostReplyRequestAction) { + const { collectionId } = action.payload + const collection: Collection = yield select(getCollection, collectionId) + const topicId = collection.forumLink ? collection.forumLink.split('/').pop() : null + try { + if (topicId) { + const forumPostReply: ForumPostReply = yield call([builder, 'getCollectionForumPostReply'], topicId) + yield put(fetchCollectionForumPostReplySuccess(collection, forumPostReply)) + } else { + throw new Error(`Invalid forum topic id for the collection id: ${collectionId}`) + } + } catch (error) { + yield put(fetchCollectionForumPostReplyFailure(collection, error.message)) + } + } + function* handleCreateCollectionAssigneeForumPost(action: CreateCollectionAssigneeForumPostRequestAction) { const { collectionId, curation } = action.payload const collection: Collection = yield select(getCollection, collectionId) diff --git a/src/modules/forum/types.ts b/src/modules/forum/types.ts index 820b0aadc..cea64c3f4 100644 --- a/src/modules/forum/types.ts +++ b/src/modules/forum/types.ts @@ -6,3 +6,10 @@ export type ForumPost = { archetype?: string created_at?: string } + +export type ForumPostReply = { + topic_id: string + highest_post_number: number + show_read_indicator: boolean + last_read_post_number?: number +} diff --git a/src/modules/translation/languages/en.json b/src/modules/translation/languages/en.json index 47e7a4d84..c66fdb8b2 100644 --- a/src/modules/translation/languages/en.json +++ b/src/modules/translation/languages/en.json @@ -1070,8 +1070,8 @@ "results": "{count} {count, plural, one {result} other {results}}", "no_items": "No collections", "new_item": "New Item", - "new_collection": "New Collection", - "new_third_party_collection": "New Linked Wearables Collection", + "new_collection": "Create Collection", + "new_third_party_collection": "Create Linked Wearables Collection", "type": "Type", "items": "Items", "collections": "Collections", @@ -1209,7 +1209,8 @@ "collection_detail_page": { "new_item": "New Item", "mint_items": "Mint Items", - "on_sale": "Put for sale", + "on_sale": "On Sale", + "put_for_sale": "Put for sale", "set_on_sale_popup": "This will allow your collection to be sold in the Decentraland Marketplace.", "unset_on_sale_popup": "This collection is on sale in the Decentraland Marketplace.", "notice": "Cool! Now you can start working on your items. {editor_link} to open the editor or click the edit button on any item.", @@ -1222,6 +1223,7 @@ "add_items_subtitle_extensions": "Accepted files are ZIP, GLTF, GLB, PNG.", "start_adding_items": "Looking good! Start adding items to your new collection", "cant_remove": "You will not be able to add or remove items after publishing your collection.", + "can_mint": "Minting allows you to transfer NFTs directly to an address.", "cant_mint": "This collection is still under review, once that process ends you'll be able to mint items", "cant_push": "This collection is still under review, items can still be minted but will not reflect new changes until approval", "under_review": "Under Review", @@ -1442,11 +1444,16 @@ "content": "The item is currently different from the one that has been previously approved by the committee. {br}The version you are seeing now does not correspond to the one visible in the marketplace and in the game. {br}You can push the changes for the committee to review this new version, or you can click on the confirm button to reset the item to it's original state." }, "sell_collection_modal": { - "title": "Do you want to sell your items?", - "turn_on_description": "By turning this switch on your collection will be available for purchase in the Decentraland Marketplace.", - "turn_on": "Turn On", - "turn_off_description": "By turning this switch off your collection will not longer be available for purchase.", - "turn_off": "Turn Off", + "put_for_sale": { + "title": "Do you want to put this collection for sale?", + "description": "The collection will be available for purchase in the Decentraland Marketplace.", + "cta": "Put for Sale" + }, + "remove_from_marketplace": { + "title": "Do you want to remove this collection from the Marketplace?", + "description": "The collection will no longer be available for purchase in the Decentraland Marketplace.", + "cta": "Remove from Marketplace" + }, "unsynced_warning": "Some items have changes that have not been approved yet." }, "manage_collection_role_modal": { diff --git a/src/modules/translation/languages/es.json b/src/modules/translation/languages/es.json index 993da1811..aed87f739 100644 --- a/src/modules/translation/languages/es.json +++ b/src/modules/translation/languages/es.json @@ -1080,8 +1080,8 @@ "results": "{count} {count, plural, one {resultado} other {resultados}}", "no_items": "Sin colecciones", "new_item": "Nuevo item", - "new_collection": "Nueva colección", - "new_third_party_collection": "Nueva colección externa", + "new_collection": "Crear colección", + "new_third_party_collection": "Crear colección externa", "type": "Tipo", "items": "Items", "collections": "Colecciones", @@ -1218,18 +1218,20 @@ "new_item": "Nuevo Item", "mint_items": "Crear Items", "on_sale": "En venta", + "put_for_sale": "Poner en venta", "set_on_sale_popup": "Esto permitirá que tu colección sea vendida en el Mercado de Decentraland.", "unset_on_sale_popup": "Esta colección está en venta en el Mercado de Decentraland.", "notice": "Bien! Ahora puedes empezar a trabajar con tus items. {editor_link} para abrir el editor o edita cada item.", "preview": "Preview en el Editor", "publish": "Publicar Colección", - "remove_from_marketplace": "Remover del marketplace", + "remove_from_marketplace": "Remover del Marketplace", "add_item": "Agregar Item", "add_items_title": "Se ve bien!", "add_items_subtitle": "Ahora puede comenzar a agregar items a su colección", "add_items_subtitle_extensions": "Formatos aceptados: ZIP, GLTF, GLB, PNG.", "start_adding_items": "¡Se ve bien! Ahora puedes comenzar a agregar items a tu colección.", "cant_remove": "No podrás agregar o remover items de tu colección una vez que esté publicada.", + "can_mint": "Crear items le permite transferir NFTs directamente a una dirección.", "cant_mint": "Esta colección todavía no está verificada, cuando ese proceso termine, podrás crear items", "cant_push": "Esta colección aún está bajo revisión, los artículos aún se pueden acuñar pero no reflejarán nuevos cambios hasta su aprobación.", "push_changes": "Subir Cambios", @@ -1436,6 +1438,16 @@ "turn_on": "Activar", "turn_off_description": "Al desactivar esta opción tu colección ya no estará en venta.", "turn_off": "Desactivar", + "put_for_sale": { + "title": "¿Quieres poner en venta esta colección?", + "description": "La colección estará en venta en el mercado de Decentraland.", + "cta": "Poner en Venta" + }, + "remove_from_marketplace": { + "title": "¿Quieres remover esta colección del Marketplace?", + "description": "La colección ya no estará en venta en el mercado de Decentraland.", + "cta": "Remover del Marketplace" + }, "unsynced_warning": "Algunos items tienen cambios que aún no se han aprobado." }, "publish_third_party_collection_modal": { diff --git a/src/modules/translation/languages/zh.json b/src/modules/translation/languages/zh.json index 11884a3ff..6d86ec1ff 100644 --- a/src/modules/translation/languages/zh.json +++ b/src/modules/translation/languages/zh.json @@ -1061,8 +1061,8 @@ "results": "{count} {count,复数,一个{result}其他{results}}", "no_items": "没有收藏", "new_item": "项目", - "new_collection": "收藏", - "new_third_party_collection": "新的链接可穿戴设备系列", + "new_collection": "创建集合", + "new_third_party_collection": "创建链接的可穿戴设备集合", "type": "类型", "items": "模型", "collections": "收藏品", @@ -1199,6 +1199,7 @@ "new_item": "新物品", "mint_items": "薄荷项目", "on_sale": "特价中", + "put_for_sale": "投放出售", "set_on_sale_popup": "这将允许您的收藏在 Decentraland 市场上出售。", "unset_on_sale_popup": "该系列在 Decentraland 市场上有售。", "notice": "凉爽的! 现在,您可以开始处理项目了。 {editor_link}打开编辑器,或在任何项目上单击“编辑”按钮。", @@ -1211,6 +1212,7 @@ "add_items_subtitle_extensions": "接受的文件是 ZIP、GLTF、GLB、PNG。", "start_adding_items": "看起来不错! 现在您可以开始将项目添加到您的收藏中了。", "cant_remove": "集合发布后,您将无法将其删除或添加到集合中。", + "can_mint": "铸币允许您将 NFT 直接转移到一个地址。", "cant_mint": "该收藏集仍在审核中,一旦该过程结束,您将能够铸造商品", "cant_push": "此集合仍在审查中,项目仍然可以铸造,但不会反映进一步的变化,直到批准。", "push_changes": "上传更改", @@ -1419,6 +1421,16 @@ "turn_on": "打开", "turn_off_description": "关闭此开关后,您的收藏将不再可用。", "turn_off": "关掉", + "put_for_sale": { + "title": "您想出售此收藏吗?", + "description": "该系列将可在 Decentraland 市场上购买。", + "cta": "投放出售" + }, + "remove_from_marketplace": { + "title": "你想从市场中删除这个集合吗?", + "description": "该系列将不再可在 Decentraland 市场上购买。", + "cta": "从市场中删除" + }, "unsynced_warning": "有些项目有尚未批准的更改。" }, "collection_managers_modal": {},