diff --git a/package.json b/package.json index fd1002f..3931009 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "formik": "^2.2.9", "git-rev-sync": "^3.0.1", "gmail-send": "^1.8.10", - "golos-lib-js": "^0.9.54", + "golos-lib-js": "^0.9.60", "iron-session": "6.0.4", "jspdf": "^2.3.0", "lodash": "^4.17.11", diff --git a/public/icons/dropdown-arrow.svg b/public/icons/dropdown-arrow.svg new file mode 100644 index 0000000..8828aac --- /dev/null +++ b/public/icons/dropdown-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/nft.png b/public/images/nft.png new file mode 100644 index 0000000..db3321a Binary files /dev/null and b/public/images/nft.png differ diff --git a/src/App.scss b/src/App.scss index 74dd489..410e3af 100644 --- a/src/App.scss +++ b/src/App.scss @@ -6,9 +6,14 @@ @include foundation-everything(true); @import "./elements/AccountMenu"; +@import "./elements/DropdownMenu"; @import "./elements/GeneratedPasswordInput"; @import "./elements/Expandable"; @import "./elements/LoadingIndicator"; +@import "./elements/nft/NFTSmallIcon"; +@import "./elements/nft/NFTTokens"; +@import "./elements/PagedDropdownMenu"; +@import "./elements/VerticalMenu"; @import "./modules/Header"; @import "./modules/LoginForm"; @@ -21,7 +26,7 @@ @import "./pages/login"; @import "./pages/register"; @import "./pages/sign/transfer"; -@import "./pages/sign/transfer"; +@import "./pages/sign/transfer_nft"; @import "./pages/oauth/[client]/[perms]"; @import "./foundation-overrides"; diff --git a/src/elements/DropdownMenu.jsx b/src/elements/DropdownMenu.jsx new file mode 100644 index 0000000..5b247d4 --- /dev/null +++ b/src/elements/DropdownMenu.jsx @@ -0,0 +1,73 @@ +import React from 'react' + +import VerticalMenu from '@/elements/VerticalMenu' +import { findParent } from '@/utils/DomUtils' + +export default class DropdownMenu extends React.Component { + constructor(props) { + super(props); + this.state = { + shown: false, + selected: props.selected + }; + } + + componentWillUnmount() { + document.removeEventListener('click', this.hide); + } + + toggle = (e) => { + const {shown} = this.state + if(shown) this.hide(e) + else this.show(e) + } + + show = (e) => { + e.preventDefault(); + this.setState({shown: true}); + setTimeout(() => { + document.addEventListener('click', this.hide) + }, 1) + }; + + hide = (e) => { + // Do not hide the dropdown if there was a click within it. + const inside_dropdown = !!findParent(e.target, 'VerticalMenu'); + if (inside_dropdown) return; + + e.preventDefault(); + this.setState({shown: false}); + document.removeEventListener('click', this.hide); + }; + + navigate = (e) => { + const a = e.target.nodeName.toLowerCase() === 'a' ? e.target : e.target.parentNode; + this.setState({show: false}); + if (a.host !== window.location.host) return; + e.preventDefault(); + window.location.href = a.pathname + a.search + }; + + getSelectedLabel = (items, selected) => { + const selectedEntry = items.find(i => i.value === selected) + const selectedLabel = selectedEntry && selectedEntry.label ? selectedEntry.label : selected + return selectedLabel + } + + render() { + const {el, items, selected, children, className, title, href, onClick, noArrow, hideSelected} = this.props; + const hasDropdown = items.length > 0 + + let entry = children || + {this.getSelectedLabel(items, selected)} + {hasDropdown && !noArrow && } + + + if(hasDropdown) entry = { onClick(e); this.toggle(e) } : this.toggle}>{entry} + + const menu = ; + const cls = 'DropdownMenu' + (this.state.shown ? ' show' : '') + (className ? ` ${className}` : '') + return React.createElement(el, {className: cls}, [entry, menu]); + } +} + diff --git a/src/elements/DropdownMenu.scss b/src/elements/DropdownMenu.scss new file mode 100644 index 0000000..429ef04 --- /dev/null +++ b/src/elements/DropdownMenu.scss @@ -0,0 +1,53 @@ +.DropdownMenu { + position: relative; + display: inline-block; + + .Icon.dropdown-arrow { + top: 2px; + margin-right: 0; + } + + > .VerticalMenu { + visibility: hidden; + // min-width: 145px; + min-width: 232px; + z-index: 1000; + + background-color: white; + + display: block; + border: 1px solid $medium-gray; + border-radius: $global-radius; + opacity: 0; + position: absolute; + top: 100%; + + // width: auto; + transform: translateY(10%); + transition: all 0.3s ease 0s, visibility 0s linear 0.3s; + box-shadow: 1px 1px 5px 0px rgba(50, 50, 50, 0.75); + } + + &.show > .VerticalMenu { + visibility: visible; + opacity: 1; + transform: translateX(0%); + transition-delay: 0s; + } + + .DropdownMenu.move-left { + .VerticalMenu { + left: -50%; + } + } + + &.above > .VerticalMenu { + bottom: 100%; + top: auto; + } + + &.top-most > .VerticalMenu { + position: fixed; + top: auto; + } +} diff --git a/src/elements/PagedDropdownMenu.jsx b/src/elements/PagedDropdownMenu.jsx new file mode 100644 index 0000000..fe931b3 --- /dev/null +++ b/src/elements/PagedDropdownMenu.jsx @@ -0,0 +1,152 @@ +import React from 'react' +import cloneDeep from 'lodash/cloneDeep' +import isEqual from 'lodash/isEqual' +import tt from 'counterpart' + +import DropdownMenu from '@/elements/DropdownMenu' +import LoadingIndicator from '@/elements/LoadingIndicator' + +const hideLastItem = true; + +export default class PagedDropdownMenu extends React.Component { + static defaultProps = { + page: 1 + } + + constructor(props) { + super(props); + this.state = { + items: [], + page: props.page, + loading: false, + }; + } + + componentDidMount() { + const { items, page, } = this.props + this.initItems(this.sliceItems(items, page)) + this.setState({ page }) + } + + componentDidUpdate(prevProps) { + const { items, page, } = this.props + if (items && (!prevProps.items || !isEqual(items, prevProps.items))) { + const sliced = this.sliceItems(items, 1) + this.initItems(sliced) + this.setState({ page: 1 }) + } else if (page && prevProps.page !== page) { + this.setState({ page }) + } + } + + sliceItems = (items, page) => { + const { onLoadMore, perPage } = this.props + if (onLoadMore) { + return items + } + const startIdx = perPage * (page - 1) + const endIdx = startIdx + perPage + 1 + const sliced = items.slice(startIdx, endIdx) + return sliced + } + + initItems = (items) => { + if (!items || !items.length) + return; + this.setState({ + items: cloneDeep(items), + }); + }; + + loadMore = async (newPage) => { + const { items, page, } = this.state; + const { onLoadMore, } = this.props; + if (!onLoadMore) { + setTimeout(async () => { + this.setState({ + page: newPage + }, () => { + this.initItems(this.sliceItems(this.props.items, newPage)) + }) + }, 10); + return + } + setTimeout(async () => { + this.setState({ + page: newPage, + loading: true, + }); + if (onLoadMore) { + const res = await onLoadMore({ page, newPage, items, }); + this.setState({ + loading: false, + }); + this.initItems(res); + } + }, 10); + }; + + nextPage = () => { + const { page, } = this.state; + this.loadMore(page + 1); + }; + + prevPage = () => { + if (this.state.page === 1) return; + const { page, } = this.state; + this.loadMore(page - 1); + }; + + _renderPaginator = () => { + const { perPage, } = this.props; + const { items, page, } = this.state; + const hasMore = items.length > perPage; + if (page === 1 && !hasMore) { + return null; + } + const hasPrev = page > 1 + return { + value: + + {hasPrev ? '< ' + tt('g.back') : ''} + + {hasMore ? tt('g.more_list') + ' >' : ''} + , + }; + }; + + render() { + const { el, selected, children, className, title, href, noArrow, perPage, renderItem, hideSelected, } = this.props + const { items, loading, } = this.state; + + let itemsWithPaginator = []; + if (!loading) { + for (let i = 0; i < items.length; ++i) { + const rendered = renderItem(items[i]) + itemsWithPaginator.push(rendered) + } + if (items.length > perPage && hideLastItem) { + itemsWithPaginator.pop(); + } + const paginator = this._renderPaginator(); + if (paginator) { + itemsWithPaginator.push(paginator); + } + } else { + itemsWithPaginator = [{value: + + }]; + } + + return () + } +}; \ No newline at end of file diff --git a/src/elements/PagedDropdownMenu.scss b/src/elements/PagedDropdownMenu.scss new file mode 100644 index 0000000..a6c88ee --- /dev/null +++ b/src/elements/PagedDropdownMenu.scss @@ -0,0 +1,16 @@ +.PagedDropdownMenu__paginator { + display: inline-block !important; + text-align: center; + color: #0078C4 !important; + font-weight: normal !important; + font-size: 100% !important; + user-select: none; + cursor: pointer; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + width: 50%; + + &.disabled { + cursor: auto; + } +} diff --git a/src/elements/VerticalMenu.jsx b/src/elements/VerticalMenu.jsx new file mode 100644 index 0000000..1cc3923 --- /dev/null +++ b/src/elements/VerticalMenu.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import LinkEx from '@/utils/LinkEx' + +export default class VerticalMenu extends React.Component { + closeMenu = (e) => { + // If this was not a left click, or if CTRL or CMD were held, do not close the menu. + if(e.button !== 0 || e.ctrlKey || e.metaKey) return; + + // Simulate clicking of document body which will close any open menus + document.body.click(); + } + + render() { + const {items, title, description, className, hideValue} = this.props; + return ; + } +} diff --git a/src/elements/VerticalMenu.scss b/src/elements/VerticalMenu.scss new file mode 100644 index 0000000..e8cbc65 --- /dev/null +++ b/src/elements/VerticalMenu.scss @@ -0,0 +1,74 @@ +.VerticalMenu { + + width: 200px; + + .Icon { + padding-left: 0.1rem; + margin-right: 1.3rem; + top: 0; + fill: $dark-gray; + } + + > li > a { + display: flex; + align-items: center !important; + line-height: 1rem; + position: relative; + padding: 0.7rem 1rem; + + font-size: 14px; + letter-spacing: 0.4px; + } + + > li > a:hover { + background-color: #f0f0f0; + } + + > li.title { + padding: 0.56rem; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.4px; + line-height: 16px; + text-align: center; + border-bottom: 1px solid $light-gray; + } + + > li.description { + padding: 0.1rem; + font-size: 87.5%; + text-align: center; + border-bottom: 1px solid $light-gray; + } + + &_nav-profile { + width: 262px; + + & > li > a { + padding: 0 1rem 0 3.8rem; + line-height: 50px; + + .Icon { + } + + &:hover { + .Icon { + fill: #3F46AD; + } + } + } + + } + + &_nav-additional { + width: 260px; + margin: 10px 0; + + & > li > a { + padding: 0 20px; + line-height: 50px; + } + } + +} + diff --git a/src/elements/nft/NFTSmallIcon.jsx b/src/elements/nft/NFTSmallIcon.jsx new file mode 100644 index 0000000..c3dba89 --- /dev/null +++ b/src/elements/nft/NFTSmallIcon.jsx @@ -0,0 +1,12 @@ +import React, { Component, } from 'react' + +class NFTSmallIcon extends Component { + render() { + const { image, ...rest } = this.props + + return + } +} + +export default NFTSmallIcon diff --git a/src/elements/nft/NFTSmallIcon.scss b/src/elements/nft/NFTSmallIcon.scss new file mode 100644 index 0000000..0ee5941 --- /dev/null +++ b/src/elements/nft/NFTSmallIcon.scss @@ -0,0 +1,11 @@ +.NFTSmallIcon { + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + border-radius: 50%; + + width: 3rem; + height: 3rem; + display: inline-block; + vertical-align: top; +} diff --git a/src/elements/nft/NFTTokens.jsx b/src/elements/nft/NFTTokens.jsx new file mode 100644 index 0000000..1fa9c1c --- /dev/null +++ b/src/elements/nft/NFTTokens.jsx @@ -0,0 +1,90 @@ +import React from 'react' +import tt from 'counterpart' + +import NFTSmallIcon from '@/elements/nft/NFTSmallIcon' +import PagedDropdownMenu from '@/elements/PagedDropdownMenu' +import DropdownMenu from '@/elements/DropdownMenu' + +export function NFTImageStub() { + return '/images/nft.png' +} + +export function parseNFTImage(json_metadata, useStub = true) { + if (json_metadata) { + const meta = JSON.parse(json_metadata) + if (meta && meta.image) return meta.image + } + if (!useStub) return null + return NFTImageStub() +} + +class NFTTokens extends React.Component { + render() { + let { tokens, selected } = this.props + + let selectedItem + + const items = [] + let i = 0 + for (const token of tokens) { + items.push({ + key: i, + link: '#', + value: token.token_id, + }) + + if (selected.toString() === token.token_id.toString()) { + const image = parseNFTImage(token.json_metadata) + + let data = {} + try { + data = JSON.parse(token.json_metadata) + } catch (err) {} + + selectedItem = + +   + + {data.title || '#' + token.token_id} + + + } + + ++i + } + + return { + const token = tokens[item.key] + + const image = parseNFTImage(token.json_metadata) + + let data = {} + try { + data = JSON.parse(token.json_metadata) + } catch (err) {} + + return { + ...item, + label: + +   + {data.title || '#' + token.token_id} + , + addon: {token.name}, + onClick: (e) => { + if (this.props.onItemClick) + this.props.onItemClick(e, token) + } + } + }} + selected={selected} + perPage={10} + hideSelected={tokens.length > 1}> + {selectedItem} + + + } +} + +export default NFTTokens diff --git a/src/elements/nft/NFTTokens.scss b/src/elements/nft/NFTTokens.scss new file mode 100644 index 0000000..6ee4480 --- /dev/null +++ b/src/elements/nft/NFTTokens.scss @@ -0,0 +1,17 @@ +.NFTTokens { + .NFTSmallIcon { + margin-top: 0.25rem; + margin-right: 0.25rem; + margin-bottom: 0.25rem; + width: 2rem; + height: 2rem; + } + + .VerticalMenu { + a { + padding: 5px !important; + padding-left: 8px !important; + color: black !important; + } + } +} diff --git a/src/locales/en.json b/src/locales/en.json index 20572ff..04df70c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -172,6 +172,7 @@ "transfer": "Transfer tokens", "donate": "Donate to user", "delegate_vs": "Delegate vesting shares", + "transfer_nft": "Transfer NFT-token", "unfreeze": "Unfreeze account", "apps_title": "Apps", "apps_empty": "You are not authorized in any app.", @@ -190,6 +191,11 @@ "memo_is_public": "This memo is public.", "submit": "Transfer" }, + "oauth_transfer_nft": { + "no_tokens": "You have not yet any NFT-tokens.", + "token_not_exist": "No such token in your wallet. You can transfer any of these tokens:", + "token_is_not_your": "This token is not your. You can transfer any of these tokens:" + }, "oauth_donate": { "submit": "Donate", "balance": "TIP-balance: " @@ -251,7 +257,15 @@ "worker_request_vote": "Voting for worker requests", "proposal_create": "Creating transaction proposals", "proposal_delete": "Removing transaction proposals", - "proposal_update": "Voting in transaction proposals" + "proposal_update": "Voting in transaction proposals", + "proposal_update_active": "Voting in transaction proposals with active authorities", + "paid_subscription_create": "Creating, deleting, updating paid subscriptions", + "paid_subscription_transfer": "Buying paid subscriptions", + "paid_subscription_cancel": "Canceling of paid subscriptions", + "nft_collection": "Creating and deleting NFT-collections", + "nft_issue": "Issueing of NFT-tokens", + "nft_transfer": "Transferring of NFT-tokens", + "nft_orders": "Selling, buying NFT-tokens" }, "recovery": { "change_title": "Change recovery account", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index 2b771f2..b6b56eb 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -172,6 +172,7 @@ "transfer": "Перевести токены", "donate": "Отблагодарить пользователя", "delegate_vs": "Делегировать Силу Голоса", + "transfer_nft": "Передать NFT-токен", "unfreeze": "Активировать аккаунт", "apps_title": "Приложения", "apps_empty": "Вы пока не авторизованы ни в одном приложении.", @@ -190,6 +191,11 @@ "memo_is_public": "Эта заметка является публичной.", "submit": "Перевести" }, + "oauth_transfer_nft": { + "no_tokens": "У вас пока еще нет NFT-токенов.", + "token_not_exist": "Выбранного токена не существует (или он не принадлежит вам). Вы можете передать любой из следующих токенов:", + "token_is_not_your": "Выбранный токен вам не принадлежит. Вы можете передать любой из следующих токенов:" + }, "oauth_donate": { "submit": "Передать", "balance": "TIP-баланс: " @@ -251,7 +257,15 @@ "worker_request_vote": "Голосование за заявки на работу (воркеры)", "proposal_create": "Создание пропозалов", "proposal_delete": "Удаление пропозалов", - "proposal_update": "Голосование в пропозалах" + "proposal_update": "Голосование в пропозалах", + "proposal_update_active": "Голосование в пропозалах с активным ключом", + "paid_subscription_create": "Создание, удаление и редактирование платных подписок", + "paid_subscription_transfer": "Покупка платных подписок", + "paid_subscription_cancel": "Отмена покупки платных подписок", + "nft_collection": "Создание и удаление коллекций NFT-токенов", + "nft_issue": "Выпуск NFT-токенов", + "nft_transfer": "Перевод NFT-токенов", + "nft_orders": "Покупка, продажа NFT-токенов" }, "recovery": { "change_title": "Задать аккаунт для восстановления", diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 0ae1fcf..7615d55 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -54,13 +54,11 @@ class Index extends React.Component { const { service_account, sign_endpoint, } = oauthCfg; let actions = []; for (let action of [ - 'transfer', 'donate', 'delegate_vs']) { + 'transfer', 'donate', 'delegate_vs', 'transfer_nft']) { actions.push( - - - + ); } let clientList = []; diff --git a/src/pages/sign/transfer_nft.jsx b/src/pages/sign/transfer_nft.jsx new file mode 100644 index 0000000..c322582 --- /dev/null +++ b/src/pages/sign/transfer_nft.jsx @@ -0,0 +1,300 @@ +import React from 'react'; +import tt from 'counterpart'; +import golos from 'golos-lib-js'; +import { Asset, } from 'golos-lib-js/lib/utils'; +import { Formik, Field, ErrorMessage, } from 'formik'; +import Head from 'next/head'; + +import LoadingIndicator from '@/elements/LoadingIndicator' +import LoginForm from '@/modules/LoginForm'; +import NFTTokens from '@/elements/nft/NFTTokens' +import Header from '@/modules/Header' +import { getOAuthCfg, getChainData, } from '@/server/oauth'; +import { getOAuthSession, } from '@/server/oauthSession'; +import { withSecureHeadersSSR, } from '@/server/security'; +import { callApi, } from '@/utils/OAuthClient'; +import validate_account_name from '@/utils/validate_account_name'; + +const uncompose = (query, initial) => { + initial.to = query.to || initial.to + if (query.token_id) initial.token_id = query.token_id + initial.memo = query.memo || initial.memo +}; + +export const getServerSideProps = withSecureHeadersSSR(async ({ req, res, resolvedUrl, query, }) => { + const action = resolvedUrl.split('?')[0].split('/')[2]; + let chainData = null; + const holder = await getOAuthSession(req, res); + if (!holder.oauthEnabled) { + return await holder.clearAndRedirect(); + } + const session = holder.session(); + let initial = null; + if (session.account) { + chainData = await getChainData(session.account, action); + if (chainData.frozen) { + return await holder.freeze(session.account) + } + initial = { + from: session.account, + to: '', + memo: '', + }; + uncompose(query, initial); + } + return { + props: { + action, + oauthCfg: getOAuthCfg(), + session, + chainData, + initial, + }, + }; +}) + +class TransferNFT extends React.Component { + static propTypes = { + }; + + state = { + }; + + componentDidMount() { + const { initial, chainData } = this.props + if (initial) { + if (initial.token_id) { + const { nft_tokens } = chainData + let exists = false + let not_your = false + let first + for (const token of nft_tokens) { + first = first || token.token_id + if (token.token_id.toString() === initial.token_id.toString()) { + exists = true + if (token.owner !== initial.from) { + not_your = true + } + break + } + } + this.setState({ + selected: exists ? initial.token_id : first, + query_token_error: !exists ? tt('oauth_transfer_nft.token_not_exist') : not_your ? tt('oauth_transfer_nft.token_is_not_your') : null + }) + } else { + const { nft_tokens } = chainData + if (nft_tokens.length) { + this.setState({ + selected: nft_tokens[0].token_id + }) + } + } + } + } + + normalizeChainData = () => { + const { chainData, } = this.props; + if (!chainData || !golos.isNativeLibLoaded()) { + return; + } + return chainData + } + + onToChange = (e, handle) => { + let value = e.target.value.trim().toLowerCase(); + e.target.value = value; + return handle(e); + }; + + validate = (values) => { + const errors = {}; + if (!values.to) { + errors.to = tt('g.required'); + } else { + const err = validate_account_name(values.to); + if (err) errors.to = err; + } + + return errors; + }; + + _onSubmit = (values, { setSubmitting, }) => { + this.setState({ + done: false, + }) + + const { action, session, oauthCfg, } = this.props; + const { sign_endpoint, } = oauthCfg; + const { from, to, } = values; + const { selected } = this.state + + let memo = values.memo || ''; + + golos.config.set('websocket', sign_endpoint); + golos.config.set('credentials', 'include'); + const callback = (err, res) => { + setSubmitting(false); + if (err) { + alert(err); + return; + } + window.close(); + this.setState({ + done: true, + }) + }; + golos.broadcast.nftTransfer('', parseInt(selected), from, to, memo, callback); + }; + + compose = (values) => { + if (!$GLS_IsBrowser) { + return; + } + + let url = window.location.href.split('?')[0]; + + const chainData = this.normalizeChainData() + const { to, memo, } = values; + + if (!golos.isNativeLibLoaded() || !chainData) + return '...'; + + url += '?'; + url += 'to=' + (to || ''); + if (this.state.selected) { + url += '&token_id=' + this.state.selected + } + url += '&memo=' + (memo || ''); + + return ( + {tt('oauth_main_jsx.link')} + {url} + ); + }; + + render() { + const { state, } = this; + const { done, selected, query_token_error } = state; + const chainData = this.normalizeChainData() + + const { action, oauthCfg, session, initial, } = this.props; + const { account, } = session; + + if (account === null) { + return (
+ + + {tt('oauth_main_jsx.' + action)} | {tt('oauth_main_jsx.title')} + +
+ +
); + } + + if (!chainData) + return null + + const { nft_tokens } = chainData + + let form = null; + if (initial && chainData) form = ( + {({ + handleSubmit, isSubmitting, isValid, dirty, errors, touched, values, handleChange, setFieldValue, + }) => ( +
+
+ @ + +
+ +
+ @ + this.onToChange(e, handleChange)} + /> +
+ + + {query_token_error &&
{query_token_error}
} + + {nft_tokens.length ? + { + e.preventDefault() + this.setState({ + selected: token.token_id, + query_token_error: null + }) + }} + /> : +
{tt('oauth_transfer_nft.no_tokens')}
} + + + + {isSubmitting && } + + {done ? + {tt('g.done')} + : null} +
+ {this.compose(values)} +
+ + )} +
); + + return (
+ + + {tt('oauth_main_jsx.' + action)} | {tt('oauth_main_jsx.title')} + +
+
+
+

{tt('oauth_main_jsx.' + action)}

+ {(!account || !form) && } + {form} +
+
+
); + } +}; + +export default TransferNFT diff --git a/src/pages/sign/transfer_nft.scss b/src/pages/sign/transfer_nft.scss new file mode 100644 index 0000000..baec762 --- /dev/null +++ b/src/pages/sign/transfer_nft.scss @@ -0,0 +1,14 @@ +.TransferNFT { + .error { + margin-bottom: 1rem; + } + .button { + margin-bottom: 0rem; + } + .done { + margin-left: 0.5rem; + } + .page-link { + margin-top: 1rem; + } +} diff --git a/src/server/oauth.js b/src/server/oauth.js index 498ecab..6bf9635 100644 --- a/src/server/oauth.js +++ b/src/server/oauth.js @@ -159,6 +159,15 @@ async function getChainData(account, action = 'transfer') { if (data.balances['GBG']) data.balances['GBG'] = acc.sbd_balance; + if (action === 'transfer_nft') { + data.nft_tokens = await golos.api.getNftTokensAsync({ + owner: account, + state: 'not_selling_only', + limit: 100, + }) + return data; + } + let uia = await golos.api.getAccountsBalancesAsync([account]); if (!uia[0]) { return data; diff --git a/src/utils/DomUtils.js b/src/utils/DomUtils.js new file mode 100644 index 0000000..36021a8 --- /dev/null +++ b/src/utils/DomUtils.js @@ -0,0 +1,5 @@ +export function findParent(el, class_name) { + if (el.className && el.className.indexOf && el.className.indexOf(class_name) !== -1) return el; + if (el.parentNode) return findParent(el.parentNode, class_name); + return null; +} diff --git a/src/utils/LinkEx.jsx b/src/utils/LinkEx.jsx new file mode 100644 index 0000000..62fd5f7 --- /dev/null +++ b/src/utils/LinkEx.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import Link from 'next/link' + +const isExternal = (url) => { + return /^https?:\/\//.test(url) +} + +class LinkEx extends React.Component { + render() { + const { props } = this + const { href, children } = props + if (isExternal(href) || href === '#') { + const rel = props.rel || 'noopener noreferrer' + return {children} + } + return {children} + } +} + +export default LinkEx diff --git a/src/utils/oauthPermissions.js b/src/utils/oauthPermissions.js index b96c2de..7ee8ec1 100644 --- a/src/utils/oauthPermissions.js +++ b/src/utils/oauthPermissions.js @@ -413,6 +413,70 @@ let permissions = { }, internal: true, }, + paid_subscription_create: { + ops: [ + 'paid_subscription_create', + 'paid_subscription_update', + 'paid_subscription_delete', + ], + cond: (op) => { + return [POSTING, op.author]; + }, + }, + paid_subscription_transfer: { + ops: [ + 'paid_subscription_transfer', + ], + cond: (op) => { + if (op.from_tip) { + return [POSTING, op.from] + } + return [ACTIVE, op.from] + }, + }, + paid_subscription_cancel: { + ops: [ + 'paid_subscription_cancel', + ], + cond: (op) => { + return [POSTING, op.subscriber] + }, + }, + nft_collection: { + ops: [ + 'nft_collection', + 'nft_collection_delete', + ], + cond: (op) => { + return [ACTIVE, op.creator] + }, + }, + nft_issue: { + ops: [ + 'nft_issue', + ], + cond: (op) => { + return [ACTIVE, op.creator] + }, + }, + nft_transfer: { + ops: [ + 'nft_transfer', + ], + cond: (op) => { + return [ACTIVE, op.from] + }, + }, + nft_orders: { + ops: [ + 'nft_sell', + 'nft_buy', + 'nft_cancel_order', + ], + cond: (op) => { + return [ACTIVE, op.seller || op.owner || op.buyer] + }, + }, }; module.exports = { diff --git a/yarn.lock b/yarn.lock index ca30db4..03de80c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1072,7 +1072,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert@2.0.0, assert@^2.0.0: +assert@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32" integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A== @@ -1082,6 +1082,17 @@ assert@2.0.0, assert@^2.0.0: object-is "^1.0.1" util "^0.12.0" +assert@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== + dependencies: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" @@ -1706,9 +1717,9 @@ core-js@^2.4.0: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.17.3: - version "3.32.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.0.tgz#7643d353d899747ab1f8b03d2803b0312a0fb3b6" - integrity sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww== + version "3.33.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.1.tgz#ef3766cfa382482d0a2c2bc5cb52c6d88805da52" + integrity sha512-qVSq3s+d4+GsqN0teRCJtM6tdEEXyWxjzbhVrCHmBS5ZTM0FS2MOS0D13dUXAWDUN6a+lHI/N1hF9Ytz6iLl9Q== core-js@^3.6.0: version "3.16.4" @@ -2005,6 +2016,15 @@ deepmerge@^2.1.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== +define-data-property@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -2012,6 +2032,15 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-properties@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2743,6 +2772,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -2783,6 +2817,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-orientation@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/get-orientation/-/get-orientation-1.1.2.tgz#20507928951814f8a91ded0a0e67b29dfab98947" @@ -2924,10 +2968,10 @@ gmail-send@^1.8.10: lodash "^4.17.15" nodemailer "^6.3.0" -golos-lib-js@^0.9.54: - version "0.9.54" - resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.54.tgz#87399d038c948f5d447e457df768c5d3fb26c8b2" - integrity sha512-YyvWp3QL5jH+1PbYl23lCIIVz0znNWytCZPAfzk4SKbGv3PRYXRB/Jp8ZZNjmJsXMby5+ZB5imRLFZo+ggfXXg== +golos-lib-js@^0.9.60: + version "0.9.60" + resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.60.tgz#abf7e88499954312c817ebc38532e8e8d0451cbd" + integrity sha512-9UlH8OLTZj70godojecYTHIJ/6X+YW80zDVTYEq4qGDjZFlIAyczqu4UHlwZIxOtlZoyFMFtxMonWEXkw3nnPg== dependencies: abort-controller "^3.0.0" assert "^2.0.0" @@ -2951,6 +2995,13 @@ golos-lib-js@^0.9.54: stream-browserify "^3.0.0" ws "^8.2.3" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@4.1.15: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" @@ -3001,11 +3052,28 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -3042,6 +3110,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -3369,7 +3444,7 @@ is-installed-globally@~0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-nan@^1.2.1: +is-nan@^1.2.1, is-nan@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== @@ -4038,9 +4113,9 @@ node-fetch@2.6.1: integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== node-fetch@^2.6.12: - version "2.6.12" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" - integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" @@ -4172,7 +4247,7 @@ object-inspect@^1.11.0, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== -object-is@^1.0.1: +object-is@^1.0.1, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -4195,6 +4270,16 @@ object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" @@ -5903,6 +5988,17 @@ util@0.12.4, util@^0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + utils-merge@1.x.x: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -6098,9 +6194,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= ws@^8.2.3: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== xtend@^4.0.2: version "4.0.2"