diff --git a/app/ResolveRoute.js b/app/ResolveRoute.js index d057967..1a8f2ea 100644 --- a/app/ResolveRoute.js +++ b/app/ResolveRoute.js @@ -112,5 +112,8 @@ export default function resolveRoute(path) if (match) { return {page: 'NFTMarketPage', params: match.slice(1)} } + if (path === '/all-nft') { + return {page: 'AllNFTPage'} + } return {page: 'NotFound'}; } diff --git a/app/RootRoute.js b/app/RootRoute.js index 77d8721..359aeed 100644 --- a/app/RootRoute.js +++ b/app/RootRoute.js @@ -39,6 +39,8 @@ export default { cb(null, [require('@pages/nft/NFTTokenPage')]); } else if (route.page === 'NFTMarketPage') { cb(null, [require('@pages/nft/NFTMarketPage')]); + } else if (route.page === 'AllNFTPage') { + cb(null, [require('@pages/nft/AllNFTPage')]) } else if (route.page === 'Market') { cb(null, [require('@pages/MarketLoader')]); } else if (route.page === 'Rating') { diff --git a/app/components/App.jsx b/app/components/App.jsx index 6fece5d..f28eded 100644 --- a/app/components/App.jsx +++ b/app/components/App.jsx @@ -38,7 +38,7 @@ const GlobalStyle = createGlobalStyle` } `; -const availableDomains = [ +export const availableDomains = [ 'golos.id', 'golos.in', 'golos.today', diff --git a/app/components/all.scss b/app/components/all.scss index fb72464..e48581c 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -85,6 +85,7 @@ @import "./pages/Rating"; @import "./pages/UserProfile"; @import "./pages/Witnesses"; +@import "./pages/nft/AllNFTPage"; @import "./pages/nft/NFTCollectionPage"; @import "./pages/nft/NFTMarketPage"; @import "./pages/nft/NFTTokenPage"; diff --git a/app/components/elements/Memo.jsx b/app/components/elements/Memo.jsx index db8d34e..2d4446e 100644 --- a/app/components/elements/Memo.jsx +++ b/app/components/elements/Memo.jsx @@ -5,6 +5,7 @@ import tt from 'counterpart'; import { memo, } from 'golos-lib-js'; import { Link, } from 'react-router'; +import { availableDomains } from 'app/components/App' import links from 'app/utils/Links' import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' import { validate_account_name, } from 'app/utils/ChainValidation' @@ -52,7 +53,7 @@ class Memo extends React.Component { for (let section of text.split(' ')) { if (section.trim().length === 0) continue const matchUserName = section.match(/(^|\s)(@[a-z][-\.a-z\d]+[a-z\d])/i) - const matchLink = section.match(links.local) + let insertPlain = true if (matchUserName) { const user2 = matchUserName[0].trim().substring(1) const userLower = user2.toLowerCase() @@ -60,11 +61,20 @@ class Memo extends React.Component { valid ? sections.push({`@${user2}`} ) : sections.push({`@${user2}`}) + insertPlain = false + } else if (section.match(links.any)) { + let hostname + try { + hostname = new URL(section.trim()).hostname + } catch (err) { + console.error(err) + } + if (availableDomains.includes(hostname)) { + sections.push({section} ) + insertPlain = false + } } - else if (matchLink) { - sections.push({section} ) - } - else { + if (insertPlain) { sections.push({section} ) } } diff --git a/app/components/elements/nft/NFTMarketCollections.jsx b/app/components/elements/nft/NFTMarketCollections.jsx index 0da0f8c..2941c77 100644 --- a/app/components/elements/nft/NFTMarketCollections.jsx +++ b/app/components/elements/nft/NFTMarketCollections.jsx @@ -4,25 +4,23 @@ import tt from 'counterpart' import Icon from 'app/components/elements/Icon' import NFTSmallIcon from 'app/components/elements/nft/NFTSmallIcon' import PagedDropdownMenu from 'app/components/elements/PagedDropdownMenu' +import { NFTImageStub } from 'app/utils/NFTUtils' class NFTMarketCollections extends React.Component { render() { let { nft_market_collections, selected } = this.props - const nft_colls = nft_market_collections ? nft_market_collections.toJS() : [] + const nft_colls = nft_market_collections ? nft_market_collections.toJS().data : [] const colls = [] colls.push({ key: '_all', link: '/nft', - label: - - {tt('nft_market_page_jsx.all_collections')} - , - value: tt('nft_market_page_jsx.all_collections') + value: tt('nft_market_collections_jsx.all_collections') }) + let i = 0 for (const nft_coll of nft_colls) { colls.push({ - key: nft_coll.name, + key: i, link: '/nft/' + nft_coll.name, label: @@ -30,12 +28,29 @@ class NFTMarketCollections extends React.Component { , value: nft_coll.name }) + ++i } - selected = selected || tt('nft_market_page_jsx.all_collections') + selected = selected || tt('nft_market_collections_jsx.all_collections') return item} + renderItem={item => { + const coll = nft_colls[item.key] + const collImg = (coll && coll.image) || NFTImageStub() + const collName = coll ? coll.name : tt('nft_market_collections_jsx.all_collections') + + return { + ...item, + label: + + {collName} + , + addon: (coll && coll.sell_order_count) ? + + {coll.sell_order_count} + : null, + } + }} selected={selected} perPage={20}> {selected} diff --git a/app/components/elements/nft/NFTMarketCollections.scss b/app/components/elements/nft/NFTMarketCollections.scss index 7a0b01a..5e3bbd6 100644 --- a/app/components/elements/nft/NFTMarketCollections.scss +++ b/app/components/elements/nft/NFTMarketCollections.scss @@ -1,4 +1,8 @@ .NFTMarketCollections { + margin-top: 6px; + margin-left: 10px; + vertical-align: top; + .NFTSmallIcon { margin-top: 0.25rem; margin-right: 0.25rem; diff --git a/app/components/elements/nft/NFTTokenItem.jsx b/app/components/elements/nft/NFTTokenItem.jsx index 9033f88..d3c1782 100644 --- a/app/components/elements/nft/NFTTokenItem.jsx +++ b/app/components/elements/nft/NFTTokenItem.jsx @@ -99,7 +99,9 @@ class NFTTokenItem extends Component { } last_price = {imageUrl && {''}} - {price.amountFloat} + 9 ? '90%' : '100%' }}> + {price.amountFloat} + } diff --git a/app/components/modules/Header.jsx b/app/components/modules/Header.jsx index 42e8fd4..c810a1d 100644 --- a/app/components/modules/Header.jsx +++ b/app/components/modules/Header.jsx @@ -116,6 +116,8 @@ class Header extends React.Component { page_title = tt('header_jsx.nft_token') } else if (route.page === `NFTMarketPage`){ page_title = tt('header_jsx.nft_market') + } else if (route.page === `AllNFTPage`){ + page_title = tt('all_nft_page_jsx.title') } else { page_name = ''; //page_title = route.page.replace( /([a-z])([A-Z])/g, '$1 $2' ).toLowerCase(); } diff --git a/app/components/modules/nft/NFTTokenSell.jsx b/app/components/modules/nft/NFTTokenSell.jsx index 9fd9e6b..e056670 100644 --- a/app/components/modules/nft/NFTTokenSell.jsx +++ b/app/components/modules/nft/NFTTokenSell.jsx @@ -3,6 +3,7 @@ import tt from 'counterpart' import { connect, } from 'react-redux' import CloseButton from 'react-foundation-components/lib/global/close-button' import { Formik, Form, Field, ErrorMessage, } from 'formik' +import { api } from 'golos-lib-js' import { Asset, AssetEditor } from 'golos-lib-js/lib/utils' import AmountField from 'app/components/elements/forms/AmountField' @@ -19,6 +20,20 @@ class NFTTokenSell extends Component { } } + async componentDidMount() { + try { + const assets = await api.getAssetsAsync('', [], '', 5000, 'by_symbol_name') + for (const asset of assets) { + asset.supply = Asset(asset.supply) + } + this.setState({ + assets + }) + } catch (err) { + console.error(err) + } + } + validate = (values) => { const errors = {} const { price } = values @@ -99,7 +114,7 @@ class NFTTokenSell extends Component { } render() { - if (this.doNotRender) { + if (this.doNotRender || !this.state.assets) { return } @@ -120,9 +135,10 @@ class NFTTokenSell extends Component { const assets = {} assets['GOLOS'] = { supply: Asset(0, 3, 'GOLOS') } assets['GBG'] = { supply: Asset(0, 3, 'GBG') } - for (const asset of this.props.assets) { - asset.supply = asset.supply.symbol ? asset.supply : Asset(asset.supply) - assets[asset.supply.symbol] = asset + for (const asset of this.state.assets) { + const sym = asset.supply.symbol + if ($STM_Config.hidden_assets && $STM_Config.hidden_assets[sym]) continue + assets[sym] = asset } return
@@ -186,7 +202,6 @@ export default connect( (state, ownProps) => { return { ...ownProps, nft_tokens: state.global.get('nft_tokens'), - assets: state.global.get('assets') } }, diff --git a/app/components/pages/nft/AllNFTPage.jsx b/app/components/pages/nft/AllNFTPage.jsx new file mode 100644 index 0000000..f72cb7c --- /dev/null +++ b/app/components/pages/nft/AllNFTPage.jsx @@ -0,0 +1,177 @@ +import React from 'react' +import { connect, } from 'react-redux' +import tt from 'counterpart' +import Reveal from 'react-foundation-components/lib/global/reveal' + +import DropdownMenu from 'app/components/elements/DropdownMenu' +import Icon from 'app/components/elements/Icon' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import NFTTokenItem from 'app/components/elements/nft/NFTTokenItem' +import NFTTokenTransfer from 'app/components/modules/nft/NFTTokenTransfer' +import NFTTokenSell from 'app/components/modules/nft/NFTTokenSell' +import g from 'app/redux/GlobalReducer' + +class AllNFTPage extends React.Component { + state = {} + + componentDidMount() { + this.refetch() + } + + refetch = () => { + this.props.fetchNFTTokens(0, this.sort, this.sortReversed) + } + + showTransfer = (e, tokenIdx) => { + e.preventDefault() + this.setState({ + showTransfer: true, + tokenIdx, + }) + } + + hideTransfer = () => { + this.setState({ + showTransfer: false, + }) + } + + showSell = (e, tokenIdx) => { + e.preventDefault() + this.setState({ + showSell: true, + tokenIdx, + }) + } + + hideSell = () => { + this.setState({ + showSell: false, + }) + } + + sortOrder = (e, sort, sortReversed) => { + e.preventDefault() + this.sort = sort + this.sortReversed = sortReversed + this.refetch() + } + + render() { + const { currentUser, nft_tokens, nft_assets, } = this.props + + const tokens = nft_tokens ? nft_tokens.toJS().data : null + const assets = nft_assets ? nft_assets.toJS() : {} + + const next_from = nft_tokens && nft_tokens.get('next_from') + + let items = [] + if (!tokens) { + items = + } else if (!tokens.length) { + items = {tt('nft_tokens_jsx.not_yet')} + } else { + for (let i = 0; i < tokens.length; ++i) { + const token = tokens[i] + items.push() + } + } + + const { showTransfer, showSell, tokenIdx } = this.state + + const sortItems = [ + { link: '#', onClick: e => { + this.sortOrder(e, 'by_last_update', false) + }, value: tt('nft_tokens_jsx.sort_new') }, + { link: '#', onClick: e => { + this.sortOrder(e, 'by_last_update', true) + }, value: tt('nft_tokens_jsx.sort_old') }, + { link: '#', onClick: e => { + this.sortOrder(e, 'by_last_price', false) + }, value: tt('nft_tokens_jsx.sort_price') }, + { link: '#', onClick: e => { + this.sortOrder(e, 'by_name', false) + }, value: tt('nft_tokens_jsx.sort_name') }, + ] + + let currentSort + if (this.sort === 'by_last_price') { + currentSort = tt('nft_tokens_jsx.sort_price') + } else if (this.sort === 'by_name') { + currentSort = tt('nft_tokens_jsx.sort_name') + } else { + if (this.sortReversed) { + currentSort = tt('nft_tokens_jsx.sort_old') + } else { + currentSort = tt('nft_tokens_jsx.sort_new') + } + } + + return (
+
+
+

{tt('all_nft_page_jsx.title')}

+    + + + {currentSort} + + + +
+
+
+
+ {items} + {next_from ?
+
+
: null} +
+
+ + + + + + + + +
) + } +} + +module.exports = { + path: '/all-nft', + component: connect( + (state, ownProps) => { + const currentUser = state.user.getIn(['current']) + + return { + currentUser, + nft_tokens: state.global.get('nft_tokens'), + nft_assets: state.global.get('nft_assets') + } + }, + dispatch => ({ + fetchNFTTokens: (start_token_id, sort, sortReversed) => { + dispatch(g.actions.fetchNftTokens({ account: '', start_token_id, sort, reverse_sort: sortReversed })) + }, + }) + )(AllNFTPage), +} \ No newline at end of file diff --git a/app/components/pages/nft/AllNFTPage.scss b/app/components/pages/nft/AllNFTPage.scss new file mode 100644 index 0000000..873e70b --- /dev/null +++ b/app/components/pages/nft/AllNFTPage.scss @@ -0,0 +1,7 @@ +.AllNFTPage { + padding-top: 1rem; + padding-bottom: 1rem; + padding-left: 0.2rem; + padding-right: 0.2rem; + flex: 1; +} diff --git a/app/components/pages/nft/NFTMarketPage.jsx b/app/components/pages/nft/NFTMarketPage.jsx index c46eda6..70b13b3 100644 --- a/app/components/pages/nft/NFTMarketPage.jsx +++ b/app/components/pages/nft/NFTMarketPage.jsx @@ -1,28 +1,34 @@ import React from 'react' import { connect, } from 'react-redux' +import { Link } from 'react-router' import tt from 'counterpart' import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import NFTMarketCollections from 'app/components/elements/nft/NFTMarketCollections' import NFTTokenItem from 'app/components/elements/nft/NFTTokenItem' import g from 'app/redux/GlobalReducer' +import session from 'app/utils/session' class NFTMarketPage extends React.Component { componentDidMount() { - setTimeout(() => { + this.props.fetchNftMarketCollections('') + this.refetch() + } + + componentDidUpdate(prevProps) { + if (this.props.routeParams.name !== prevProps.routeParams.name) { this.refetch() - }, 500) + } } refetch = () => { - const { currentUser } = this.props - const username = currentUser && currentUser.get('username') - this.props.fetchNftMarket(username, '', 0, this.sort, this.sortReversed) + const collName = this.props.routeParams.name || '' + const curName = session.load().currentName + this.props.fetchNftMarket(curName, collName, 0, this.sort, this.sortReversed) } render() { - const { currentUser, nft_orders, own_nft_orders, nft_assets, routeParams } = this.props - - const { name } = routeParams + const { currentUser, nft_market_collections, nft_orders, own_nft_orders, nft_assets, routeParams } = this.props let content const orders = nft_orders ? nft_orders.toJS().data : null @@ -65,7 +71,7 @@ class NFTMarketPage extends React.Component { {items} {next_from ?
: null}
@@ -74,9 +80,18 @@ class NFTMarketPage extends React.Component {
} + const selected = this.props.routeParams.name + return
-

{tt('header_jsx.nft_market')}

+
+

{tt('header_jsx.nft_market')}

+ {nft_market_collections && + } + + {tt('all_nft_page_jsx.title')} + +
{content}
@@ -90,18 +105,23 @@ module.exports = { const currentUser = state.user.getIn(['current']) const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]) + const nft_market_collections = state.global.get('nft_market_collections') const nft_orders = state.global.get('nft_orders') const own_nft_orders = state.global.get('own_nft_orders') return { currentUser, currentAccount, + nft_market_collections, nft_orders, own_nft_orders, nft_assets: state.global.get('nft_assets') } }, dispatch => ({ + fetchNftMarketCollections: (start_name) => { + dispatch(g.actions.fetchNftMarketCollections({ start_name })) + }, fetchNftMarket: (account, collectionName, start_order_id, sort, reverse_sort) => { dispatch(g.actions.fetchNftMarket({ account, collectionName, start_order_id, sort, reverse_sort })) }, diff --git a/app/locales/en.json b/app/locales/en.json index 834b797..924efce 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -786,6 +786,11 @@ "your_token": "Your token", "owner": "Owner" }, + "all_nft_page_jsx": { + "title": "All NFT on the Golos", + "no_at_all": "No NFT-tokens on Golos. You can ", + "no_at_all2": "issue your own!" + }, "nft_collection_page_jsx": { "owner_is": "Owner of collection is ", "not_yet": "No tokens yet in this collection." @@ -812,6 +817,14 @@ "no_own_orders": "You are not selling any NFT token yet.", "own_orders": "My orders" }, + "nft_market_collections_jsx": { + "all_collections": "All collections", + "order_count": { + "zero": "No orders", + "one": "1 order", + "other": "%(count)s orders" + } + }, "invites_jsx": { "create_invite": "Create new invite check", "create_invite_info": "Cheques (invite codes) are a universal tool for transferring of GOLOS tokens to other people outside the blockchain. There are two ways to redeem the code: transfer its balance to your account or register a new account using it.", diff --git a/app/locales/ru-RU.json b/app/locales/ru-RU.json index 1deb421..9d9dcd7 100644 --- a/app/locales/ru-RU.json +++ b/app/locales/ru-RU.json @@ -1140,6 +1140,11 @@ "your_token": "Ваш токен", "owner": "Владелец" }, + "all_nft_page_jsx": { + "title": "Все NFT на Golos", + "no_at_all": "На Golos нет ни одного NFT-токена. Вы можете ", + "no_at_all2": "выпустить первый токен!" + }, "nft_collection_page_jsx": { "owner_is": "Владелец коллекции - ", "not_yet": "В этой коллекции еще нет токенов." @@ -1166,6 +1171,14 @@ "no_own_orders": "Вы пока не продаете NFT-токенов.", "own_orders": "Мои ордеры" }, + "nft_market_collections_jsx": { + "all_collections": "Все коллекции", + "order_count": { + "zero": "Нет ордеров", + "one": "1 ордер", + "other": "%(count)s ордеров" + } + }, "invites_jsx": { "create_invite": "Создание чека", "create_invite_info": "Чеки (инвайт-коды) — инструмент для передачи токенов другим людям вне блокчейна. Использовать чек можно двумя способами: перевести его баланс на аккаунт (форма для этого ниже) или зарегистрировать с его помощью новый аккаунт.", diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index f106594..59403b7 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -64,6 +64,7 @@ export function* fetchDataWatches () { yield fork(watchFetchNftTokens) yield fork(watchFetchNftCollectionTokens) yield fork(watchFetchNftMarket) + yield fork(watchFetchNftMarketCollections) } export function* watchGetContent() { @@ -701,4 +702,37 @@ export function* fetchNftMarket({ payload: { account, collectionName, start_orde } catch (err) { console.error('fetchNftMarket', err) } -} \ No newline at end of file +} + +export function* watchFetchNftMarketCollections() { + yield takeLatest('global/FETCH_NFT_MARKET_COLLECTIONS', fetchNftMarketCollections) +} + +export function* fetchNftMarketCollections({ payload: { start_name } }) { + try { + const limit = 99 + + const nft_colls = yield call([api, api.getNftCollectionsAsync], { + start_name, + limit: limit + 1, + sort: 'by_token_count' + }) + + let next_from + if (nft_colls.length > limit) { + next_from = nft_colls.pop().name + } + + try { + yield fillNftCollectionImages(nft_colls) + } catch (err) { + console.error(err) + } + + yield put(GlobalReducer.actions.receiveNftMarketCollections({ + nft_colls, start_name, next_from + })) + } catch (err) { + console.error('fetchNftMarketCollections', err) + } +} diff --git a/app/redux/GlobalReducer.js b/app/redux/GlobalReducer.js index ae8a45c..e37e2fa 100644 --- a/app/redux/GlobalReducer.js +++ b/app/redux/GlobalReducer.js @@ -51,6 +51,11 @@ const upsertOwnNftOrders = (state, own_nft_orders, start_order_id, next_from) => return upsertPagedItems(state, 'own_nft_orders', own_nft_orders, start_order_id, next_from) } +const upsertNftMarketColls = (state, nft_colls, start_name, next_from) => { + return upsertPagedItems(state, 'nft_market_collections', nft_colls, start_name, next_from) +} + + export default createModule({ name: 'global', initialState: Map({ @@ -266,6 +271,18 @@ export default createModule({ return new_state }, }, + { + action: 'FETCH_NFT_MARKET_COLLECTIONS', + reducer: state => state, + }, + { + action: 'RECEIVE_NFT_MARKET_COLLECTIONS', + reducer: (state, { payload: { nft_colls, start_name, next_from, } }) => { + let new_state = state + new_state = upsertNftMarketColls(new_state, nft_colls, start_name, next_from) + return new_state + }, + }, { action: 'LINK_REPLY', reducer: (state, { payload: op }) => { diff --git a/app/redux/TransactionSaga.js b/app/redux/TransactionSaga.js index e75b2bd..68a75c9 100644 --- a/app/redux/TransactionSaga.js +++ b/app/redux/TransactionSaga.js @@ -144,12 +144,13 @@ function* broadcastOperation( } } yield call(broadcastPayload, {payload}) - let eventType = type.replace(/^([a-z])/, g => g.toUpperCase()).replace(/_([a-z])/g, g => g[1].toUpperCase()); - if (eventType === 'Comment' && !op.parent_author) eventType = 'Post'; - const page = eventType === 'Vote' ? `@${op.author}/${op.permlink}` : ''; - serverApiRecordEvent(eventType, page); } catch(error) { console.error('TransactionSaga', error) + try { + serverApiRecordEvent('node_error/tx', JSON.stringify(operations) + ' ||| ' + error.toString()) + } catch (err2) { + console.error('Cannot record tx error event:', err2) + } if(errorCallback) errorCallback(error.toString()) } } @@ -222,6 +223,12 @@ function* broadcastPayload({payload: {operations, keys, username, hideErrors, su console.error('TransactionSaga\tbroadcast', error) // status: error + try { + serverApiRecordEvent('node_error/tx_broadcast', JSON.stringify(operations) + ' ||| ' + error.toString()) + } catch (err2) { + console.error('Cannot record tx broadcast error event:', err2) + } + yield put(tr.actions.error({operations, error, hideErrors, errorCallback})) for (const [type, operation] of operations) { diff --git a/server/api/general.js b/server/api/general.js index 8e5b148..9a96a4e 100644 --- a/server/api/general.js +++ b/server/api/general.js @@ -76,7 +76,6 @@ export default function useGeneralApi(app) { const params = ctx.request.body; const {csrf, type, value} = typeof(params) === 'string' ? JSON.parse(params) : params; if (!ctx.checkCSRF(csrf)) return; - console.log('-- /record_event -->', ctx.session.uid, type, value); const str_value = typeof value === 'string' ? value : JSON.stringify(value); recordWebEvent(ctx, type, str_value); ctx.body = JSON.stringify({status: 'ok'}); diff --git a/server/record_web_event.js b/server/record_web_event.js index 78ec96e..da2203b 100644 --- a/server/record_web_event.js +++ b/server/record_web_event.js @@ -1,2 +1,3 @@ export default function recordWebEvent(ctx, event_type, value) { + console.log('-- /record_event -->', event_type, value) }