diff --git a/functions/.env.civic-liker b/functions/.env.civic-liker index 7c0e2e116..c73d81dab 100644 --- a/functions/.env.civic-liker +++ b/functions/.env.civic-liker @@ -1,5 +1,5 @@ EXTERNAL_URL=https://liker.land -API_URL=https://liker.land/api +API_URL=https://liker.land LIKECOIN_API_BASE=https://api.like.co SENTRY_REPORT_URI=https://sentry.io/api/1423120/security/?sentry_key=01fb2a31c1bf4234bd8d36ed84732859 BACKUP_BUCKET=gs://liker-land-firestore-backup diff --git a/functions/.env.civic-liker-develop b/functions/.env.civic-liker-develop index 60cda57c4..7e5cd93aa 100644 --- a/functions/.env.civic-liker-develop +++ b/functions/.env.civic-liker-develop @@ -1,6 +1,6 @@ IS_TESTNET=TRUE EXTERNAL_URL=https://rinkeby.liker.land -API_URL=https://rinkeby.liker.land/api +API_URL=https://rinkeby.liker.land LIKECOIN_API_BASE=https://api.rinkeby.like.co SENTRY_REPORT_URI=https://sentry.io/api/1423136/security/?sentry_key=d2ce18926cf6477496214312f4a761ed BACKUP_BUCKET=gs://liker-land-develop-firestore-backup diff --git a/functions/modules/firebase.js b/functions/modules/firebase.js index feb4dac61..33ebc58e1 100644 --- a/functions/modules/firebase.js +++ b/functions/modules/firebase.js @@ -10,6 +10,9 @@ const getCollectionIfDefined = root => root ? database.collection(root) : null; const userCollection = getCollectionIfDefined(process.env.FIRESTORE_USER_ROOT); +const walletUserCollection = getCollectionIfDefined( + process.env.FIRESTORE_WALLET_USER_ROOT +); const nftMintSubscriptionCollection = getCollectionIfDefined( process.env.FIRESTORE_NFT_MINT_SUBSCRIPTION_ROOT ); @@ -18,5 +21,6 @@ module.exports = { db, FieldValue, userCollection, + walletUserCollection, nftMintSubscriptionCollection, }; diff --git a/src/components/Icon/Hide.vue b/src/components/Icon/Hide.vue new file mode 100644 index 000000000..85e528064 --- /dev/null +++ b/src/components/Icon/Hide.vue @@ -0,0 +1,17 @@ + + diff --git a/src/components/Icon/StartFilled.vue b/src/components/Icon/StartFilled.vue new file mode 100644 index 000000000..d17a78e3f --- /dev/null +++ b/src/components/Icon/StartFilled.vue @@ -0,0 +1,15 @@ + diff --git a/src/components/Icon/StartOutlined.vue b/src/components/Icon/StartOutlined.vue new file mode 100644 index 000000000..4742ab421 --- /dev/null +++ b/src/components/Icon/StartOutlined.vue @@ -0,0 +1,16 @@ + diff --git a/src/components/NFTFeatured/index.vue b/src/components/NFTFeatured/index.vue new file mode 100644 index 000000000..2ec0662ab --- /dev/null +++ b/src/components/NFTFeatured/index.vue @@ -0,0 +1,167 @@ + + + diff --git a/src/components/NFTPortfolio/Base.vue b/src/components/NFTPortfolio/Base.vue index 2a158d73d..9946b9a3c 100644 --- a/src/components/NFTPortfolio/Base.vue +++ b/src/components/NFTPortfolio/Base.vue @@ -3,7 +3,7 @@ :collected-count="collectedCount" :is-writing-nft="isWritingNFT" > - + @@ -79,6 +80,8 @@ diff --git a/src/components/NFTPortfolio/Item.vue b/src/components/NFTPortfolio/Item.vue index 022729ac3..59fcf2d41 100644 --- a/src/components/NFTPortfolio/Item.vue +++ b/src/components/NFTPortfolio/Item.vue @@ -18,9 +18,15 @@ :image-src="NFTImageUrl" :is-collecting="uiIsOpenCollectModal && isCollecting" :own-count="ownCount" + :display-state="nftDisplayState" @collect="handleClickCollect" @load-cover="handleCoverLoaded" /> + diff --git a/src/components/NFTPortfolio/MainView.vue b/src/components/NFTPortfolio/MainView.vue index b9c0b4b02..a0318889a 100644 --- a/src/components/NFTPortfolio/MainView.vue +++ b/src/components/NFTPortfolio/MainView.vue @@ -303,6 +303,9 @@ export default { case NFT_CLASS_LIST_SORTING.NFT_OWNED_COUNT: return this.$t('order_menu_collected'); + case NFT_CLASS_LIST_SORTING.DISPLAY_STATE: + return this.$t('order_menu_display_state'); + default: return ''; } diff --git a/src/config/sitemap.js b/src/config/sitemap.js index 25875d1f6..4aadf65a2 100644 --- a/src/config/sitemap.js +++ b/src/config/sitemap.js @@ -28,6 +28,7 @@ async function getSitemapRoutes() { getTopCollectors, ].map(url => axios.get(url).catch(err => { + // eslint-disable-next-line no-console console.error(err); return {}; }) diff --git a/src/constant/index.js b/src/constant/index.js index 79045d104..34eabb477 100644 --- a/src/constant/index.js +++ b/src/constant/index.js @@ -165,3 +165,9 @@ export const NFT_GEM_NAME = [ export const ROUGH_LIKE_TO_USD_PRICE = 0.01; export const LOGIN_MESSAGE = 'Login'; + +export const NFT_DISPLAY_STATE = { + FEATURED: 'featured', + HIDDEN: 'hidden', + DEFAULT: 'default', +}; diff --git a/src/locales/en.json b/src/locales/en.json index 592b69338..2100f18b3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -114,6 +114,8 @@ "nft_portfolio_page_label_other": "Other", "nft_portfolio_page_label_loading": "Loading", "nft_portfolio_page_label_loading_more": "Loading more", + "nft_portfolio_page_label_featured": "Featured", + "nft_portfolio_page_label_hidden": "Hidden", "nft_supply_section_title": "Item Supply", "nft_widget_button_collect": "Collect Now", "footer_nav_about_liker_land": "About Liker Land", @@ -125,6 +127,7 @@ "order_menu_price": "Price", "order_menu_time": "Time", "order_menu_collected": "Own", + "order_menu_display_state": "Featured", "order_menu_by":" By {order}", "tx_modal_quitAlert_content": "Transaction will be cancelled if network confirmations has not completed.\nThe paid gas fee cannot be refunded.", "tx_modal_quitAlert_confirm": "Cancel", diff --git a/src/mixins/auth.js b/src/mixins/auth.js deleted file mode 100644 index 6418ffda5..000000000 --- a/src/mixins/auth.js +++ /dev/null @@ -1,55 +0,0 @@ -import { mapActions, mapGetters } from 'vuex'; -import stringify from 'fast-json-stable-stringify'; -import { - LOGIN_MESSAGE, - LIKECOIN_CHAIN_ID, - LIKECOIN_CHAIN_MIN_DENOM, -} from '@/constant/index'; - -export default { - computed: { - ...mapGetters(['getAddress', 'getSigner']), - }, - methods: { - ...mapActions(['initIfNecessary']), - async signLogin() { - if (!this.getSigner) { - await this.initIfNecessary(); - } - const memo = [ - `${LOGIN_MESSAGE}:`, - JSON.stringify({ - ts: Date.now(), - address: this.getAddress, - }), - ].join(' '); - const payload = { - chain_id: LIKECOIN_CHAIN_ID, - memo, - msgs: [], - fee: { - gas: '0', - amount: [{ denom: LIKECOIN_CHAIN_MIN_DENOM, amount: '0' }], - }, - sequence: '0', - account_number: '0', - }; - try { - const { - signed: message, - signature: { signature, pub_key: publicKey }, - } = await this.getSigner.sign(this.getAddress, payload); - const data = { - signature, - publicKey: publicKey.value, - message: stringify(message), - from: this.getAddress, - }; - await this.$api.post('/api/v2/users/login', data); - } catch (error) { - return error; - } - return undefined; - }, - }, -}; diff --git a/src/mixins/nft.js b/src/mixins/nft.js index 17a6f8ff5..a03b046c1 100644 --- a/src/mixins/nft.js +++ b/src/mixins/nft.js @@ -8,6 +8,7 @@ import { TX_STATUS, LIKECOIN_NFT_API_WALLET, LIKECOIN_NFT_COLLECT_WITHOUT_WALLET_ITEMS_BY_CREATORS, + NFT_DISPLAY_STATE, } from '~/constant'; import { @@ -93,6 +94,8 @@ export default { computed: { ...mapGetters([ 'getUserInfoByAddress', + 'getNFTClassFeaturedSetByAddress', + 'getNFTClassHiddenSetByAddress', 'getNFTClassPurchaseInfoById', 'getNFTClassMetadataById', 'getNFTClassOwnerInfoById', @@ -214,7 +217,7 @@ export default { // Collector Info nftCollectorWalletAddress() { - if (!this.nftId) return undefined; + if (!this.nftId) return ''; return Object.keys(this.collectorMap).find(collector => { const nftIdList = this.collectorMap[collector]; return nftIdList.find(nftId => this.nftId === nftId); @@ -300,11 +303,29 @@ export default { getWalletIdentityType() { return wallet => (wallet === this.iscnOwner ? 'creator' : 'collector'); }, + nftDisplayState() { + // should use the address in URL as the subject address when browsing other's profile + const subjectAddress = + this.$route.name === 'id' ? this.$route.params.id : this.getAddress; + if ( + this.getNFTClassFeaturedSetByAddress(subjectAddress)?.has(this.classId) + ) { + return NFT_DISPLAY_STATE.FEATURED; + } + if ( + this.getNFTClassHiddenSetByAddress(subjectAddress)?.has(this.classId) + ) { + return NFT_DISPLAY_STATE.HIDDEN; + } + return NFT_DISPLAY_STATE.DEFAULT; + }, }, watch: { getAddress(newAddress) { if (newAddress) { this.fetchUserCollectedCount(); + this.fetchUserNFTListFeatured(); + this.fetchUserNFTListHidden(); } }, uiTxNFTStatus(status) { @@ -353,6 +374,8 @@ export default { 'uiSetTxError', 'walletFetchLIKEBalance', 'fetchNFTListByAddress', + 'fetchNFTListFeaturedByAddress', + 'fetchNFTListHiddenByAddress', ]), async fetchISCNMetadata() { if (!this.iscnId) return; @@ -492,6 +515,12 @@ export default { async fetchUserCollectedCount() { await this.updateUserCollectedCount(this.classId, this.getAddress); }, + async fetchUserNFTListFeatured() { + await this.fetchNFTListFeaturedByAddress(this.getAddress); + }, + async fetchUserNFTListHidden() { + await this.fetchNFTListHiddenByAddress(this.getAddress); + }, async collectNFT() { try { const purchaseEventParams = { diff --git a/src/mixins/portfolio.js b/src/mixins/portfolio.js index 2404c8175..ace51200d 100644 --- a/src/mixins/portfolio.js +++ b/src/mixins/portfolio.js @@ -23,7 +23,9 @@ const NFT_INFO_FETCH_CONCURRENT_REQUEST_MAX = 10; const throttleNFTInfoFetch = throat(NFT_INFO_FETCH_CONCURRENT_REQUEST_MAX); -export default { +export const createPorfolioMixin = ({ + shouldApplyDisplayState = true, +} = {}) => ({ tabOptions, mixins: [clipboardMixin, userInfoMixin], data() { @@ -48,10 +50,16 @@ export default { 'getNFTListMapByAddress', 'getNFTClassMetadataById', ]), + isDashboardPage() { + return this.$route.name === 'dashboard'; + }, currentTab() { const { tab } = this.$route.query; return tabOptions[tab] ? tab : DEFAULT_TAB; }, + isCurrentTabCollected() { + return this.currentTab === tabOptions.collected; + }, isCurrentTabCreated() { return this.currentTab === tabOptions.created; }, @@ -105,13 +113,16 @@ export default { collectorWallet: this.wallet, sorting: this.nftClassListOfCollectedSorting, order: this.nftClassListOfCollectedSortingOrder, + shouldApplyDisplayState, }); }, nftClassListOfCreatedInOrder() { return this.getNFTClassIdListSorterForCreated({ list: this.nftClassListOfCreatedExcludedOther, + collectorWallet: this.wallet, sorting: this.nftClassListOfCreatedSorting, order: this.nftClassListOfCreatedSortingOrder, + shouldApplyDisplayState, }); }, nftClassListOfOtherInOrder() { @@ -120,6 +131,7 @@ export default { collectorWallet: this.wallet, sorting: this.nftClassListOfOtherSorting, order: this.nftClassListOfOtherSortingOrder, + shouldApplyDisplayState, }); }, currentNFTClassListShowCount() { @@ -183,9 +195,11 @@ export default { } }, currentNFTClassSortingOptionList() { + const options = []; + switch (this.currentTab) { case tabOptions.collected: - return [ + options.push( { sorting: NFT_CLASS_LIST_SORTING.PRICE, order: NFT_CLASS_LIST_SORTING_ORDER.DESC, @@ -205,11 +219,12 @@ export default { { sorting: NFT_CLASS_LIST_SORTING.NFT_OWNED_COUNT, order: NFT_CLASS_LIST_SORTING_ORDER.DESC, - }, - ]; + } + ); + break; case tabOptions.created: - return [ + options.push( { sorting: NFT_CLASS_LIST_SORTING.PRICE, order: NFT_CLASS_LIST_SORTING_ORDER.DESC, @@ -225,11 +240,12 @@ export default { { sorting: NFT_CLASS_LIST_SORTING.ISCN_TIMESTAMP, order: NFT_CLASS_LIST_SORTING_ORDER.ASC, - }, - ]; + } + ); + break; case tabOptions.other: - return [ + options.push( { sorting: NFT_CLASS_LIST_SORTING.ISCN_TIMESTAMP, order: NFT_CLASS_LIST_SORTING_ORDER.DESC, @@ -237,12 +253,21 @@ export default { { sorting: NFT_CLASS_LIST_SORTING.ISCN_TIMESTAMP, order: NFT_CLASS_LIST_SORTING_ORDER.ASC, - }, - ]; + } + ); + break; default: - return []; } + + if (!shouldApplyDisplayState) { + options.push({ + sorting: NFT_CLASS_LIST_SORTING.DISPLAY_STATE, + order: NFT_CLASS_LIST_SORTING_ORDER.DESC, + }); + } + + return options; }, }, watch: { @@ -273,6 +298,8 @@ export default { 'fetchNFTClassMetadata', 'fetchNFTPurchaseInfo', 'fetchNFTOwners', + 'fetchNFTListFeaturedByAddress', + 'fetchNFTListHiddenByAddress', ]), updatePortfolioGrid() { const { portfolioMainView } = this.$refs; @@ -315,7 +342,11 @@ export default { } }, async loadNFTListByAddress(address) { - const fetchPromise = this.fetchNFTListByAddress(address); + const fetchPromise = Promise.all([ + this.fetchNFTListByAddress(address), + this.fetchNFTListFeaturedByAddress(address), + this.fetchNFTListHiddenByAddress(address), + ]); if (!this.getNFTListMapByAddress(address)) { this.isLoading = true; await fetchPromise; @@ -387,4 +418,6 @@ export default { } }, }, -}; +}); + +export default createPorfolioMixin(); diff --git a/src/pages/_id/index.vue b/src/pages/_id/index.vue index 8698f6a83..0f2247cb2 100644 --- a/src/pages/_id/index.vue +++ b/src/pages/_id/index.vue @@ -166,14 +166,34 @@ export default { isLoading(isLoading) { if (!isLoading) { if (this.$route.hash === this.creatorFollowSectionHash) { + if (!this.isCurrentTabCreated) { + this.changeTab(tabOptions.created); + } this.$nextTick(this.scrollToCreatorFollowSection); } else if ( - !this.isCurrentTabCreated && - // NOTE: Seems computed property `this.nftClassListOfCollected` is not reflecting the actual state - !this.nftClassListMap?.collected.length + // If collected tab is empty + this.isCurrentTabCollected && + !this.nftClassListOfCollectedExcludedOther.length ) { - // Go to created tab if collected tab is empty - this.changeTab(tabOptions.created); + if (this.nftClassListOfOther.length) { + // Go to other tab if not empty + this.changeTab(tabOptions.other); + } else { + // Go to created tab if other tab is empty + this.changeTab(tabOptions.created); + } + } else if ( + // If other tab is empty + this.isCurrentTabOther && + !this.nftClassListOfOther.length + ) { + if (this.nftClassListOfCollectedExcludedOther.length) { + // Go to collected tab if not empty + this.changeTab(tabOptions.other); + } else { + // Go to created tab if collected tab is empty + this.changeTab(tabOptions.created); + } } } }, diff --git a/src/pages/dashboard/index.vue b/src/pages/dashboard/index.vue index 42ba65f64..1d1ecdb34 100644 --- a/src/pages/dashboard/index.vue +++ b/src/pages/dashboard/index.vue @@ -70,14 +70,16 @@ import { mapActions } from 'vuex'; import { logTrackerEvent } from '~/util/EventLogger'; +import { createPorfolioMixin, tabOptions } from '~/mixins/portfolio'; import walletMixin from '~/mixins/wallet'; -import portfolioMixin, { tabOptions } from '~/mixins/portfolio'; -import authMixin from '~/mixins/auth'; export default { name: 'MyDashboardPage', layout: 'default', - mixins: [walletMixin, portfolioMixin, authMixin], + mixins: [ + walletMixin, + createPorfolioMixin({ shouldApplyDisplayState: false }), + ], head() { const title = this.$t('dashboard_title'); const description = this.$t('dashboard_description'); @@ -165,9 +167,6 @@ export default { } this.changeTab(tab); }, - async handleSignLogin() { - await this.signLogin(); - }, goMyPortfolio() { logTrackerEvent(this, 'MyDashboard', 'GoToMyPortfolio', this.wallet, 1); this.$router.push({ diff --git a/src/pages/nft/class/_classId/_nftId.vue b/src/pages/nft/class/_classId/_nftId.vue index cbb36c290..6759d3a05 100644 --- a/src/pages/nft/class/_classId/_nftId.vue +++ b/src/pages/nft/class/_classId/_nftId.vue @@ -114,6 +114,11 @@ @collect="handleCollectFromPreviewSection" @view-content="handleViewContent" /> + { }); return; } catch (error) { + // eslint-disable-next-line no-console console.error(error); next(err); } diff --git a/src/server/api/routes/users/v2/nfts/featured.js b/src/server/api/routes/users/v2/nfts/featured.js index 337f910a4..8556a69ac 100644 --- a/src/server/api/routes/users/v2/nfts/featured.js +++ b/src/server/api/routes/users/v2/nfts/featured.js @@ -20,8 +20,8 @@ router.get('/:wallet/nfts/featured', async (req, res, next) => { res.status(400).send('INVALID_ADDRESS'); return; } - const userDoc = await walletUserCollection(user).get(); - const { featuredNFTClassIds = [] } = userDoc.data(); + const userDoc = await walletUserCollection.doc(user).get(); + const { featuredNFTClassIds = [] } = userDoc.data() || {}; res.json({ featured: featuredNFTClassIds }); } catch (err) { handleRestfulError(req, res, next, err); diff --git a/src/server/api/routes/users/v2/nfts/hidden.js b/src/server/api/routes/users/v2/nfts/hidden.js index 73d56366c..05dab0b5e 100644 --- a/src/server/api/routes/users/v2/nfts/hidden.js +++ b/src/server/api/routes/users/v2/nfts/hidden.js @@ -20,8 +20,8 @@ router.get('/:wallet/nfts/hidden', async (req, res, next) => { res.status(400).send('INVALID_ADDRESS'); return; } - const userDoc = await walletUserCollection(user).get(); - const { hiddenNFTClassIds = [] } = userDoc.data(); + const userDoc = await walletUserCollection.doc(user).get(); + const { hiddenNFTClassIds = [] } = userDoc.data() || {}; res.json({ hidden: hiddenNFTClassIds }); } catch (err) { handleRestfulError(req, res, next, err); diff --git a/src/store/index.js b/src/store/index.js index e13d5799d..05a1634bf 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -21,8 +21,8 @@ const createStore = () => } try { if (req.cookies && req.cookies[AUTH_COOKIE_NAME]) { - const userInfo = await this.$api.$get(api.getLoginStatus()); - commit(types.USER_SET_USER_INFO, userInfo); + const { user } = await this.$api.$get(api.getUserV2Self()); + commit(types.WALLET_SET_LOGIN_ADDRESS, user); } } catch (err) { if (err.response) { diff --git a/src/store/modules/nft.js b/src/store/modules/nft.js index 8be283a5a..8dffcd60b 100644 --- a/src/store/modules/nft.js +++ b/src/store/modules/nft.js @@ -19,6 +19,8 @@ const state = () => ({ metadataByNFTClassAndNFTIdMap: {}, ownerInfoByClassIdMap: {}, userClassIdListMap: {}, + userNFTClassFeaturedSetMap: {}, + userNFTClassHiddenSetMap: {}, userLastCollectedTimestampMap: {}, }); @@ -42,6 +44,18 @@ const mutations = { [TYPES.NFT_SET_USER_CLASSID_LIST_MAP](state, { address, nfts }) { Vue.set(state.userClassIdListMap, address, nfts); }, + [TYPES.NFT_SET_USER_NFT_CLASS_FEATURED_SET_MAP]( + state, + { address, classIdSet } + ) { + Vue.set(state.userNFTClassFeaturedSetMap, address, classIdSet); + }, + [TYPES.NFT_SET_USER_NFT_CLASS_HIDDEN_SET_MAP]( + state, + { address, classIdSet } + ) { + Vue.set(state.userNFTClassHiddenSetMap, address, classIdSet); + }, [TYPES.NFT_SET_USER_LAST_COLLECTED_TIMESTAMP_MAP]( state, { address, timestampMap } @@ -66,6 +80,33 @@ function compareIsWritingNFT(getters, classIdA, classIdB) { return 0; } +function compareNFTByFeatured(getters, address, classIdA, classIdB) { + const featuredSet = getters.getNFTClassFeaturedSetByAddress(address); + if (!featuredSet) return 0; + const aIsFeatured = featuredSet.has(classIdA); + const bIsFeatured = featuredSet.has(classIdB); + if (aIsFeatured && !bIsFeatured) return -1; + if (!aIsFeatured && bIsFeatured) return 1; + return 0; +} + +function compareNFTByHidden(getters, address, classIdA, classIdB) { + const hiddenSet = getters.getNFTClassHiddenSetByAddress(address); + if (!hiddenSet) return 0; + const aIsHidden = hiddenSet.has(classIdA); + const bIsHidden = hiddenSet.has(classIdB); + if (aIsHidden && !bIsHidden) return 1; + if (!aIsHidden && bIsHidden) return -1; + return 0; +} + +function compareNFTDisplayState(getters, address, classIdA, classIdB) { + const result = compareNFTByFeatured(getters, address, classIdA, classIdB); + return result === 0 + ? compareNFTByHidden(getters, address, classIdA, classIdB) + : result; +} + function compareNumber(X, Y, order) { if (Y === undefined) return -1; // keep X in front of Y if (X === undefined) return 1; // move Y in front of X @@ -81,6 +122,10 @@ function compareNumber(X, Y, order) { const getters = { NFTClassIdList: state => state.userClassIdListMap, getNFTListMapByAddress: state => address => state.userClassIdListMap[address], + getNFTClassFeaturedSetByAddress: state => address => + state.userNFTClassFeaturedSetMap[address], + getNFTClassHiddenSetByAddress: state => address => + state.userNFTClassHiddenSetMap[address], getNFTClassPurchaseInfoById: state => id => state.purchaseInfoByClassIdMap[id], getNFTClassMetadataById: state => id => state.metadataByClassIdMap[id], @@ -96,13 +141,35 @@ const getters = { state.metadataByNFTClassAndNFTIdMap[`${classId}-${nftId}`], getUserLastCollectedTimestampByAddress: state => address => state.userLastCollectedTimestampMap[address], + filterNFTClassListWithState: state => (nfts, collectorWallet) => + nfts.filter( + ({ classId }) => + !state.userNFTClassHiddenSetMap[collectorWallet]?.has(classId) + ), getNFTClassIdListSorterForCreated: (_, getters) => ({ list, + collectorWallet: collector, sorting, order = NFT_CLASS_LIST_SORTING_ORDER.DESC, + shouldApplyDisplayState, }) => { - const sorted = [...list].sort((nA, nB) => { + const filtered = shouldApplyDisplayState + ? getters.filterNFTClassListWithState(list, collector) + : [...list]; + const sorted = filtered.sort((nA, nB) => { const [{ classId: a }, { classId: b }] = [nA, nB]; + if ( + shouldApplyDisplayState || + sorting === NFT_CLASS_LIST_SORTING.DISPLAY_STATE + ) { + const isFeaturedCompareResult = compareNFTDisplayState( + getters, + collector, + a, + b + ); + if (isFeaturedCompareResult !== 0) return isFeaturedCompareResult; + } const isWritingNFTCompareResult = compareIsWritingNFT(getters, a, b); if (isWritingNFTCompareResult !== 0) return isWritingNFTCompareResult; let X; @@ -123,15 +190,30 @@ const getters = { }); return sorted; }, - getNFTClassListSorterForCollected: (_, getters) => ({ list, collectorWallet: collector, sorting, order = NFT_CLASS_LIST_SORTING_ORDER.DESC, + shouldApplyDisplayState, }) => { - const sorted = [...list].sort((nA, nB) => { + const filtered = shouldApplyDisplayState + ? getters.filterNFTClassListWithState(list, collector) + : [...list]; + const sorted = filtered.sort((nA, nB) => { const [{ classId: a }, { classId: b }] = [nA, nB]; + if ( + shouldApplyDisplayState || + sorting === NFT_CLASS_LIST_SORTING.DISPLAY_STATE + ) { + const isFeaturedCompareResult = compareNFTDisplayState( + getters, + collector, + a, + b + ); + if (isFeaturedCompareResult !== 0) return isFeaturedCompareResult; + } const isWritingNFTCompareResult = compareIsWritingNFT(getters, a, b); if (isWritingNFTCompareResult !== 0) return isWritingNFTCompareResult; let X; @@ -295,6 +377,56 @@ const actions = { timestampMap, }); }, + async fetchNFTListFeaturedByAddress({ commit }, address) { + const { data } = await this.$api.get(api.formatFeaturedNFTUrl(address)); + commit(TYPES.NFT_SET_USER_NFT_CLASS_FEATURED_SET_MAP, { + address, + classIdSet: new Set(data.featured), + }); + }, + async fetchNFTListHiddenByAddress({ commit }, address) { + const { data } = await this.$api.get(api.formatHiddenNFTUrl(address)); + commit(TYPES.NFT_SET_USER_NFT_CLASS_HIDDEN_SET_MAP, { + address, + classIdSet: new Set(data.hidden), + }); + }, + async addNFTFeatured({ state, commit }, { address, classId }) { + const classIdSet = state.userNFTClassFeaturedSetMap[address]; + classIdSet.add(classId); + commit(TYPES.NFT_SET_USER_NFT_CLASS_FEATURED_SET_MAP, { + address, + classIdSet: new Set(classIdSet), // clone to trigger reactivity + }); + await this.$api.post(api.formatFeaturedNFTUrl(address), { classId }); + }, + async addNFTHidden({ state, commit }, { address, classId }) { + const classIdSet = state.userNFTClassHiddenSetMap[address]; + classIdSet.add(classId); + commit(TYPES.NFT_SET_USER_NFT_CLASS_HIDDEN_SET_MAP, { + address, + classIdSet: new Set(classIdSet), // clone to trigger reactivity + }); + await this.$api.post(api.formatHiddenNFTUrl(address), { classId }); + }, + async removeNFTFeatured({ state, commit }, { address, classId }) { + const classIdSet = state.userNFTClassFeaturedSetMap[address]; + classIdSet.delete(classId); + commit(TYPES.NFT_SET_USER_NFT_CLASS_FEATURED_SET_MAP, { + address, + classIdSet: new Set(classIdSet), // clone to trigger reactivity + }); + await this.$api.delete(`${api.formatFeaturedNFTUrl(address)}/${classId}`); + }, + async removeNFTHidden({ state, commit }, { address, classId }) { + const classIdSet = state.userNFTClassHiddenSetMap[address]; + classIdSet.delete(classId); + commit(TYPES.NFT_SET_USER_NFT_CLASS_HIDDEN_SET_MAP, { + address, + classIdSet: new Set(classIdSet), // clone to trigger reactivity + }); + await this.$api.delete(`${api.formatHiddenNFTUrl(address)}/${classId}`); + }, }; export default { diff --git a/src/store/modules/wallet.js b/src/store/modules/wallet.js index 60b660175..32e2dd111 100644 --- a/src/store/modules/wallet.js +++ b/src/store/modules/wallet.js @@ -1,9 +1,14 @@ /* eslint no-param-reassign: "off" */ - +import stringify from 'fast-json-stable-stringify'; +import { + LOGIN_MESSAGE, + LIKECOIN_CHAIN_ID, + LIKECOIN_CHAIN_MIN_DENOM, +} from '@/constant/index'; import { LIKECOIN_WALLET_CONNECTOR_CONFIG } from '@/constant/network'; import * as types from '@/store/mutation-types'; import { getAccountBalance } from '~/util/nft'; -import { getUserInfoMinByAddress } from '~/util/api'; +import { getUserInfoMinByAddress, postUserV2Login } from '~/util/api'; import { setLoggerUser } from '~/util/EventLogger'; import { WALLET_SET_IS_DEBUG, @@ -22,6 +27,7 @@ const state = () => ({ isDebug: false, address: '', signer: null, + loginAddress: '', connector: null, likerInfo: null, isInited: null, @@ -40,6 +46,9 @@ const mutations = { [WALLET_SET_SIGNER](state, signer) { state.signer = signer; }, + [types.WALLET_SET_LOGIN_ADDRESS](state, loginAddress) { + state.loginAddress = loginAddress; + }, [WALLET_SET_METHOD_TYPE](state, method) { state.methodType = method; }, @@ -60,6 +69,8 @@ const mutations = { const getters = { getAddress: state => state.address, getSigner: state => state.signer, + loginAddress: state => state.loginAddress, + walletHasLoggedIn: state => state.address === state.loginAddress, getConnector: state => state.connector, getLikerInfo: state => state.likerInfo, walletMethodType: state => state.methodType, @@ -129,6 +140,7 @@ const actions = { commit(types.WALLET_SET_SIGNER, null); commit(types.WALLET_SET_CONNECTOR, null); commit(types.WALLET_SET_LIKERINFO, null); + commit(types.WALLET_SET_LOGIN_ADDRESS, ''); }, async restoreSession({ dispatch }) { @@ -168,6 +180,47 @@ const actions = { commit(types.WALLET_SET_LIKE_BALANCE_FETCH_PROMISE, undefined); } }, + async signLogin({ state, commit, dispatch }) { + if (!state.signer) { + await dispatch('initIfNecessary'); + } + const { address } = state; + const memo = [ + `${LOGIN_MESSAGE}:`, + JSON.stringify({ + ts: Date.now(), + address, + }), + ].join(' '); + const payload = { + chain_id: LIKECOIN_CHAIN_ID, + memo, + msgs: [], + fee: { + gas: '0', + amount: [{ denom: LIKECOIN_CHAIN_MIN_DENOM, amount: '0' }], + }, + sequence: '0', + account_number: '0', + }; + try { + const { + signed: message, + signature: { signature, pub_key: publicKey }, + } = await state.signer.sign(address, payload); + const data = { + signature, + publicKey: publicKey.value, + message: stringify(message), + from: address, + }; + await this.$api.post(postUserV2Login(), data); + commit(types.WALLET_SET_LOGIN_ADDRESS, address); + } catch (error) { + commit(types.WALLET_SET_LOGIN_ADDRESS, null); + throw error; + } + }, }; export default { diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js index 1b91870e3..95c7dac25 100644 --- a/src/store/mutation-types.js +++ b/src/store/mutation-types.js @@ -4,6 +4,10 @@ export const NFT_SET_NFT_CLASS_METADATA = 'NFT_SET_NFT_CLASS_METADATA'; export const NFT_SET_NFT_CLASS_OWNER_INFO = 'NFT_SET_NFT_CLASS_OWNER_INFO'; export const NFT_SET_NFT_METADATA = 'NFT_SET_NFT_METADATA'; export const NFT_SET_USER_CLASSID_LIST_MAP = 'NFT_SET_USER_CLASSID_LIST_MAP'; +export const NFT_SET_USER_NFT_CLASS_FEATURED_SET_MAP = + 'NFT_SET_USER_NFT_CLASS_FEATURED_SET_MAP'; +export const NFT_SET_USER_NFT_CLASS_HIDDEN_SET_MAP = + 'NFT_SET_USER_HIDDEN_CLASS_ID_LIST_MAP'; export const NFT_SET_USER_LAST_COLLECTED_TIMESTAMP_MAP = 'NFT_SET_USER_LAST_COLLECTED_TIMESTAMP_MAP'; @@ -33,6 +37,7 @@ export const USER_UPDATE_USER_INFO = 'USER_UPDATE_USER_INFO'; export const WALLET_SET_IS_DEBUG = 'WALLET_SET_IS_DEBUG'; export const WALLET_SET_ADDRESS = 'WALLET_SET_ADDRESS'; export const WALLET_SET_SIGNER = 'WALLET_SET_SIGNER'; +export const WALLET_SET_LOGIN_ADDRESS = 'WALLET_SET_LOGIN_ADDRESS'; export const WALLET_SET_METHOD_TYPE = 'WALLET_SET_METHOD_TYPE'; export const WALLET_SET_CONNECTOR = 'WALLET_SET_CONNECTOR'; export const WALLET_SET_LIKERINFO = 'WALLET_SET_LIKERINFO'; diff --git a/src/util/api/index.js b/src/util/api/index.js index 211221c57..f3c0f402e 100644 --- a/src/util/api/index.js +++ b/src/util/api/index.js @@ -260,3 +260,12 @@ export const nftMintSubscriptionAPI = ({ id, email, wallet }) => { id ? `/${id}` : '' }?${querystring.stringify(qsPayload)}`; }; + +export const getUserV2Self = () => '/api/v2/users/self'; +export const postUserV2Login = () => '/api/v2/users/login'; + +export const formatFeaturedNFTUrl = wallet => + `/api/v2/users/${wallet}/nfts/featured`; + +export const formatHiddenNFTUrl = wallet => + `/api/v2/users/${wallet}/nfts/hidden`; diff --git a/src/util/nft.js b/src/util/nft.js index 47994cd02..6a66c1bfc 100644 --- a/src/util/nft.js +++ b/src/util/nft.js @@ -22,6 +22,7 @@ export const NFT_CLASS_LIST_SORTING = { ISCN_TIMESTAMP: 'ISCN_TIMESTAMP', LAST_COLLECTED_NFT: 'LAST_COLLECTED_NFT', NFT_OWNED_COUNT: 'NFT_OWNED_COUNT', + DISPLAY_STATE: 'DISPLAY_STATE', }; export const NFT_CLASS_LIST_SORTING_ORDER = {