diff --git a/src/app/ResolveRoute.js b/src/app/ResolveRoute.js index f5b0223bd..1af9bdd2d 100644 --- a/src/app/ResolveRoute.js +++ b/src/app/ResolveRoute.js @@ -3,7 +3,7 @@ import GDPRUserList from './utils/GDPRUserList'; export const routeRegex = { UserProfile1: /^\/(@[\w\.\d-]+)\/?$/, - UserProfile2: /^\/(@[\w\.\d-]+)\/(transfers|curation-rewards|author-rewards|permissions|communities|password|settings|delegations)\/?$/, + UserProfile2: /^\/(@[\w\.\d-]+)\/(transfers|curation-rewards|author-rewards|permissions|communities|password|settings|delegations|proposals|witnesses)\/?$/, }; export default function resolveRoute(path) { diff --git a/src/app/ResolveRoute.test.js b/src/app/ResolveRoute.test.js index 53f69e70d..a6e2ef173 100644 --- a/src/app/ResolveRoute.test.js +++ b/src/app/ResolveRoute.test.js @@ -7,7 +7,7 @@ describe('routeRegex', () => { ['UserProfile1', /^\/(@[\w\.\d-]+)\/?$/], [ 'UserProfile2', - /^\/(@[\w\.\d-]+)\/(transfers|curation-rewards|author-rewards|permissions|communities|password|settings|delegations)\/?$/, + /^\/(@[\w\.\d-]+)\/(transfers|curation-rewards|author-rewards|permissions|communities|password|settings|delegations|proposals|witnesses)\/?$/, ], ]; diff --git a/src/app/assets/stylesheets/_themes.scss b/src/app/assets/stylesheets/_themes.scss index 75164e68f..0b13dc2ac 100755 --- a/src/app/assets/stylesheets/_themes.scss +++ b/src/app/assets/stylesheets/_themes.scss @@ -9,6 +9,8 @@ $themes: ( backgroundColorOpaque: $color-background-off-white, backgroundTransparent: transparent, backgroundColorWarning: $color-background-warning, + backgroundColorDanger: $alert-color, + backgroundColorSecondary: $color-white, moduleBackgroundColor: $color-white, menuBackgroundColor: $color-background-dark, moduleMediumBackgroundColor: $color-white, @@ -20,6 +22,7 @@ $themes: ( borderDark: 1px solid $color-text-gray, borderAccent: 1px solid $color-blue, borderWarning: 1px solid $color-text-warning, + borderDanger: 1px solid $alert-color, borderTransparent: transparent, roundedCorners: 5px, roundedCornersTop: 5px 5px 0 0, @@ -30,6 +33,7 @@ $themes: ( textColorAccent: $color-text-blue, textColorAccentHover: $color-blue-original-dark, textColorError: $color-text-red, + textColorOnDanger: $color-white, contentBorderAccent: $color-transparent, buttonBackground: $color-blue-original-dark, buttonBackgroundHover: $color-blue-original-light, @@ -48,6 +52,8 @@ $themes: ( backgroundColorOpaque: $color-background-off-white, backgroundTransparent: transparent, backgroundColorWarning: $color-background-warning, + backgroundColorDanger: $alert-color, + backgroundColorSecondary: $color-white, moduleBackgroundColor: $color-white, menuBackgroundColor: $color-background-dark, moduleMediumBackgroundColor: $color-transparent, @@ -59,6 +65,7 @@ $themes: ( borderDark: 1px solid $color-text-gray, borderAccent: 1px solid $color-teal, borderWarning: 1px solid $color-text-warning, + borderDanger: 1px solid $alert-color, borderTransparent: transparent, roundedCorners: 5px, roundedCornersTop: 5px 5px 0 0, @@ -69,6 +76,7 @@ $themes: ( textColorAccent: $color-text-teal, textColorAccentHover: $color-teal, textColorError: $color-text-red, + textColorOnDanger: $color-white, contentBorderAccent: $color-teal, buttonBackground: $color-blue-black, buttonBackgroundHover: $color-teal, @@ -87,6 +95,8 @@ $themes: ( backgroundColorEmphasis: $color-background-super-dark, backgroundColorOpaque: $color-blue-dark, backgroundColorWarning: $color-background-warning-dark, + backgroundColorDanger: $alert-color, + backgroundColorSecondary: $color-blue-dark, moduleBackgroundColor: $color-background-dark, backgroundTransparent: transparent, menuBackgroundColor: $color-blue-dark, @@ -99,6 +109,7 @@ $themes: ( borderDark: 1px solid $color-text-gray-light, borderAccent: 1px solid $color-teal, borderWarning: 1px solid $color-background-warning-dark, + borderDanger: 1px solid $alert-color, borderTransparent: transparent, roundedCorners: 5px, roundedCornersTop: 5px 5px 0 0, @@ -109,6 +120,7 @@ $themes: ( textColorAccent: $color-teal, textColorAccentHover: $color-teal-light, textColorError: $color-text-red, + textColorOnDanger: $color-white, contentBorderAccent: $color-teal, buttonBackground: $color-white, buttonBackgroundHover: $color-teal, diff --git a/src/app/client_config.js b/src/app/client_config.js index 79361d549..190d7b289 100644 --- a/src/app/client_config.js +++ b/src/app/client_config.js @@ -51,3 +51,7 @@ export const SITE_DESCRIPTION = // various export const SUPPORT_EMAIL = 'support@' + APP_DOMAIN; + +// External links +export const STEEMDB_BLOCK_URL = "https://steemdb.io/block"; +export const STEEMDB_TRANSACTION_URL = "https://steemdb.io/tx"; \ No newline at end of file diff --git a/src/app/components/all.scss b/src/app/components/all.scss index 1b3a9a319..15c5e2d55 100644 --- a/src/app/components/all.scss +++ b/src/app/components/all.scss @@ -29,6 +29,8 @@ @import './elements/OutgoingDelegations'; @import './elements/VotersModal'; @import './elements/ProposalCreatorModal'; +@import './elements/RouteSettings'; +@import './elements/WithdrawRoutesModal'; // modules @import './modules/Header/styles'; diff --git a/src/app/components/cards/TransferHistoryRow/index.jsx b/src/app/components/cards/TransferHistoryRow/index.jsx index c2056831a..5b97f50fd 100644 --- a/src/app/components/cards/TransferHistoryRow/index.jsx +++ b/src/app/components/cards/TransferHistoryRow/index.jsx @@ -6,12 +6,15 @@ import Memo from 'app/components/elements/Memo'; import { numberWithCommas, vestsToSp } from 'app/utils/StateFunctions'; import tt from 'counterpart'; import GDPRUserList from 'app/utils/GDPRUserList'; +import { STEEMDB_BLOCK_URL, STEEMDB_TRANSACTION_URL } from 'app/client_config'; class TransferHistoryRow extends React.Component { render() { const { op, context, + block, + trx, curation_reward, author_reward, benefactor_reward, @@ -287,8 +290,14 @@ class TransferHistoryRow extends React.Component { } return ( - + + {block && (
+ Block: {block} +
)} + {(trx && trx !== '0000000000000000000000000000000000000000') && (
+ TxID: {trx} +
)} { + const { from, to, percentage, asset } = operation; + + const getAssetLabel = (isAutoVest) => { + return isAutoVest ? tt('advanced_routes.steem_power') : tt('advanced_routes.steem'); + }; + + return ( +
+
+ + {tt('advanced_routes.from')} + + +
+
+ + {tt('advanced_routes.to')} + + +
+
+ + {tt('advanced_routes.percentage')} + + + +
+
+ ); +}; + +ConfirmWithdrawVestingRoute.propTypes = { + operation: PropTypes.shape({ + from: PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + percentage: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + asset: PropTypes.bool.isRequired, + }).isRequired, +}; + +export default ConfirmWithdrawVestingRoute; diff --git a/src/app/components/elements/ConversionsModal.jsx b/src/app/components/elements/ConversionsModal.jsx index b6e06b8e0..d93f9cff4 100644 --- a/src/app/components/elements/ConversionsModal.jsx +++ b/src/app/components/elements/ConversionsModal.jsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactModal from 'react-modal'; import CloseButton from 'app/components/elements/CloseButton'; +import tt from 'counterpart'; ReactModal.defaultStyles.overlay.backgroundColor = 'rgba(0, 0, 0, 0.6)'; @@ -9,22 +10,22 @@ const ConversionsModal = ({ isOpen, onClose, combinedConversions }) => {
-

Conversions

+

{tt('converttosteem_jsx.current_conversions')}

{combinedConversions.length === 0 ? ( -

No conversion data available.

+

{tt('converttosteem_jsx.no_conversion_data')}

) : ( - - - - + + + + diff --git a/src/app/components/elements/ConvertToSteem.jsx b/src/app/components/elements/ConvertToSteem.jsx index cf232d936..54bb851a2 100644 --- a/src/app/components/elements/ConvertToSteem.jsx +++ b/src/app/components/elements/ConvertToSteem.jsx @@ -22,6 +22,7 @@ class ConvertToSteem extends React.Component { this.shouldComponentUpdate = shouldComponentUpdate(this, 'ConvertToSteem'); this.state = { toggle_check: false, + errorMessage: undefined, }; this.initForm(props); } @@ -73,10 +74,10 @@ class ConvertToSteem extends React.Component { this.setState({ loading: false }); if (onClose) onClose(); }; - const error = () => { - this.setState({ loading: false }); + const error = (msg_error) => { + this.setState({ loading: false, errorMessage: String(msg_error) }); }; - this.setState({ loading: true }); + this.setState({ errorMessage: undefined, loading: true }); convert(currentUser, amount.value, success, error); }; @@ -103,7 +104,7 @@ class ConvertToSteem extends React.Component { render() { const { onClose, currentUser, sbd_balance } = this.props; - const { loading, amount, marketRate } = this.state; + const { loading, amount, marketRate, errorMessage } = this.state; const { submitting, valid, handleSubmit } = this.state.convertToSteem; const { prices } = this.props; @@ -276,6 +277,13 @@ class ConvertToSteem extends React.Component { ) : null} + {errorMessage && ( +
+
+ {errorMessage} +
+
+ )}
diff --git a/src/app/components/elements/OutgoingDelegations.jsx b/src/app/components/elements/OutgoingDelegations.jsx index 961077570..21c2daecc 100644 --- a/src/app/components/elements/OutgoingDelegations.jsx +++ b/src/app/components/elements/OutgoingDelegations.jsx @@ -319,14 +319,12 @@ class OutgoingDelegations extends React.Component { type="text" id="delegatee" name="delegatee" + placeholder={tt( + 'outgoingdelegations_jsx.filters.search_delegatee' + )} value={delegatee} onChange={this.handleFindAccounts} /> -
diff --git a/src/app/components/elements/OutgoingDelegations.scss b/src/app/components/elements/OutgoingDelegations.scss index 1596d53c2..fe5b6e1b7 100644 --- a/src/app/components/elements/OutgoingDelegations.scss +++ b/src/app/components/elements/OutgoingDelegations.scss @@ -2,11 +2,20 @@ width: 100%; font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; - border: 1px solid #7e7d7d; outline: none; padding: 0.25rem 0.5rem 0.5rem 0; border-radius: 5px; padding: 1.5rem .5rem; + border-color: transparent; + background: transparent; + box-shadow: 0 0 5px rgba(109, 207, 246, 0.5); + @include themify($themes) { + color: themed('textColorPrimary'); + } + input::placeholder { + font-weight: lighter; + color: #7e7d7d; + } } .input-box{ @@ -16,30 +25,5 @@ .input-box input:focus{ border: 1px solid #06D6A9; -} - -.input-box label { - transform: translateX(10px) translateY(-3rem); - left: 0; - padding: 1rem; - padding-top: 0.5rem; - pointer-events: none; - font-size: 1rem; - color: #7e7d7d; - transition: 0.5s; - margin-right: 15px; - max-width: fit-content; -} - -.input-box input:focus ~ label, -.input-box .focus ~ label{ - transform: translateX(10px) translateY(-3.95rem); - font-size: .75rem; - padding: .45rem; - padding-top: .1rem; - padding-bottom: .1rem; - background-color: #06D6A9; - font-weight: bold; - color: #FFFFFF; - border-radius: 5px; + background: transparent; } diff --git a/src/app/components/elements/RouteSettings.jsx b/src/app/components/elements/RouteSettings.jsx index a14535840..8bad6ebe5 100644 --- a/src/app/components/elements/RouteSettings.jsx +++ b/src/app/components/elements/RouteSettings.jsx @@ -1,25 +1,45 @@ import React from 'react'; +import reactForm from 'app/utils/ReactForm'; import { connect } from 'react-redux'; import tt from 'counterpart'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; import * as transactionActions from 'app/redux/TransactionReducer'; import * as userActions from 'app/redux/UserReducer'; import * as globalActions from 'app/redux/GlobalReducer'; import Icon from 'app/components/elements/Icon'; +import { FormattedHTMLMessage } from 'app/Translator'; +import { validate_account_name } from 'app/utils/ChainValidation'; +import ConfirmWithdrawVestingRoute from 'app/components/elements/ConfirmWithdrawVestingRoute'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import { api } from '@steemit/steem-js'; class RouteSettings extends React.Component { constructor(props) { super(props); + this.shouldComponentUpdate = shouldComponentUpdate(this, 'RouteSettings'); this.state = { - broadcasting: false, errorMessage: undefined, - proxyAccount: '', - percentage: 100, - autoVest: false, + remainingPercentage: 100, + maxWithdrawRoutes: 10, }; + this.initForm(props); } - componentDidMount() { + async componentWillMount() { this.updateRemainingPercentage(this.props.withdraw_routes); + await this.fetchConfig() + } + + fetchConfig() { + api.callAsync('database_api.get_config', {}) + .then(res => { + this.setState({ + maxWithdrawRoutes: res.STEEM_MAX_WITHDRAW_ROUTES + }); + }) + .catch(err => { + console.error('Error fetching config:', err); + }); } componentDidUpdate(prevProps) { @@ -32,180 +52,337 @@ class RouteSettings extends React.Component { if (!routes) return; const totalRoutedPercentage = routes.reduce((total, route) => total + route.percent, 0); const remainingPercentage = 100 - (totalRoutedPercentage / 100); - this.setState({ percentage: remainingPercentage }); + this.setState({ remainingPercentage }); } removeWithdrawRoute = (toAccount) => { - this.setState({ broadcasting: true, errorMessage: undefined }); + this.setState({ loading: true, errorMessage: undefined }); const { account, setWithdrawVestingRoute } = this.props; - - // To remove a route, you set the percent to 0 setWithdrawVestingRoute({ account, proxy: toAccount, percent: 0, autoVest: false, - successCallback: () => this.setState({ broadcasting: false }), - errorCallback: (error) => this.setState({ broadcasting: false, errorMessage: String(error) }), + successCallback: () => this.setState({ loading: false }), + errorCallback: (error) => this.setState({ loading: false, errorMessage: String(error) }), }); }; - setWithdrawRoute = (e) => { - e.preventDefault(); - this.setState({ broadcasting: true, errorMessage: undefined }); - + setWithdrawRoute = () => { + this.setState({ loading: true, errorMessage: undefined }); const { account, setWithdrawVestingRoute, hideModal } = this.props; - const { proxyAccount, percentage, autoVest } = this.state; + const { to, percentage, asset } = this.state; setWithdrawVestingRoute({ account, - proxy: proxyAccount, - percent: Math.round(percentage * 100), // API expects percentage * 100 - autoVest, + proxy: to.value, + percent: Math.round(percentage.value * 100), // API expects percentage * 100 + autoVest: asset.value === 'SP', successCallback: () => { - this.setState({ broadcasting: false }); - hideModal(); // Close modal on success + this.setState({ loading: false }); + to.props.onChange('') + percentage.props.onChange(0) + // hideModal(); }, errorCallback: (error) => { - this.setState({ broadcasting: false, errorMessage: String(error) }); + this.setState({ loading: false, errorMessage: String(error) }); }, }); }; - handleInputChange = (event) => { - const { name, value } = event.target; - this.setState({ [name]: value }); + validatePercentage = (percentage, remainingPercentage, to) => { + const { withdraw_routes } = this.props; + const withdrawRoutes = withdraw_routes && withdraw_routes.length > 0 + ? withdraw_routes + : []; + if (remainingPercentage <= 0) { + return null + } + if (!percentage || isNaN(percentage)) { + return 'Percentage is required and must be a number.'; + } + const percentageFloat = parseFloat(percentage); + if (percentageFloat <= 0 || percentageFloat > 100) { + return `Percentage must be greater than 0 and less than or equal to ${100}.`; + } + const existingRoute = withdrawRoutes.find(route => route.to_account === to); + if (existingRoute) { + const updatedRemaining = remainingPercentage + (existingRoute.percent / 100); + if (percentageFloat > updatedRemaining) { + return `Only ${updatedRemaining}% is left.`; + } + } else { + if (percentageFloat > remainingPercentage) { + return `Only ${remainingPercentage}% is left.`; + } + } + return null; + } + + onChangeTo = async value => { + const cleanValue = value.replace(/\s+/g, ''); + this.state.to.props.onChange(cleanValue); }; - - handleCheckboxChange = (event) => { - const { name, checked } = event.target; - this.setState({ [name]: checked }); + + initForm(props) { + const fields = ['percentage', 'to','asset']; + const validate = values => { + const { remainingPercentage } = this.state; + return { + percentage: this.validatePercentage(values.percentage, remainingPercentage, values.to ), + to: validate_account_name(values.to), + }; + }; + reactForm({ + name: 'routeSettings', + instance: this, + fields, + initialValues: { percentage: 0, to: '', asset: 'STEEM' }, + validation: validate, + }); } render() { - if (!this.props.account) return null; // Render nothing if account data is not available yet + if (!this.props.account) return null; + const { remainingPercentage, maxWithdrawRoutes, errorMessage, to, percentage, asset, loading } = this.state; + const { withdraw_routes, hideModal, account } = this.props; + const { valid, handleSubmit, submitting } = this.state.routeSettings; - const { broadcasting, errorMessage, proxyAccount, percentage, autoVest } = this.state; - const { withdraw_routes, hideModal } = this.props; + const sortedRoutes = withdraw_routes && withdraw_routes.length > 0 + ? [...withdraw_routes].sort((a, b) => b.percent - a.percent) + : []; - let totalRoutedPercentage = 0; - const currentRoutesList = withdraw_routes && withdraw_routes.map(route => { - totalRoutedPercentage += route.percent; + const currentRoutesList = sortedRoutes.map(route => { return ( -
-
-
@{route.to_account}
-
{route.percent / 100}% of power down
-
-
- -
-
+
+ + + + + ); }); - - const remainingPercentage = 100 - (totalRoutedPercentage / 100); + const remainingRoutes = maxWithdrawRoutes - sortedRoutes.length const currentRoutes = ( -
-

Current Withdraw Routes

-

Your active power down routing configurations.

-
- {withdraw_routes && withdraw_routes.length > 0 ? currentRoutesList :

No withdraw routes are set.

} -
- {withdraw_routes && withdraw_routes.length > 0 && ( -
-
- Total routed: {totalRoutedPercentage / 100}% -
-
- Remaining to you: {remainingPercentage}% -
+
+
{tt('advanced_routes.current_routes', { accounts_number: (remainingRoutes) })}
+ + {sortedRoutes && sortedRoutes.length > 0 ? ( +
+
IDRequest IDAmountDate{tt('converttosteem_jsx.id')}{tt('converttosteem_jsx.request_id')}{tt('g.amount')}{tt('g.date')}
+ + {route.to_account} + + {route.percent / 100}%{route.auto_vest ? tt('advanced_routes.steem_power') : tt('advanced_routes.steem')} +
this.removeWithdrawRoute(route.to_account)} + title={tt('g.remove')} + disabled={loading} + > + + × + +
+
+ + + + + + + + + + { + + + + + } + {currentRoutesList} + +
{tt('advanced_routes.account')}{tt('advanced_routes.percentage')}{tt('advanced_routes.receive')}{tt('advanced_routes.remove')}
+ + {account} + + {remainingPercentage}%{tt('advanced_routes.steem')} +
+ ) : ( +

{tt('advanced_routes.no_routes')}

)} ); + return ( -
+
-

{tt('userwallet_jsx.advanced_routes')}

+

+ {tt('userwallet_jsx.set_advanced_routes')} +

- - {currentRoutes} -
- -

Add New Route

-
-
- - - +
{ + this.setWithdrawRoute(); + })}> +
+
+
+ +
+
+
-
- -
-
-
-

Withdraw Route Information:

-
    -
  • This only sets up routing rules for future power downs.
  • -
  • You must still initiate a power down separately.
  • -
  • Auto-vest converts payments directly to STEEM Power.
  • -
+
+
+ {tt('advanced_routes.from')} +
+
+
+ @ + +
- {errorMessage &&

{errorMessage}

}
-
+
+
+ {tt('advanced_routes.to')} +
+
+
+ @ + { + await this.onChangeTo(e.target.value); + }} + /> +
+
+ {(to && to.touched && to.error) ? ( +
+ {to && + to.touched && + to.error && + to.error}  +
+ ) : null} +
+
+
+ {tt('advanced_routes.percentage')} +
+
+
+ + {asset && asset.value && ( + + + + )} +
+
+ {(percentage && percentage.touched && percentage.error) ? ( +
+ {percentage && + percentage.touched && + percentage.error && + percentage.error}  +
+ ) : null} -
-
- - + {(remainingRoutes <= 0 || remainingPercentage <= 0) && (
+ {remainingRoutes <= 0 && tt('advanced_routes.not_remaining_routes')} + {(remainingRoutes <= 0 && remainingPercentage <= 0) &&
} + {remainingPercentage <= 0 && tt('advanced_routes.not_remaining_percentage')} +
)}
-
+ {currentRoutes} + {errorMessage && ( +
+
+ {errorMessage} +
+
+ )} +
+
+ {loading && ( + + +
+
+ )} + {!loading && ( + + + + )} +
+
+
); } @@ -218,11 +395,11 @@ export default connect( const accountName = values.get('account'); const routes = state.user.get('withdraw_routes'); - + const withdraw_routes = routes && routes.toJS ? routes.toJS() : []; - return { - ...ownProps, + return { + ...ownProps, account: accountName, withdraw_routes, }; @@ -244,6 +421,15 @@ export default connect( dispatch(userActions.getWithdrawRoutes({ account })); if (successCallback) return successCallback(...args); }; + const confirm = () => ( + + ); const isRemove = percent === 0; dispatch( transactionActions.broadcastOperation({ @@ -254,12 +440,7 @@ export default connect( percent: percent, auto_vest: autoVest, }, - confirm: isRemove - ? tt('g.are_you_sure') - : tt('userwallet_jsx.confirm_route_setup', { - percent: percent / 100, - account: proxy, - }), + confirm, successCallback: successCallbackWrapper, errorCallback, }) diff --git a/src/app/components/elements/RouteSettings.scss b/src/app/components/elements/RouteSettings.scss new file mode 100644 index 000000000..cd5ff254a --- /dev/null +++ b/src/app/components/elements/RouteSettings.scss @@ -0,0 +1,25 @@ +.flex-container-1 { + -ms-flex: 0 0 16.66667%; + flex: 0 0 16.66667%; + max-width: 16.66667%; +} + +.flex-container-2 { + -ms-flex: 0 0 83.33333%; + flex: 0 0 83.33333%; + max-width: 83.33333%; +} + +@media (max-width: 425px) { + .flex-container-1 { + -ms-flex: 0 0 33.33333%; + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + + .flex-container-2 { + -ms-flex: 0 0 66.66667%; + flex: 0 0 66.66667%; + max-width: 66.66667%; + } +} diff --git a/src/app/components/elements/VotersModal.jsx b/src/app/components/elements/VotersModal.jsx index 0df564ef0..cad6c639d 100644 --- a/src/app/components/elements/VotersModal.jsx +++ b/src/app/components/elements/VotersModal.jsx @@ -183,7 +183,7 @@ class VotersModal extends React.Component { }; handleResize = () => { - this.setState({ isSmallScreen: window.innerWidth <= 425 }); + this.setState({ isSmallScreen: window.innerWidth <= 650 }); }; updateVotersAccount = res => { @@ -260,7 +260,7 @@ class VotersModal extends React.Component {
open_modal} + onAfterOpen={open_modal} onRequestClose={close_modal} className={ nightmodeEnabled @@ -330,7 +330,7 @@ class VotersModal extends React.Component { />
-
+
{isSmallScreen ? ( - - {userInfo.name} - + {`${tt('proposals.voters.account')}: `} + {userInfo.name} + - {this.proxyVoteStyle( - numberWithCommas( - userInfo.steemPower - ) - )} + {`${tt('proposals.voters.own_sp')}: `} + {this.proxyVoteStyle(numberWithCommas(userInfo.steemPower), ' SP')} + - {this.proxyVoteStyle( - numberWithCommas(userInfo.proxySP) - )} + {`${tt('proposals.voters.proxy_sp')}: `} + {this.proxyVoteStyle(numberWithCommas(userInfo.proxySP), ' SP')} diff --git a/src/app/components/elements/Voting.scss b/src/app/components/elements/Voting.scss index 8465e6429..fd5f3c290 100644 --- a/src/app/components/elements/Voting.scss +++ b/src/app/components/elements/Voting.scss @@ -377,3 +377,46 @@ margin: 0 auto; padding-bottom: 2px; } + +// /* =========================== +// Voting buttons: DISABLED ONLY +// =========================== */ + +// .Voting__button-up.disabled:not(.Voting__button--upvoted) { +// path { +// @include themify($themes) { fill: #a5a5a5; } +// } +// circle { +// @include themify($themes) { +// fill: transparent; +// stroke: #a5a5a5; +// } +// } +// a { pointer-events: none; } +// & a:hover { +// path { @include themify($themes) { fill: #a5a5a5; } } +// circle { @include themify($themes) { fill: transparent; stroke: #a5a5a5; } } +// } +// } + +// .Voting__button--upvoted.disabled { + +// circle { +// @include themify($themes) { +// fill: #a5a5a5; +// stroke: #a5a5a5; +// } +// } + +// path { fill: #000; } +// a { pointer-events: none; } +// & a:hover { +// path { fill: #000; } +// circle { +// @include themify($themes) { +// fill: #a5a5a5; +// stroke: #a5a5a5; +// } +// } +// } +// } diff --git a/src/app/components/elements/WalletSubMenu.jsx b/src/app/components/elements/WalletSubMenu.jsx index 4ef46580c..e59690755 100644 --- a/src/app/components/elements/WalletSubMenu.jsx +++ b/src/app/components/elements/WalletSubMenu.jsx @@ -51,6 +51,22 @@ export default ({ accountname, isMyAccount, showTab }) => { ) : null} +
  • + + {tt('navigation.witnesses')} + +
  • +
  • + + {tt('g.proposals')} + +
  • ); }; diff --git a/src/app/components/elements/WithdrawRoutesModal.jsx b/src/app/components/elements/WithdrawRoutesModal.jsx new file mode 100644 index 000000000..65a85e04c --- /dev/null +++ b/src/app/components/elements/WithdrawRoutesModal.jsx @@ -0,0 +1,39 @@ + +import React from 'react'; +import ReactModal from 'react-modal'; +import CloseButton from 'app/components/elements/CloseButton'; +import tt from 'counterpart'; +import WithdrawRoutesTable from 'app/components/elements/WithdrawRoutesTable'; + +ReactModal.defaultStyles.overlay.backgroundColor = 'rgba(0, 0, 0, 0.6)'; + +class WithdrawRoutesModal extends React.Component { + render() { + const { isOpen, onClose, routes, accountName, steemPower } = this.props; + + return ( + + +
    +

    {tt('advanced_routes.current_withdraw_route')}

    + {routes.length === 0 ? ( +

    {tt('userwallet_jsx.no_withdraw_routes')}

    + ) : ( + + )} +
    +
    + ); + } +} + +export default WithdrawRoutesModal; diff --git a/src/app/components/elements/WithdrawRoutesModal.scss b/src/app/components/elements/WithdrawRoutesModal.scss new file mode 100644 index 000000000..a83212697 --- /dev/null +++ b/src/app/components/elements/WithdrawRoutesModal.scss @@ -0,0 +1,57 @@ +.ContainerModal__content, +.ContainerModal__content--night { + position: absolute; + min-width: 300px; + box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.1), 7px 7px 0 0 #06D6A9; + border-radius: 0; + border: transparent; + transition: 0.2s all ease-in-out; + min-height: 500px; + top: 50%; + left: 50%; + right: auto; + bottom: auto; + transform: translate(-50%, -45%); + overflow-y: auto; + background-color: #ffffff; + color: #000000; + padding: 1rem; + margin-top: .5rem; +} + +.ContainerModal__content .header, +.ContainerModal__content--night { + max-width: 75rem; + margin-right: auto; + margin-left: auto; +} + +.ContainerModal__content--night { + background-color: #2c3136; + color: #ffffff; +} + +@media screen and (max-width: 39.9375em) { + .reveal { + width: 100%; + max-width: none; + height: 100vh; + min-height: 100vh; + margin-left: 0; + } +} + +@media print, +screen and (min-width: 40em) { + .ContainerModal__content { + width: 600px; + max-width: 75rem; + } +} + +@media print, +screen and (min-width: 1000px) { + .ContainerModal__content { + width: 680px; + } +} diff --git a/src/app/components/elements/WithdrawRoutesTable.jsx b/src/app/components/elements/WithdrawRoutesTable.jsx new file mode 100644 index 000000000..1df271152 --- /dev/null +++ b/src/app/components/elements/WithdrawRoutesTable.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import tt from 'counterpart'; + +class WithdrawRoutesTable extends React.Component { + render() { + const { routes, accountName, steemPower } = this.props; + + const totalPercent = routes.reduce((acc, r) => acc + r.percent, 0); + const remainingPercent = 10000 - totalPercent; + + return ( +
    + + + + + + {steemPower && } + + + + + + + {steemPower && } + + {routes.map((route) => ( + + + + {steemPower && ( + + )} + + ))} + +
    {tt('advanced_routes.account')}{tt('advanced_routes.percent')}{tt('advanced_routes.receive_amount')}
    + + {accountName} + + {remainingPercent / 100}%{`${(remainingPercent / 10000 * parseFloat(steemPower)).toFixed(3)} ${tt('advanced_routes.steem')}`}
    + + {route.to_account} + + {route.percent / 100}% + {`${(route.percent / 10000 * parseFloat(steemPower)).toFixed(3)} ${route.auto_vest ? 'SP' : tt('advanced_routes.steem')}`} +
    +
    + ); + } +} + +export default WithdrawRoutesTable; diff --git a/src/app/components/modules/AuthorRewards.jsx b/src/app/components/modules/AuthorRewards.jsx index 28ef0defb..5dec78585 100644 --- a/src/app/components/modules/AuthorRewards.jsx +++ b/src/app/components/modules/AuthorRewards.jsx @@ -75,6 +75,8 @@ class AuthorRewards extends React.Component { .map((item, index) => { // Filter out rewards if (item[1].op[0] === 'author_reward') { + const trx_id = item[1].trx_id + const block_id = item[1].block if (!finalDate) { finalDate = new Date(item[1].timestamp).getTime(); } @@ -111,6 +113,8 @@ class AuthorRewards extends React.Component { ); diff --git a/src/app/components/modules/CurationRewards.jsx b/src/app/components/modules/CurationRewards.jsx index fa6da5b51..2ab4173b0 100644 --- a/src/app/components/modules/CurationRewards.jsx +++ b/src/app/components/modules/CurationRewards.jsx @@ -72,6 +72,8 @@ class CurationRewards extends React.Component { .map((item, index) => { // Filter out rewards if (item[1].op[0] === 'curation_reward') { + const trx_id = item[1].trx_id + const block_id = item[1].block if (!finalDate) { finalDate = new Date(item[1].timestamp).getTime(); } @@ -88,6 +90,8 @@ class CurationRewards extends React.Component { totalRewards += vest; return ( { + if (!routes) return; + const totalRoutedPercentage = routes.reduce((total, route) => total + route.percent, 0); + const remainingPercentage = 100 - (totalRoutedPercentage / 100); + this.setState({ remainingPercentage }); + } + render() { - const { broadcasting, new_withdraw, manual_entry } = this.state; + const { broadcasting, new_withdraw, manual_entry, remainingPercentage, toggleAckRoutes } = this.state; const { account, available_shares, @@ -46,7 +65,61 @@ class Powerdown extends React.Component { to_withdraw, vesting_shares, delegated_vesting_shares, + withdraw_routes } = this.props; + + const sortedRoutes = withdraw_routes && withdraw_routes.length > 0 + ? [...withdraw_routes].sort((a, b) => b.percent - a.percent) + : []; + const hasRoutes = sortedRoutes.length > 0; + const currentRoutesList = sortedRoutes.map(route => { + const receive = (route.percent / 10000 * parseFloat(vestsToSp(this.props.state, new_withdraw))).toFixed(3) + return ( + + + + {route.to_account} + + + {route.percent / 100}% + {`${receive} ${route.auto_vest ? 'SP' : tt('advanced_routes.steem')}`} + + ); + }); + + const currentRoutes = ( +
    +
    {tt('advanced_routes.current_withdraw_route')}
    + {hasRoutes ? ( +
    + + + + + + + + + + { + + + + } + {currentRoutesList} + +
    {tt('advanced_routes.account')}{tt('advanced_routes.percentage')}{tt('advanced_routes.receive_amount')}
    + + {account} + + {remainingPercentage}%{`${(remainingPercentage / 100 * parseFloat(vestsToSp(this.props.state, new_withdraw))).toFixed(3)} ${tt('advanced_routes.steem')}`}
    +
    + ) : ( +

    {tt('advanced_routes.no_routes')}

    + )} +
    + ); + const formatSp = amount => numberWithCommas(vestsToSp(this.props.state, amount)); const sliderChange = value => { @@ -173,11 +246,35 @@ class Powerdown extends React.Component { {LIQUID_TICKER}

      {notes}
    +
    +
    + {currentRoutes} +
    +
    + {hasRoutes && ( +
    +
    + + {tt('advanced_routes.acknowledge_routes')} + + +
    +
    + )} @@ -204,6 +301,10 @@ export default connect( const available_shares = vesting_shares - to_withdraw - withdrawn - delegated_vesting_shares; + const routes = state.user.get('withdraw_routes'); + + const withdraw_routes = routes && routes.toJS ? routes.toJS() : []; + return { ...ownProps, account, @@ -213,6 +314,7 @@ export default connect( to_withdraw, vesting_shares, withdrawn, + withdraw_routes, }; }, // mapDispatchToProps diff --git a/src/app/components/modules/ProposalList/Proposal.jsx b/src/app/components/modules/ProposalList/Proposal.jsx index 77e9e18af..db4c31f27 100644 --- a/src/app/components/modules/ProposalList/Proposal.jsx +++ b/src/app/components/modules/ProposalList/Proposal.jsx @@ -45,6 +45,22 @@ export default class Proposal extends React.Component { this.setState({ show_remove_proposal_modal: show }); }; + isNonEmptyString = (v) => { + return typeof v === 'string' && v.trim().length > 0; + } + + isSameAccount= (a, b) => { + try { + return ( + this.isNonEmptyString(a) && + this.isNonEmptyString(b) && + a.trim().toLowerCase() === b.trim().toLowerCase() + ); + } catch (error) { + return false; + } + } + render() { const { id, @@ -66,8 +82,11 @@ export default class Proposal extends React.Component { triggerModal, getNewId, paid_proposals, + walletSectionAccount, } = this.props; + const canChangeVote = !walletSectionAccount || this.isSameAccount(currentUser, walletSectionAccount); + const { show_remove_proposal_modal } = this.state; let isMyAccount; if (currentUser) { @@ -190,14 +209,34 @@ export default class Proposal extends React.Component { > {abbreviateNumber(votesToSP)}
    - + {canChangeVote ? ( + + + + ) : ( - + )} {isMyAccount && (
    +
    0) { + last_proposal = proposals[0]; + } + this.setState({ + proposals, + loading: false, + last_proposal, + limit, + }); + } + + onFilterProposals = async status => { + this.setState({ status }); + await this.load(false, { status }); + }; + + onOrderProposals = async order_by => { + this.setState({ order_by }); + await this.load(false, { order_by }); + }; + + onOrderDirection = async order_direction => { + this.setState({ order_direction }); + await this.load(false, { order_direction }); + }; + + getVotersAccounts = voters_accounts => { + this.setState({ voters_accounts }); + }; + + getVoters = (voters, lastVoter) => { + this.setState({ voters, lastVoter }); + }; + + getNewId = new_id => { + this.setState({ new_id }); + }; + + setIsVotersDataLoading = is_voters_data_loaded => { + this.setState({ is_voters_data_loaded }); + }; + setPaidProposals = paid_proposals => { + this.setState({ paid_proposals }); + }; + + getAllProposals( + last_proposal, + order_by, + order_direction, + limit, + status, + start + ) { + return this.props.listProposals({ + voter_id: this.props.walletSectionAccount || this.props.currentUser, + last_proposal, + order_by, + order_direction, + limit, + status, + start, + }); + } + + voteOnProposal = async (proposalId, voteForIt, onSuccess, onFailure) => { + return this.props.voteOnProposal( + this.props.currentUser, + [proposalId], + voteForIt, + async () => { + if (onSuccess) onSuccess(); + }, + () => { + if (onFailure) onFailure(); + } + ); + }; + + fetchGlobalProps() { + api.callAsync('condenser_api.get_dynamic_global_properties', []) + .then(res => + this.setState({ + total_vests: res.total_vesting_shares, + total_vest_steem: res.total_vesting_fund_steem, + }) + ) + .catch(err => console.log(err)); + } + + fetchVoters() { + this.fetchAllVotersWithPause({ + proposalId: this.state.new_id, + timeout: INITIAL_TIMEOUT, + maxToLoad: LOAD_ALL_VOTERS ? null : MAX_INITIAL_LOAD, + }) + .then(res => { + this.getVoters(res, ...res.slice(-1)); + }) + .catch(err => console.log(err)); + } + + fetchDataForVests() { + const voters = this.state.voters; + const new_id = this.state.new_id; + + const selected_proposal_voters = voters.filter( + v => v.proposal.proposal_id === new_id + ); + const voters_map = selected_proposal_voters.map(name => name.voter); + api.getAccountsAsync(voters_map) + .then(res => this.getVotersAccounts(res)) + .catch(err => console.log(err)); + } + + async getVotedProposals({ accountName, proposalIdsSet }) { + const votedMap = {}; + + try { + const result = await new Promise((resolve, reject) => { + api.callAsync( + 'database_api.list_proposal_votes', + { + start: [accountName], + limit: 1000, + order: 'by_voter_proposal', + order_direction: 'ascending', + status: 'all', + }, + (err, res) => { + if (err) reject(err); + else resolve(res); + } + ); + }); + + const votes = (result && result.proposal_votes) || []; + if (votes.length === 0 || votes[0].voter !== accountName) { + return votedMap; + } + + for (const vote of votes) { + if (vote.voter !== accountName) break; + const proposalId = vote.proposal.proposal_id; + if (proposalIdsSet.has(proposalId)) { + votedMap[proposalId] = true; + } + } + + return votedMap; + } catch (err) { + console.error('Error al obtener propuestas votadas:', err); + return votedMap; + } + } + + async updateProposalVotes(currentUser) { + if (typeof currentUser !== 'string' || currentUser.length <= 1) return; + + const { proposals } = this.state; + const proposalIdsSet = new Set(proposals.map(p => p.id)); + const votedMap = await this.getVotedProposals({ accountName: currentUser, proposalIdsSet }); + + const updatedProposals = proposals.map(p => ({ + ...p, + upVoted: !!votedMap[p.id], + })); + + this.setState({ proposals: updatedProposals }); + } + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + async fetchAllVotersWithPause({ + proposalId, + lastVoter = '', + accumulated = [], + timeout = INITIAL_TIMEOUT, + maxToLoad = null, + }) { + try { + const res = await new Promise((resolve, reject) => { + api.callAsync( + 'database_api.list_proposal_votes', + { + start: [proposalId, lastVoter], + limit: 1000, + order: 'by_proposal_voter', + order_direction: 'ascending', + status: 'active', + }, + (err, result) => { + if (err) reject(err); + else resolve(result); + } + ); + }); + + const votes = (res && res.proposal_votes) || []; + if (votes.length === 0) return accumulated; + const allVoters = accumulated.concat(votes); + if (maxToLoad && allVoters.length >= maxToLoad) { + return allVoters.slice(0, maxToLoad); + } + if (votes.length < 1000) { + return allVoters; + } + if (votes && votes.length >= 2) { + try { + const firstProposalId = votes[0].proposal.proposal_id; + const lastProposalId = votes.at(-1).proposal.proposal_id; + if ( + firstProposalId !== proposalId || + lastProposalId !== proposalId + ) { + return allVoters; + } + } catch (error) { + console.error(error); + } + } + const nextVoter = votes.at(-1) ? votes.at(-1).voter : undefined; + await this.delay(timeout); + const nextTimeout = Math.min(timeout + 250, MAX_TIMEOUT); + return this.fetchAllVotersWithPause({ + proposalId, + lastVoter: nextVoter, + accumulated: allVoters, + timeout: nextTimeout, + maxToLoad, + }); + } catch (err) { + console.error('Error al obtener votantes:', err); + return accumulated; + } + } + + onClickLoadMoreProposals = e => { + e.preventDefault(); + this.load(); + }; + + triggerCreatorsModal = () => { + this.setState({ + open_creators_modal: !this.state.open_creators_modal, + }); + }; + + triggerVotersModal = () => { + this.setState({ + open_voters_modal: !this.state.open_voters_modal, + }); + }; + + submitProposal = (proposal, onSuccess, onFailure) => { + this.props.createProposal( + this.props.currentUser || proposal.creator, + proposal.receiver, + proposal.startDate, + proposal.endDate, + `${parseFloat(proposal.dailyAmount).toFixed(3)} SBD`, + proposal.title, + proposal.permlink, + async () => { + this.triggerCreatorsModal(); + if (onSuccess) onSuccess(); + }, + () => { + if (onFailure) onFailure(); + } + ); + }; + + removeProposalById = id => { + this.setState(prevState => ({ + proposals: prevState.proposals.filter( + proposal => proposal.id !== id + ), + })); + }; + + render() { + const { + proposals, + loading, + status, + order_by, + order_direction, + voters, + voters_accounts, + open_creators_modal, + open_voters_modal, + total_vests, + total_vest_steem, + is_voters_data_loaded, + new_id, + } = this.state; + + const mergeVoters = [...voters]; + + const { nightmodeEnabled } = this.props; + + let showBottomLoading = false; + if (loading && proposals && proposals.length > 0) { + showBottomLoading = true; + } + const selected_proposal_voters = mergeVoters.filter( + v => v.proposal.proposal_id === new_id + ); + const voters_map = selected_proposal_voters.map(name => name.voter); // voter name + const accounts_map = []; + const acc_proxied_vests = []; + const proxies_name_by_voter = []; + const proxies_vote = {}; + voters_accounts.forEach(acc => { + accounts_map.push(acc.vesting_shares); + const proxied = acc.proxied_vsf_votes + .map(r => parseInt(r, 10)) + .reduce((a, b) => a + b, 0); + acc_proxied_vests.push(proxied); + proxies_name_by_voter.push(acc.proxy); + if (acc.proxy) { + proxies_vote[acc.proxy] = false; + } + }); + const steem_power = []; + const proxy_sp = []; + const total_sp = []; + let global_total_sp = 0; + + const calculatePowers = () => { + const total_vestsNew = parseFloat(total_vests.split(' ')[0]); + const total_vest_steemNew = parseFloat( + total_vest_steem.split(' ')[0] + ); + + for (let i = 0; i < accounts_map.length; i++) { + const vests_account = parseFloat(accounts_map[i].split(' ')[0]); + const vests_proxy = acc_proxied_vests[i]; + + const vesting_steem_account = + total_vest_steemNew * (vests_account / total_vestsNew); + const vesting_steem_proxy = + total_vest_steemNew * + (vests_proxy / total_vestsNew) * + 0.000001; + + const total = vesting_steem_account + vesting_steem_proxy; + + steem_power.push(vesting_steem_account); + proxy_sp.push(vesting_steem_proxy); + total_sp.push(total); + const voter = voters_map[i]; + if (Object.keys(proxies_vote).includes(voter)) { + proxies_vote[voter] = true; + } + global_total_sp += total; + } + }; + calculatePowers(); + const simpleVotesToSp = total_votes => { + const total_vestsNew = parseFloat(total_vests.split(' ')[0]); + const total_vest_steemNew = parseFloat( + total_vest_steem.split(' ')[0] + ); + return ( + total_vest_steemNew * + (total_votes / total_vestsNew) * + 0.000001 + ).toFixed(2); + }; + const pro_aux = proposals.find(p => p.proposal_id === new_id); + let total_votes_aux = 0; + if (pro_aux && pro_aux.total_votes) { + total_votes_aux = simpleVotesToSp(pro_aux.total_votes); + } + const total_acc_sp_obj = {}; + voters_map.forEach((voter, i) => { + const proxy_name = proxies_name_by_voter[i]; + const proxy_vote = proxies_vote[proxy_name] || false; + const influence = total_votes_aux + ? (total_sp[i] / total_votes_aux) * 100 + : 0; + total_acc_sp_obj[voter] = [ + total_sp[i], + steem_power[i], + proxy_sp[i], + proxy_name, + proxy_vote, + influence, + ]; + }); + const sort_merged_total_sp = []; + for (const value in total_acc_sp_obj) { + sort_merged_total_sp.push([value, ...total_acc_sp_obj[value]]); + } + sort_merged_total_sp.sort((a, b) => b[1] - a[1]); + + return ( +
    + + + +
    + {!loading ? ( + + {tt('proposals.load_more')} + + ) : null} + + {showBottomLoading ? ( + {tt('proposals.loading')} + ) : null} +
    +
    + ); + } +} + +Proposals.propTypes = { + listProposals: PropTypes.func.isRequired, + createProposal: PropTypes.func.isRequired, + voteOnProposal: PropTypes.func.isRequired, +}; + +export default connect( + state => { + const user = state.user.get('current'); + const currentUser = user && user.get('username'); + const proposals = state.proposal.get('proposals', List()); + const last = proposals.size - 1; + const last_id = + (proposals.size && proposals.get(last).get('id')) || null; + const newProposals = + proposals.size >= 10 ? proposals.delete(last) : proposals; + + return { + currentUser, + proposals: newProposals, + last_id, + nightmodeEnabled: state.app.getIn([ + 'user_preferences', + 'nightmode', + ]), + }; + }, + dispatch => { + return { + voteOnProposal: ( + voter, + proposal_ids, + approve, + successCallback, + errorCallback + ) => { + dispatch( + transactionActions.broadcastOperation({ + type: 'update_proposal_votes', + operation: { voter, proposal_ids, approve }, + successCallback, + errorCallback, + }) + ); + }, + createProposal: ( + creator, + receiver, + start_date, + end_date, + daily_pay, + subject, + permlink, + successCallback, + errorCallback + ) => { + dispatch( + transactionActions.broadcastOperation({ + type: 'create_proposal', + operation: { + creator, + receiver, + start_date, + end_date, + daily_pay, + subject, + permlink, + }, + successCallback, + errorCallback, + }) + ); + }, + listProposals: payload => { + return new Promise((resolve, reject) => { + dispatch( + proposalActions.listProposals({ + ...payload, + resolve, + reject, + }) + ); + }); + }, + setRouteTag: () => + dispatch(appActions.setRouteTag({ routeTag: 'proposals' })), + }; + } +)(Proposals) diff --git a/src/app/components/modules/SidePanel/index.jsx b/src/app/components/modules/SidePanel/index.jsx index d0fc230af..487799c67 100644 --- a/src/app/components/modules/SidePanel/index.jsx +++ b/src/app/components/modules/SidePanel/index.jsx @@ -139,21 +139,11 @@ const SidePanel = ({ // label: tt('navigation.chat'), // link: 'https://steem.chat/home', // }, - { - value: 'jobs', - label: tt('navigation.jobs'), - link: 'https://jobs.lever.co/steemit', - }, // { // value: 'tools', // label: tt('navigation.app_center'), // link: 'https://steemprojects.com/', // }, - { - value: 'business', - label: tt('navigation.business_center'), - link: 'https://steemeconomy.com/', - }, { value: 'api_docs', label: tt('navigation.api_docs'), @@ -286,4 +276,4 @@ export default connect( dispatch(appActions.setUserPreferences(payload)); }, }) -)(SidePanel); \ No newline at end of file +)(SidePanel); diff --git a/src/app/components/modules/Transfer.jsx b/src/app/components/modules/Transfer.jsx index 86cb89dd7..60795da31 100644 --- a/src/app/components/modules/Transfer.jsx +++ b/src/app/components/modules/Transfer.jsx @@ -44,6 +44,7 @@ class TransferForm extends Component { following: PropTypes.object.isRequired, totalVestingFund: PropTypes.number.isRequired, totalVestingShares: PropTypes.number.isRequired, + errorMessage: undefined, }; static defaultProps = { @@ -312,7 +313,10 @@ class TransferForm extends Component { }; errorCallback = estr => { - this.setState({ trxError: estr, loading: false, tronLoading: false }); + this.setState({ + trxError: estr, loading: false, tronLoading: false, + errorMessage: String(estr), + }); }; balanceValue() { @@ -367,11 +371,12 @@ class TransferForm extends Component { }; onChangeTo = async value => { - this.state.to.props.onChange(value); + const cleanValue = value.replace(/\s+/g, ''); + this.state.to.props.onChange(cleanValue); const { transferType } = this.props.initialValues; if (transferType === 'Transfer to Account') { - this.checkExchangeStatus(value); - this.setState({ toggle_check: false }) + this.checkExchangeStatus(cleanValue); + this.setState({ toggle_check: false }); } }; @@ -412,7 +417,7 @@ class TransferForm extends Component { { LIQUID_TOKEN, VESTING_TOKEN } ); const { to, amount, asset, memo } = this.state; - const { loading, advanced, toggle_check } = this.state; + const { loading, advanced, toggle_check, errorMessage } = this.state; const { currentUser, toVesting, @@ -434,7 +439,7 @@ class TransferForm extends Component {
    { // steem transfer - this.setState({ loading: true }); + this.setState({ loading: true, errorMessage: undefined }); dispatchSubmit({ ...data, errorCallback: this.errorCallback, @@ -563,6 +568,14 @@ class TransferForm extends Component {
    {to.touched &&

    {toVesting && powerTip3}

    }
    + {(to && to.touched && to.error) ? ( +
    + {to && + to.touched && + to.error && + to.error}  +
    + ) : null}
    )} @@ -762,6 +775,15 @@ class TransferForm extends Component {
    )} + + {errorMessage && ( +
    +
    + {errorMessage} +
    +
    + )} +
    {loading && ( diff --git a/src/app/components/modules/UserWallet.jsx b/src/app/components/modules/UserWallet.jsx index 49651543a..b08ae387a 100644 --- a/src/app/components/modules/UserWallet.jsx +++ b/src/app/components/modules/UserWallet.jsx @@ -38,11 +38,13 @@ import { recordAdsView, userActionRecord } from 'app/utils/ServerApiClient'; import QRCode from 'react-qr'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; import ConversionsModal from 'app/components/elements/ConversionsModal'; +import WithdrawRoutesModal from 'app/components/elements/WithdrawRoutesModal'; import ChangeRecoveryAccount from 'app/components/modules/ChangeRecoveryAccount'; import { fetchData } from 'app/utils/steemApi'; const DAYS_TO_HIDE = 5; const assetPrecision = 1000; +const PD_DISMISS_DAYS = 7; class UserWallet extends React.Component { constructor() { @@ -54,6 +56,9 @@ class UserWallet extends React.Component { timestamp: null, showChangeRecoveryModal: false, showConversionsModal: false, + showWithdrawRoutesModal: false, + showPowerDownAlert: false, + showWithdrawRoutesAlert: false, conversions: [], conversionValue: 0, sbdPrice: 0, @@ -61,13 +66,6 @@ class UserWallet extends React.Component { this.shouldComponentUpdate = shouldComponentUpdate(this, 'UserWallet'); } - componentDidMount() { - const { account, getWithdrawRoutes } = this.props; - if (account && getWithdrawRoutes) { - getWithdrawRoutes(account.get('name')); - } - } - // All event handlers are defined as class methods for performance and stable 'this' context. onShowSteemTrade = e => { if (e && e.preventDefault) e.preventDefault(); @@ -170,6 +168,8 @@ class UserWallet extends React.Component { console.warn("[componentDidMount] Error parsing localStorage data:", e); } } + this.checkPowerDownAlert(); + this.checkWithdrawRoutesAlert(); } async componentDidUpdate(prevProps) { @@ -196,6 +196,7 @@ class UserWallet extends React.Component { } } } + const routesChanged = ((this.props.withdraw_routes || []).length !== (prevProps.withdraw_routes || []).length); const isMyAccount = currentUser && currentUser.get('username') === account.get('name'); const currentHistorySize = account.get('other_history', List()).size; const prevHistorySize = prevProps.account.get('other_history', List()).size; @@ -204,7 +205,7 @@ class UserWallet extends React.Component { } else if (!accountChanged && currentHistorySize !== prevHistorySize) { await this.loadInitialConversions(); } - if (account && currentUserChange && currentUser && isMyAccount) { + if ((accountChanged || currentUserChange) && isMyAccount) { try { const userName = account.get('name'); const storageKey = `button_click_${userName}`; @@ -232,8 +233,226 @@ class UserWallet extends React.Component { console.warn("[componentDidUpdate] Error parsing localStorage data:", e); } } + if (account && currentUserChange && currentUser && isMyAccount || prevProps.gprops !== this.props.gprops) { + this.checkPowerDownAlert(); + } + if (accountChanged || currentUserChange || routesChanged) { + this.checkWithdrawRoutesAlert(); + } } + computeWithdrawRoutesSignature = (routes) => { + try { + let arr = (routes || []).slice().map(function (r) { + let acct = r && r.to_account ? String(r.to_account) : ''; + let pct = r && typeof r.percent !== 'undefined' ? Number(r.percent) : 0; + let pctN = isFinite(pct) ? pct : 0; + return acct + ':' + pctN; + }); + arr.sort(); + return arr.join('|'); + } catch (e) { + console.warn('[WithdrawRoutesAlert] compute signature failed', e); + return ''; + } + }; + + getWithdrawRoutesStorageKey = () => { + const { account } = this.props; + if (!account) return null; + const userName = account.get('name'); + return 'withdraw_routes_alert_' + userName; + }; + + showAdvanced = e => { + e.preventDefault(); + const { account } = this.props; + this.props.showAdvanced({ + account: account.get('name'), + }); + }; + + isDismissedWithdrawRoutesAlert = (currentSignature) => { + try { + let key = this.getWithdrawRoutesStorageKey(); + if (!key) return false; + let raw = localStorage.getItem(key); + if (!raw) return false; + let parsed = JSON.parse(raw); + if (!parsed || !parsed.dismissedAt) return false; + let lastSig = typeof parsed.sig === 'string' ? parsed.sig : ''; + if (currentSignature && currentSignature !== lastSig) { + return false; + } + let ts = new Date(parsed.dismissedAt).getTime(); + let diffDays = (Date.now() - ts) / (1000 * 60 * 60 * 24); + return diffDays <= PD_DISMISS_DAYS; + } catch (e) { + console.warn('[WithdrawRoutesAlert] read localStorage failed', e); + return false; + } + }; + + dismissWithdrawRoutesAlert = () => { + try { + let key = this.getWithdrawRoutesStorageKey(); + if (key) { + let routes = this.props.withdraw_routes || []; + let sig = this.computeWithdrawRoutesSignature(routes); + localStorage.setItem( + key, + JSON.stringify({ + dismissedAt: new Date().toISOString(), + sig: sig + }) + ); + } + } catch (e) { + console.warn('[WithdrawRoutesAlert] write localStorage failed', e); + } + this.setState({ showWithdrawRoutesAlert: false }); + }; + + checkWithdrawRoutesAlert = () => { + let account = this.props.account; + let currentUser = this.props.currentUser; + let gprops = this.props.gprops; + let routes = this.props.withdraw_routes || []; + if (!account || !currentUser || !gprops) { + this.setState({ showWithdrawRoutesAlert: false }); + return; + } + + let isMyAccount = currentUser.get('username') === account.get('name'); + if (!isMyAccount) { + this.setState({ showWithdrawRoutesAlert: false }); + return; + } + try { + let acc = account.toJS(); + let gp = typeof gprops.toJS === 'function' ? gprops.toJS() : gprops; + let pdAmount = Number(powerdownSteem(acc, gp)) || 0; + if (!pdAmount || pdAmount <= 0) { + try { + let k1 = this.getWithdrawRoutesStorageKey(); + if (k1) localStorage.removeItem(k1); + } catch (_) {} + this.setState({ showWithdrawRoutesAlert: false }); + return; + } + } catch (e) { + console.warn('[WithdrawRoutesAlert] powerdown calc failed', e); + this.setState({ showWithdrawRoutesAlert: false }); + return; + } + let count = routes && routes.length ? routes.length : 0; + if (!count || count <= 0) { + // try { + // let key = this.getWithdrawRoutesStorageKey(); + // if (key) localStorage.removeItem(key); + // } catch (_) {} + this.setState({ showWithdrawRoutesAlert: false }); + return; + } + + let sig = this.computeWithdrawRoutesSignature(routes); + if (this.isDismissedWithdrawRoutesAlert(sig)) { + this.setState({ showWithdrawRoutesAlert: false }); + return; + } + + this.setState({ showWithdrawRoutesAlert: true }); + }; + + getPowerDownStorageKey = () => { + const { account } = this.props; + if (!account) return null; + const userName = account.get('name'); + return `powerdown_alert_${userName}`; + }; + isDismissedPowerDownAlert = (currentPdAmount) => { + try { + const key = this.getPowerDownStorageKey(); + if (!key) return false; + const raw = localStorage.getItem(key); + if (!raw) return false; + const parsed = JSON.parse(raw); + if (!parsed || !parsed.dismissedAt) return false; + const lastDismissedAmount = typeof parsed.pdAmount === 'number' ? parsed.pdAmount : 0; + const EPS = 1; + if (typeof currentPdAmount === 'number' && Math.abs(currentPdAmount - lastDismissedAmount) > EPS) { + return false; + } + const ts = new Date(parsed.dismissedAt).getTime(); + const diffDays = (Date.now() - ts) / (1000 * 60 * 60 * 24); + return diffDays <= PD_DISMISS_DAYS; + } catch (e) { + console.warn('[PowerDownAlert] read localStorage failed', e); + return false; + } + }; + dismissPowerDownAlert = () => { + try { + const key = this.getPowerDownStorageKey(); + if (key) { + const { account, gprops } = this.props; + let pdAmount = 0; + if (account && gprops) { + const acc = account.toJS(); + const gp = gprops.toJS ? gprops.toJS() : gprops; + pdAmount = Number(powerdownSteem(acc, gp)) || 0; + } + localStorage.setItem( + key, + JSON.stringify({ + dismissedAt: new Date().toISOString(), + pdAmount: pdAmount, + }) + ); + } + } catch (e) { + console.warn('[PowerDownAlert] write localStorage failed', e); + } + this.setState({ showPowerDownAlert: false }); + }; + checkPowerDownAlert = () => { + const { account, gprops, currentUser } = this.props; + + if (!account || !gprops) { + this.setState({ showPowerDownAlert: false }); + return; + } + + const isMyAccount = + currentUser && currentUser.get('username') === account.get('name'); + + if (!isMyAccount) { + this.setState({ showPowerDownAlert: false }); + return; + } + + const acc = account.toJS(); + const gp = gprops.toJS ? gprops.toJS() : gprops; + const totalSP = vestingSteem(acc, gp); + const pdAmount = powerdownSteem(acc, gp); + + if (!pdAmount || pdAmount <= 0) { + try { + const key = this.getPowerDownStorageKey(); + if (key) localStorage.removeItem(key); + } catch (_) {} + this.setState({ showPowerDownAlert: false }); + return; + } + if (this.isDismissedPowerDownAlert(pdAmount)) { + this.setState({ showPowerDownAlert: false }); + return; + } + + const pct = totalSP > 0 ? (pdAmount / totalSP) * 100 : 0; + this.setState({ showPowerDownAlert: true }); + }; + async loadInitialConversions() { try { const account = this.props.account; @@ -289,14 +508,6 @@ class UserWallet extends React.Component { } }; - showAdvanced = e => { - e.preventDefault(); - const { account } = this.props; - this.props.showAdvanced({ - account: account.get('name'), - }); - }; - getCurrentApr = gprops => { // The inflation was set to 9.5% at block 7m const initialInflationRate = 9.5; @@ -352,10 +563,10 @@ class UserWallet extends React.Component { account, currentUser, open_orders, - notify, withdraw_routes, + notify, } = this.props; - const { showQR, hasClicked, showChangeRecoveryModal, conversionValue } = this.state; + const { showQR, hasClicked, showChangeRecoveryModal, conversionValue, showPowerDownAlert } = this.state; const gprops = this.props.gprops.toJS(); // do not render if account is not loaded or available @@ -371,6 +582,73 @@ class UserWallet extends React.Component { const isMyAccount = currentUser && currentUser.get('username') === account.get('name'); + let powerDownWarningBox = null; + if (showPowerDownAlert && powerdown_steem > 0 && isMyAccount) { + try { + const pct = vesting_steem > 0 ? (powerdown_steem * 4 / vesting_steem) * 100 : 0; + const isHighRisk = pct >= 50; + const warningBoxClass = 'UserWallet__warningbox' + (isHighRisk ? ' UserWallet__warningbox--danger' : ''); + powerDownWarningBox = ( +
    +
    +
    + + {tt('powerdown_alert.message', { + amount: numberWithCommas((powerdown_steem * 4).toFixed(3)), + percent: pct.toFixed(2), + })} + +
    + +
    +
    +
    +
    + ); + } catch (error) { + console.log(error) + } + } + let withdrawRoutesWarningBox = null; + if (this.state.showWithdrawRoutesAlert) { + try { + withdrawRoutesWarningBox = ( +
    +
    +
    + + {tt('advanced_routes.withdraw_routes_detected')} + +
    + + +
    +
    +
    +
    + ); + } catch (e) { + console.warn(e); + } + } const disabledWarning = false; // isMyAccount = false; // false to hide wallet transactions @@ -439,9 +717,7 @@ class UserWallet extends React.Component { } const balance_steem = parseFloat(account.get('balance').split(' ')[0]); - const saving_balance_steem = parseFloat( - savings_balance.split(' ')[0] - ); + const saving_balance_steem = parseFloat(savings_balance.split(' ')[0]); const divesting = parseFloat(account.get('vesting_withdraw_rate').split(' ')[0]) > 0.0; @@ -517,7 +793,8 @@ class UserWallet extends React.Component { .map(item => { const data = item.getIn([1, 'op', 1]); const type = item.getIn([1, 'op', 0]); - + const trx_id = item.getIn([1, 'trx_id']) + const block_id = item.getIn([1, 'block']) // Filter out rewards if ( type === 'curation_reward' || @@ -536,6 +813,8 @@ class UserWallet extends React.Component { ); @@ -726,6 +1005,33 @@ class UserWallet extends React.Component { break; default: } + // withdraw routes + const routes = withdraw_routes && withdraw_routes.length > 0 ? [...withdraw_routes] : []; + const sortedRoutes = routes.sort((a, b) => b.percent - a.percent); + let message = null; + + if (sortedRoutes.length === 1 && sortedRoutes[0].percent === 10000) { + message = ( + + {tt('userwallet_jsx.routed_to_single')} + + {`@${sortedRoutes[0].to_account}`}. + + + ); + } else if (sortedRoutes.length >= 1 && sortedRoutes[0].percent < 10000) { + message = ( + + this.setState({ showWithdrawRoutesModal: true })} + > + {tt('userwallet_jsx.view_all_withdraw_routes')} + . + + ); + } const combinedConversions = [ ...(this.state.conversions || []).map(item => ({ @@ -842,22 +1148,6 @@ class UserWallet extends React.Component { console.error(e); } - let advancedRoutesNotification = null; - if (isMyAccount && withdraw_routes && withdraw_routes.size > 0) { - const message = - 'Custom withdrawal routes were configured to receive vesting payments. Please reconfirm in the Advanced Routes options.'; - - advancedRoutesNotification = ( -
    -
    -
    -

    {message}

    -
    -
    -
    - ); - } - return (
    {(showChangeRecoveryModal && accountToRecover && recoveryAccount) && ( @@ -869,8 +1159,13 @@ class UserWallet extends React.Component { /> )} {recoveryWarningBox} + {withdrawRoutesWarningBox} + {powerDownWarningBox} {claimbox}
    + {/*
    + +
    */}
    - {advancedRoutesNotification}
    {powerdown_steem != 0 && ( @@ -1116,8 +1410,19 @@ class UserWallet extends React.Component { )} />{' '} {'(~' + powerdown_balance_str + ' STEEM)'}. + {' '} + {message} )} + {this.state.showWithdrawRoutesModal && ( + this.setState({ showWithdrawRoutesModal: false })} + routes={sortedRoutes} + accountName={account.get('name')} + steemPower={powerdown_balance_str} + /> + )}
    {disabledWarning && ( @@ -1172,12 +1477,12 @@ export default connect( (state, ownProps) => { const price_per_steem = pricePerSteem(state); const savings_withdraws = state.user.get('savings_withdraws'); + const routes = state.user.get('withdraw_routes'); + const withdraw_routes = routes && routes.toJS ? routes.toJS() : []; const gprops = state.global.get('props'); const sbd_interest = gprops.get('sbd_interest_rate'); // This is current logined user. const currentUser = ownProps.currentUser; - const withdraw_routes = state.user.get('withdraw_routes'); - return { ...ownProps, open_orders: state.market.get('open_orders'), @@ -1186,8 +1491,8 @@ export default connect( sbd_interest, gprops, trackingId: state.app.getIn(['trackingId'], null), - currentUser, withdraw_routes, + currentUser, conversionsSuccess: state.transaction.get('conversions'), prices: state.transaction.get('prices'), }; diff --git a/src/app/components/modules/UserWallet.scss b/src/app/components/modules/UserWallet.scss index c39fc5379..6b51ac1dc 100644 --- a/src/app/components/modules/UserWallet.scss +++ b/src/app/components/modules/UserWallet.scss @@ -155,3 +155,36 @@ } } } + +.UserWallet__warningbox--danger { + @include themify($themes) { + background-color: themed('backgroundColorDanger'); + border: themed('borderDanger'); + color: themed('textColorOnDanger'); + } ++ + .UserWallet__warningbox__text { + @include themify($themes) { + color: themed('textColorOnDanger'); + } + } ++ + .UserWallet__warningbox__buttons { + .button.hollow { + @include themify($themes) { + border: 1px solid themed('textColorOnDanger'); + color: themed('textColorOnDanger'); + } + &:hover, + &:focus { + @include themify($themes) { + border-color: themed('textColorOnDanger'); + color: themed('textColorOnDanger'); + } + text-shadow: none; + box-shadow: none; + background: rgba(255, 255, 255, 0.12); + } + } + } +} diff --git a/src/app/components/modules/Witnesses.jsx b/src/app/components/modules/Witnesses.jsx new file mode 100644 index 000000000..aa5eb9b5d --- /dev/null +++ b/src/app/components/modules/Witnesses.jsx @@ -0,0 +1,561 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import links from 'app/utils/Links'; +import Icon from 'app/components/elements/Icon'; +import * as transactionActions from 'app/redux/TransactionReducer'; +import ByteBuffer from 'bytebuffer'; +import { is, Set, List } from 'immutable'; +import * as globalActions from 'app/redux/GlobalReducer'; +import * as appActions from 'app/redux/AppReducer'; +import tt from 'counterpart'; +import { userActionRecord } from 'app/utils/ServerApiClient'; +import { FormattedHTMLMessage } from 'app/Translator'; + +const Long = ByteBuffer.Long; +const { string, func, object } = PropTypes; + +const DISABLED_SIGNING_KEY = 'STM1111111111111111111111111111111114T1Anm'; + +function _blockGap(head_block, last_block) { + if (!last_block || last_block < 1) return 'forever'; + const secs = (head_block - last_block) * 3; + if (secs < 120) return 'recently'; + const mins = Math.floor(secs / 60); + if (mins < 120) return mins + ' mins ago'; + const hrs = Math.floor(mins / 60); + if (hrs < 48) return hrs + ' hrs ago'; + const days = Math.floor(hrs / 24); + if (days < 14) return days + ' days ago'; + const weeks = Math.floor(days / 7); + if (weeks < 104) return weeks + ' weeks ago'; +} + +class Witnesses extends React.Component { + static propTypes = { + // HTML properties + + // Redux connect properties + witnesses: object.isRequired, + accountWitnessVote: func.isRequired, + username: string, + witness_votes: object, + }; + + constructor() { + super(); + this.state = { customUsername: '', proxy: '', proxyFailed: false }; + this.accountWitnessVote = (accountName, approve, e) => { + e.preventDefault(); + const { username, accountWitnessVote } = this.props; + this.setState({ customUsername: '' }); + accountWitnessVote(username, accountName, approve); + }; + this.onWitnessChange = e => { + const customUsername = e.target.value; + this.setState({ customUsername }); + // Force update to ensure witness vote appears + this.forceUpdate(); + }; + this.accountWitnessProxy = e => { + e.preventDefault(); + const { username, accountWitnessProxy } = this.props; + accountWitnessProxy(username, this.state.proxy, state => { + this.setState(state); + }); + }; + } + + componentWillMount() { + this.props.setRouteTag(); + } + componentDidMount() { + if (this.props.walletSectionAccount && !this.props.current_proxy && !this.props.username) { + const { fetchAccountWitnessVotes } = this.props; + fetchAccountWitnessVotes(this.props.walletSectionAccount); + } + } + + componentDidUpdate(prevProps) { + const { walletSectionAccount, fetchAccountWitnessVotes } = this.props; + if (walletSectionAccount !== prevProps.walletSectionAccount) { + if (walletSectionAccount && !this.props.current_proxy && !this.props.username) { + fetchAccountWitnessVotes(walletSectionAccount); + } + } + } + + shouldComponentUpdate(np, ns) { + return ( + !is(np.witness_votes, this.props.witness_votes) || + !is(np.witnessVotesInProgress, this.props.witnessVotesInProgress) || + np.witnesses !== this.props.witnesses || + np.current_proxy !== this.props.current_proxy || + np.username !== this.props.username || + ns.customUsername !== this.state.customUsername || + ns.proxy !== this.state.proxy || + ns.proxyFailed !== this.state.proxyFailed + ); + } + + render() { + const { + props: { + witness_votes, + witnessVotesInProgress, + current_proxy, + head_block, + }, + state: { customUsername, proxy }, + accountWitnessVote, + accountWitnessProxy, + onWitnessChange, + } = this; + const sorted_witnesses = this.props.witnesses.sort((a, b) => + Long.fromString(String(b.get('votes'))).subtract( + Long.fromString(String(a.get('votes'))).toString() + ) + ); + let witness_vote_count = 30; + let rank = 1; + const { canVote } = this.props + const witnesses = sorted_witnesses.map(item => { + const owner = item.get('owner'); + const thread = item.get('url'); + const myVote = witness_votes ? witness_votes.has(owner) : null; + const signingKey = item.get('signing_key'); + const lastBlock = item.get('last_confirmed_block_num'); + const isDisabled = + signingKey == DISABLED_SIGNING_KEY; + const votingActive = witnessVotesInProgress.has(owner); + const classUp = + 'Voting__button Voting__button-up' + + (myVote === true ? ' Voting__button--upvoted' : '') + + (votingActive ? ' votingUp' : ''); + const up = ( + + ); + + let witness_link = ''; + if (thread) { + if (!/^https?:\/\//.test(thread)) { + witness_link = '(No URL provided)'; + } else if (links.remote.test(thread)) { + witness_link = ( + + {tt('witnesses_jsx.external_site')}  + + ); + } else { + witness_link = ( + + {tt('witnesses_jsx.witness_thread')}  + + ); + } + } + + const ownerStyle = isDisabled + ? { textDecoration: 'line-through', color: '#AAA' } + : {}; + + return ( + + + {rank < 10 && '0'} + {rank++} +    + + {votingActive ? ( + up + ) : canVote ? ( + + {up} + + ) : ( + + {up} + + )} + + + + + {owner} + + {isDisabled && ( + + {' '} + ({tt('witnesses_jsx.disabled')}{' '} + {_blockGap(head_block, lastBlock)}) + + )} + + {witness_link} + + ); + }); + + let addl_witnesses = false; + if (witness_votes) { + witness_vote_count -= witness_votes.size; + addl_witnesses = witness_votes + .union(witnessVotesInProgress) + .filter(item => { + return !sorted_witnesses.has(item); + }) + .map(item => { + const votingActive = witnessVotesInProgress.has(item); + const classUp = + 'Voting__button Voting__button-up' + + (votingActive + ? ' votingUp' + : ' Voting__button--upvoted'); + const up = ( + + ); + return ( +
    +
    + + {/*className="Voting"*/} + + {votingActive ? ( + up + ) : ( + + {up} + + )} +   + + + {item} +
    +
    + ); + }) + .toArray(); + } + + return ( +
    +
    +
    +

    {tt('witnesses_jsx.top_witnesses')}

    + + + {tt('witnesses_jsx.witnesses_description')} + + + + {current_proxy && current_proxy.length ? null : ( +

    + + {tt( + 'witnesses_jsx.you_have_votes_remaining', + { count: witness_vote_count } + )}. + {' '} + {tt( + 'witnesses_jsx.you_can_vote_for_maximum_of_witnesses' + )}. +

    + )} +
    +
    + {current_proxy ? null : ( +
    +
    + + + + + + + + {witnesses.toArray()} +
    + {tt('witnesses_jsx.witness')} + {tt('witnesses_jsx.information')} +
    +
    +
    + )} + + {current_proxy ? null : ( +
    +
    +

    + {tt( + 'witnesses_jsx.if_you_want_to_vote_outside_of_top_enter_account_name' + )}. +

    + +
    + @ + +
    + +
    +
    + +
    + {addl_witnesses} +
    +
    +
    +
    + )} + +
    +
    +

    + {current_proxy + ? tt('witnesses_jsx.witness_set') + : tt('witnesses_jsx.set_witness_proxy')} +

    + {current_proxy ? ( +
    +
    + {tt('witnesses_jsx.witness_proxy_current')}:{' '} + {current_proxy} +
    + +
    +
    + +
    + +
    +
    +
    +
    + ) : ( +
    +
    + @ + { + this.setState({ + proxy: e.target.value, + }); + }} + /> +
    + +
    +
    +
    + )} + {this.state.proxyFailed && ( +

    + {tt('witnesses_jsx.proxy_update_error')}. +

    + )} +
    +
    +
    +
    + ); + } +} + +const isNonEmptyString = v => typeof v === 'string' && v.trim().length > 0; +const norm = s => (typeof s === 'string' ? s.trim().toLowerCase() : ''); + +export default connect( + (state, ownProps) => { + // const current_user = state.user.get('current'); + // const username = current_user && current_user.get('username'); + // const current_account = + // current_user && state.global.getIn(['accounts', username]); + // const witness_votes = + // current_account && current_account.get('witness_votes').toSet(); + // const current_proxy = + // current_account && current_account.get('proxy'); + // const witnesses = state.global.get('witnesses', List()); + // const witnessVotesInProgress = state.global.get( + // `transaction_witness_vote_active_${username}`, + // Set() + // ); + const current_user = state.user.get('current'); + const username = current_user && current_user.get('username'); + + const hasPropAccount = isNonEmptyString(ownProps.walletSectionAccount); + const sourceAccountName = hasPropAccount + ? ownProps.walletSectionAccount.trim() + : username; + + const source_account = isNonEmptyString(sourceAccountName) + ? state.global.getIn(['accounts', sourceAccountName]) + : null; + + const current_proxy = source_account ? source_account.get('proxy') : null; + const witness_votes = source_account && source_account.get('witness_votes') + ? source_account.get('witness_votes').toSet() + : undefined; + + const witnessVotesInProgress = isNonEmptyString(sourceAccountName) + ? state.global.get( + `transaction_witness_vote_active_${sourceAccountName}`, + Set() + ) + : Set(); + + const witnesses = state.global.get('witnesses', List()); + const canVote = + !hasPropAccount || (isNonEmptyString(username) && norm(username) === norm(sourceAccountName)); + + return { + head_block: state.global.getIn(['props', 'head_block_number']), + witnesses, + username, + witness_votes, + witnessVotesInProgress, + current_proxy, + canVote, + sourceAccountName, + }; + }, + dispatch => { + return { + accountWitnessVote: (username, witness, approve) => { + userActionRecord('account_witness_vote', { + username, + witness, + }); + dispatch( + transactionActions.broadcastOperation({ + type: 'account_witness_vote', + operation: { account: username, witness, approve }, + confirm: !approve + ? 'You are about to remove your vote for this witness' + : null, + }) + ); + }, + accountWitnessProxy: (account, proxy, stateCallback) => { + userActionRecord('account_witness_proxy', { + username: account, + proxy, + }); + dispatch( + transactionActions.broadcastOperation({ + type: 'account_witness_proxy', + operation: { account, proxy }, + confirm: proxy.length + ? 'Set proxy to: ' + proxy + : 'You are about to remove your proxy.', + successCallback: () => { + dispatch( + globalActions.updateAccountWitnessProxy({ + account, + proxy, + }) + ); + stateCallback({ + proxyFailed: false, + proxy: '', + }); + }, + errorCallback: e => { + stateCallback({ proxyFailed: true }); + }, + }) + ); + }, + setRouteTag: () => + dispatch( + appActions.setRouteTag({ routeTag: 'vote_to_witness' }) + ), + fetchAccountWitnessVotes: (accountName) => { + return dispatch(transactionActions.fetchAccountWitnessVotes({accountName})) + }, + }; + } +)(Witnesses) diff --git a/src/app/components/pages/Proposals.jsx b/src/app/components/pages/Proposals.jsx index c6bc7eb17..00f322f6f 100644 --- a/src/app/components/pages/Proposals.jsx +++ b/src/app/components/pages/Proposals.jsx @@ -1,582 +1,12 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { actions as proposalActions } from 'app/redux/ProposalSaga'; -import * as transactionActions from 'app/redux/TransactionReducer'; // TODO: Only import what we need. -import * as appActions from 'app/redux/AppReducer'; -import { List } from 'immutable'; import PropTypes from 'prop-types'; -import { api } from '@steemit/steem-js'; -import ProposalListContainer from 'app/components/modules/ProposalList/ProposalListContainer'; -import { - LOAD_ALL_VOTERS, - MAX_INITIAL_LOAD, - INITIAL_TIMEOUT, - MAX_TIMEOUT, -} from 'app/components/modules/ProposalList/constants'; -import VotersModal from 'app/components/elements/VotersModal'; -import ProposalCreatorModal from 'app/components/elements/ProposalCreatorModal'; -import tt from 'counterpart'; +import ProposalsComponent from 'app/components/modules/Proposals'; class Proposals extends React.Component { - constructor(props) { - super(props); - this.state = { - proposals: [], - loading: true, - limit: 50, - last_proposal: false, - status: 'votable', - order_by: 'by_total_votes', - order_direction: 'descending', - open_voters_modal: false, - open_creators_modal: false, - voters: [], - voters_accounts: [], - total_vests: '', - total_vest_steem: '', - new_id: '', - is_voters_data_loaded: false, - lastVoter: '', - paid_proposals: [], - }; - this.fetchVoters = this.fetchVoters.bind(this); - this.fetchGlobalProps = this.fetchGlobalProps.bind(this); - this.fetchDataForVests = this.fetchDataForVests.bind(this); - this.setIsVotersDataLoading = this.setIsVotersDataLoading.bind(this); - this.getVotedProposals = this.getVotedProposals.bind(this); - } - async componentWillMount() { - this.props.setRouteTag(); - await this.load(); - } - componentDidMount() { - this.fetchGlobalProps(); - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.new_id !== this.state.new_id) { - this.fetchVoters(); - this.setIsVotersDataLoading(false); - } - if (prevState.voters !== this.state.voters) { - this.fetchDataForVests(); - } - if (prevState.voters_accounts !== this.state.voters_accounts) { - this.setIsVotersDataLoading(!this.state.is_voters_data_loaded); - } - if (prevProps.currentUser !== this.props.currentUser) { - this.updateProposalVotes(this.props.currentUser); - } - } - - getStartValue(order_by, order_direction) { - const minDate = '1970-01-01T00:00:00'; - const maxDate = '2038-01-19T03:14:07'; - const startValueByOrderType = { - by_total_votes: { - ascending: [0], - descending: [-1, 0], - }, - by_creator: { - ascending: [''], - descending: ['zzzzzzzzzzzzzz'], - }, - by_start_date: { - ascending: [minDate], - descending: [maxDate], - }, - by_end_date: { - ascending: [minDate], - descending: [maxDate], - }, - }; - const value = startValueByOrderType[order_by][order_direction]; - return value; - } - - async load(quiet = false, options = {}) { - if (quiet) { - this.setState({ loading: true }); - } - - const { status, order_by, order_direction } = options; - - const isFiltering = !!(status || order_by || order_direction); - - let limit; - - if (isFiltering) { - limit = this.state.limit; - } else { - limit = this.state.limit + this.state.proposals.length; - } - - const start = this.getStartValue( - order_by || this.state.order_by, - order_direction || this.state.order_direction - ); - const proposals = - (await this.getAllProposals( - this.state.last_proposal, - order_by || this.state.order_by, - order_direction || this.state.order_direction, - limit, - status || this.state.status, - start - )) || []; - let last_proposal = false; - if (proposals.length > 0) { - last_proposal = proposals[0]; - } - this.setState({ - proposals, - loading: false, - last_proposal, - limit, - }); - } - - onFilterProposals = async status => { - this.setState({ status }); - await this.load(false, { status }); - }; - - onOrderProposals = async order_by => { - this.setState({ order_by }); - await this.load(false, { order_by }); - }; - - onOrderDirection = async order_direction => { - this.setState({ order_direction }); - await this.load(false, { order_direction }); - }; - - getVotersAccounts = voters_accounts => { - this.setState({ voters_accounts }); - }; - - getVoters = (voters, lastVoter) => { - this.setState({ voters, lastVoter }); - }; - - getNewId = new_id => { - this.setState({ new_id }); - }; - - setIsVotersDataLoading = is_voters_data_loaded => { - this.setState({ is_voters_data_loaded }); - }; - setPaidProposals = paid_proposals => { - this.setState({ paid_proposals }); - }; - - getAllProposals( - last_proposal, - order_by, - order_direction, - limit, - status, - start - ) { - return this.props.listProposals({ - voter_id: this.props.currentUser, - last_proposal, - order_by, - order_direction, - limit, - status, - start, - }); - } - - voteOnProposal = async (proposalId, voteForIt, onSuccess, onFailure) => { - return this.props.voteOnProposal( - this.props.currentUser, - [proposalId], - voteForIt, - async () => { - if (onSuccess) onSuccess(); - }, - () => { - if (onFailure) onFailure(); - } - ); - }; - - fetchGlobalProps() { - api.callAsync('condenser_api.get_dynamic_global_properties', []) - .then(res => - this.setState({ - total_vests: res.total_vesting_shares, - total_vest_steem: res.total_vesting_fund_steem, - }) - ) - .catch(err => console.log(err)); - } - - fetchVoters() { - this.fetchAllVotersWithPause({ - proposalId: this.state.new_id, - timeout: INITIAL_TIMEOUT, - maxToLoad: LOAD_ALL_VOTERS ? null : MAX_INITIAL_LOAD, - }) - .then(res => { - this.getVoters(res, ...res.slice(-1)); - }) - .catch(err => console.log(err)); - } - - fetchDataForVests() { - const voters = this.state.voters; - const new_id = this.state.new_id; - - const selected_proposal_voters = voters.filter( - v => v.proposal.proposal_id === new_id - ); - const voters_map = selected_proposal_voters.map(name => name.voter); - api.getAccountsAsync(voters_map) - .then(res => this.getVotersAccounts(res)) - .catch(err => console.log(err)); - } - - async getVotedProposals({ accountName, proposalIdsSet }) { - const votedMap = {}; - - try { - const result = await new Promise((resolve, reject) => { - api.callAsync( - 'database_api.list_proposal_votes', - { - start: [accountName], - limit: 1000, - order: 'by_voter_proposal', - order_direction: 'ascending', - status: 'all', - }, - (err, res) => { - if (err) reject(err); - else resolve(res); - } - ); - }); - - const votes = (result && result.proposal_votes) || []; - if (votes.length === 0 || votes[0].voter !== accountName) { - return votedMap; - } - - for (const vote of votes) { - if (vote.voter !== accountName) break; - const proposalId = vote.proposal.proposal_id; - if (proposalIdsSet.has(proposalId)) { - votedMap[proposalId] = true; - } - } - - return votedMap; - } catch (err) { - console.error('Error al obtener propuestas votadas:', err); - return votedMap; - } - } - - async updateProposalVotes(currentUser) { - if (typeof currentUser !== 'string' || currentUser.length <= 1) return; - - const { proposals } = this.state; - const proposalIdsSet = new Set(proposals.map(p => p.id)); - const votedMap = await this.getVotedProposals({ accountName: currentUser, proposalIdsSet }); - - const updatedProposals = proposals.map(p => ({ - ...p, - upVoted: !!votedMap[p.id], - })); - - this.setState({ proposals: updatedProposals }); - } - - delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - async fetchAllVotersWithPause({ - proposalId, - lastVoter = '', - accumulated = [], - timeout = INITIAL_TIMEOUT, - maxToLoad = null, - }) { - try { - const res = await new Promise((resolve, reject) => { - api.callAsync( - 'database_api.list_proposal_votes', - { - start: [proposalId, lastVoter], - limit: 1000, - order: 'by_proposal_voter', - order_direction: 'ascending', - status: 'active', - }, - (err, result) => { - if (err) reject(err); - else resolve(result); - } - ); - }); - - const votes = (res && res.proposal_votes) || []; - if (votes.length === 0) return accumulated; - const allVoters = accumulated.concat(votes); - if (maxToLoad && allVoters.length >= maxToLoad) { - return allVoters.slice(0, maxToLoad); - } - if (votes.length < 1000) { - return allVoters; - } - if (votes && votes.length >= 2) { - try { - const firstProposalId = votes[0].proposal.proposal_id; - const lastProposalId = votes.at(-1).proposal.proposal_id; - if ( - firstProposalId !== proposalId || - lastProposalId !== proposalId - ) { - return allVoters; - } - } catch (error) { - console.error(error); - } - } - const nextVoter = votes.at(-1) ? votes.at(-1).voter : undefined; - await this.delay(timeout); - const nextTimeout = Math.min(timeout + 250, MAX_TIMEOUT); - return this.fetchAllVotersWithPause({ - proposalId, - lastVoter: nextVoter, - accumulated: allVoters, - timeout: nextTimeout, - maxToLoad, - }); - } catch (err) { - console.error('Error al obtener votantes:', err); - return accumulated; - } - } - - onClickLoadMoreProposals = e => { - e.preventDefault(); - this.load(); - }; - - triggerCreatorsModal = () => { - this.setState({ - open_creators_modal: !this.state.open_creators_modal, - }); - }; - - triggerVotersModal = () => { - this.setState({ - open_voters_modal: !this.state.open_voters_modal, - }); - }; - - submitProposal = (proposal, onSuccess, onFailure) => { - this.props.createProposal( - this.props.currentUser || proposal.creator, - proposal.receiver, - proposal.startDate, - proposal.endDate, - `${parseFloat(proposal.dailyAmount).toFixed(3)} SBD`, - proposal.title, - proposal.permlink, - async () => { - this.triggerCreatorsModal(); - if (onSuccess) onSuccess(); - }, - () => { - if (onFailure) onFailure(); - } - ); - }; - - removeProposalById = id => { - this.setState(prevState => ({ - proposals: prevState.proposals.filter( - proposal => proposal.id !== id - ), - })); - }; - render() { - const { - proposals, - loading, - status, - order_by, - order_direction, - voters, - voters_accounts, - open_creators_modal, - open_voters_modal, - total_vests, - total_vest_steem, - is_voters_data_loaded, - new_id, - } = this.state; - - const mergeVoters = [...voters]; - - const { nightmodeEnabled } = this.props; - - let showBottomLoading = false; - if (loading && proposals && proposals.length > 0) { - showBottomLoading = true; - } - const selected_proposal_voters = mergeVoters.filter( - v => v.proposal.proposal_id === new_id - ); - const voters_map = selected_proposal_voters.map(name => name.voter); // voter name - const accounts_map = []; - const acc_proxied_vests = []; - const proxies_name_by_voter = []; - const proxies_vote = {}; - voters_accounts.forEach(acc => { - accounts_map.push(acc.vesting_shares); - const proxied = acc.proxied_vsf_votes - .map(r => parseInt(r, 10)) - .reduce((a, b) => a + b, 0); - acc_proxied_vests.push(proxied); - proxies_name_by_voter.push(acc.proxy); - if (acc.proxy) { - proxies_vote[acc.proxy] = false; - } - }); - const steem_power = []; - const proxy_sp = []; - const total_sp = []; - let global_total_sp = 0; - - const calculatePowers = () => { - const total_vestsNew = parseFloat(total_vests.split(' ')[0]); - const total_vest_steemNew = parseFloat( - total_vest_steem.split(' ')[0] - ); - - for (let i = 0; i < accounts_map.length; i++) { - const vests_account = parseFloat(accounts_map[i].split(' ')[0]); - const vests_proxy = acc_proxied_vests[i]; - - const vesting_steem_account = - total_vest_steemNew * (vests_account / total_vestsNew); - const vesting_steem_proxy = - total_vest_steemNew * - (vests_proxy / total_vestsNew) * - 0.000001; - - const total = vesting_steem_account + vesting_steem_proxy; - - steem_power.push(vesting_steem_account); - proxy_sp.push(vesting_steem_proxy); - total_sp.push(total); - const voter = voters_map[i]; - if (Object.keys(proxies_vote).includes(voter)) { - proxies_vote[voter] = true; - } - global_total_sp += total; - } - }; - calculatePowers(); - const simpleVotesToSp = total_votes => { - const total_vestsNew = parseFloat(total_vests.split(' ')[0]); - const total_vest_steemNew = parseFloat( - total_vest_steem.split(' ')[0] - ); - return ( - total_vest_steemNew * - (total_votes / total_vestsNew) * - 0.000001 - ).toFixed(2); - }; - const pro_aux = proposals.find(p => p.proposal_id === new_id); - let total_votes_aux = 0; - if (pro_aux && pro_aux.total_votes) { - total_votes_aux = simpleVotesToSp(pro_aux.total_votes); - } - const total_acc_sp_obj = {}; - voters_map.forEach((voter, i) => { - const proxy_name = proxies_name_by_voter[i]; - const proxy_vote = proxies_vote[proxy_name] || false; - const influence = total_votes_aux - ? (total_sp[i] / total_votes_aux) * 100 - : 0; - total_acc_sp_obj[voter] = [ - total_sp[i], - steem_power[i], - proxy_sp[i], - proxy_name, - proxy_vote, - influence, - ]; - }); - const sort_merged_total_sp = []; - for (const value in total_acc_sp_obj) { - sort_merged_total_sp.push([value, ...total_acc_sp_obj[value]]); - } - sort_merged_total_sp.sort((a, b) => b[1] - a[1]); - return ( -
    - - - -
    - {!loading ? ( - - {tt('proposals.load_more')} - - ) : null} - - {showBottomLoading ? ( - {tt('proposals.loading')} - ) : null} -
    -
    - ); + + ) } } @@ -588,87 +18,5 @@ Proposals.propTypes = { module.exports = { path: 'proposals', - component: connect( - state => { - const user = state.user.get('current'); - const currentUser = user && user.get('username'); - const proposals = state.proposal.get('proposals', List()); - const last = proposals.size - 1; - const last_id = - (proposals.size && proposals.get(last).get('id')) || null; - const newProposals = - proposals.size >= 10 ? proposals.delete(last) : proposals; - - return { - currentUser, - proposals: newProposals, - last_id, - nightmodeEnabled: state.app.getIn([ - 'user_preferences', - 'nightmode', - ]), - }; - }, - dispatch => { - return { - voteOnProposal: ( - voter, - proposal_ids, - approve, - successCallback, - errorCallback - ) => { - dispatch( - transactionActions.broadcastOperation({ - type: 'update_proposal_votes', - operation: { voter, proposal_ids, approve }, - successCallback, - errorCallback, - }) - ); - }, - createProposal: ( - creator, - receiver, - start_date, - end_date, - daily_pay, - subject, - permlink, - successCallback, - errorCallback - ) => { - dispatch( - transactionActions.broadcastOperation({ - type: 'create_proposal', - operation: { - creator, - receiver, - start_date, - end_date, - daily_pay, - subject, - permlink, - }, - successCallback, - errorCallback, - }) - ); - }, - listProposals: payload => { - return new Promise((resolve, reject) => { - dispatch( - proposalActions.listProposals({ - ...payload, - resolve, - reject, - }) - ); - }); - }, - setRouteTag: () => - dispatch(appActions.setRouteTag({ routeTag: 'proposals' })), - }; - } - )(Proposals), + component: Proposals, }; diff --git a/src/app/components/pages/UserProfile.jsx b/src/app/components/pages/UserProfile.jsx index 48f845510..9a3b7e977 100644 --- a/src/app/components/pages/UserProfile.jsx +++ b/src/app/components/pages/UserProfile.jsx @@ -14,6 +14,8 @@ import UserKeys from 'app/components/elements/UserKeys'; import PasswordReset from 'app/components/elements/PasswordReset'; import CreateCommunity from 'app/components/elements/CreateCommunity'; import Delegations from 'app/components/modules/Delegations'; +import Proposals from 'app/components/modules/Proposals'; +import Witnesses from 'app/components/modules/Witnesses'; import UserWallet from 'app/components/modules/UserWallet'; import Settings from 'app/components/modules/Settings'; import CurationRewards from 'app/components/modules/CurationRewards'; @@ -70,6 +72,10 @@ export default class UserProfile extends React.Component { } componentDidUpdate(prevProps) { + const { accountname, getWithdrawRoutes } = this.props; + if (accountname && accountname !== prevProps.accountname) { + getWithdrawRoutes(accountname); + } this.redirect(); } @@ -150,7 +156,39 @@ export default class UserProfile extends React.Component { />
    ); - } else if (section === 'delegations') { + } else if (section === 'proposals') { + walletClass = 'active'; + tab_content = ( +
    +
    +
    + +
    +
    +
    + +
    + ); + } else if (section === 'witnesses') { + walletClass = 'active'; + tab_content = ( +
    +
    +
    + +
    +
    +
    + +
    + ); + } else if (section === 'delegations') { walletClass = 'active'; tab_content = (
    diff --git a/src/app/components/pages/Witnesses.jsx b/src/app/components/pages/Witnesses.jsx index 7781b0471..9f081b29f 100644 --- a/src/app/components/pages/Witnesses.jsx +++ b/src/app/components/pages/Witnesses.jsx @@ -1,504 +1,15 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { Link } from 'react-router'; -import links from 'app/utils/Links'; -import Icon from 'app/components/elements/Icon'; -import * as transactionActions from 'app/redux/TransactionReducer'; -import ByteBuffer from 'bytebuffer'; -import { is, Set, List } from 'immutable'; -import * as globalActions from 'app/redux/GlobalReducer'; -import * as appActions from 'app/redux/AppReducer'; -import tt from 'counterpart'; -import { userActionRecord } from 'app/utils/ServerApiClient'; - -const Long = ByteBuffer.Long; -const { string, func, object } = PropTypes; - -const DISABLED_SIGNING_KEY = 'STM1111111111111111111111111111111114T1Anm'; - -function _blockGap(head_block, last_block) { - if (!last_block || last_block < 1) return 'forever'; - const secs = (head_block - last_block) * 3; - if (secs < 120) return 'recently'; - const mins = Math.floor(secs / 60); - if (mins < 120) return mins + ' mins ago'; - const hrs = Math.floor(mins / 60); - if (hrs < 48) return hrs + ' hrs ago'; - const days = Math.floor(hrs / 24); - if (days < 14) return days + ' days ago'; - const weeks = Math.floor(days / 7); - if (weeks < 104) return weeks + ' weeks ago'; -} +import WitnessesComponent from 'app/components/modules/Witnesses'; class Witnesses extends React.Component { - static propTypes = { - // HTML properties - - // Redux connect properties - witnesses: object.isRequired, - accountWitnessVote: func.isRequired, - username: string, - witness_votes: object, - }; - - constructor() { - super(); - this.state = { customUsername: '', proxy: '', proxyFailed: false }; - this.accountWitnessVote = (accountName, approve, e) => { - e.preventDefault(); - const { username, accountWitnessVote } = this.props; - this.setState({ customUsername: '' }); - accountWitnessVote(username, accountName, approve); - }; - this.onWitnessChange = e => { - const customUsername = e.target.value; - this.setState({ customUsername }); - // Force update to ensure witness vote appears - this.forceUpdate(); - }; - this.accountWitnessProxy = e => { - e.preventDefault(); - const { username, accountWitnessProxy } = this.props; - accountWitnessProxy(username, this.state.proxy, state => { - this.setState(state); - }); - }; - } - - componentWillMount() { - this.props.setRouteTag(); - } - - shouldComponentUpdate(np, ns) { - return ( - !is(np.witness_votes, this.props.witness_votes) || - !is(np.witnessVotesInProgress, this.props.witnessVotesInProgress) || - np.witnesses !== this.props.witnesses || - np.current_proxy !== this.props.current_proxy || - np.username !== this.props.username || - ns.customUsername !== this.state.customUsername || - ns.proxy !== this.state.proxy || - ns.proxyFailed !== this.state.proxyFailed - ); - } - render() { - const { - props: { - witness_votes, - witnessVotesInProgress, - current_proxy, - head_block, - }, - state: { customUsername, proxy }, - accountWitnessVote, - accountWitnessProxy, - onWitnessChange, - } = this; - const sorted_witnesses = this.props.witnesses.sort((a, b) => - Long.fromString(String(b.get('votes'))).subtract( - Long.fromString(String(a.get('votes'))).toString() - ) - ); - let witness_vote_count = 30; - let rank = 1; - - const witnesses = sorted_witnesses.map(item => { - const owner = item.get('owner'); - const thread = item.get('url'); - const myVote = witness_votes ? witness_votes.has(owner) : null; - const signingKey = item.get('signing_key'); - const lastBlock = item.get('last_confirmed_block_num'); - const noBlock7days = (head_block - lastBlock) * 3 > 604800; - const isDisabled = - signingKey == DISABLED_SIGNING_KEY || noBlock7days; - const votingActive = witnessVotesInProgress.has(owner); - const classUp = - 'Voting__button Voting__button-up' + - (myVote === true ? ' Voting__button--upvoted' : '') + - (votingActive ? ' votingUp' : ''); - const up = ( - - ); - - let witness_link = ''; - if (thread) { - if (!/^https?:\/\//.test(thread)) { - witness_link = '(No URL provided)'; - } else if (links.remote.test(thread)) { - witness_link = ( - - {tt('witnesses_jsx.external_site')}  - - ); - } else { - witness_link = ( - - {tt('witnesses_jsx.witness_thread')}  - - ); - } - } - - const ownerStyle = isDisabled - ? { textDecoration: 'line-through', color: '#AAA' } - : {}; - - return ( - - - {rank < 10 && '0'} - {rank++} -    - - {votingActive ? ( - up - ) : ( - - {up} - - )} - - - - - {owner} - - {isDisabled && ( - - {' '} - ({tt('witnesses_jsx.disabled')}{' '} - {_blockGap(head_block, lastBlock)}) - - )} - - {witness_link} - - ); - }); - - let addl_witnesses = false; - if (witness_votes) { - witness_vote_count -= witness_votes.size; - addl_witnesses = witness_votes - .union(witnessVotesInProgress) - .filter(item => { - return !sorted_witnesses.has(item); - }) - .map(item => { - const votingActive = witnessVotesInProgress.has(item); - const classUp = - 'Voting__button Voting__button-up' + - (votingActive - ? ' votingUp' - : ' Voting__button--upvoted'); - const up = ( - - ); - return ( -
    -
    - - {/*className="Voting"*/} - - {votingActive ? ( - up - ) : ( - - {up} - - )} -   - - - {item} -
    -
    - ); - }) - .toArray(); - } - return ( -
    -
    -
    -

    {tt('witnesses_jsx.top_witnesses')}

    - {current_proxy && current_proxy.length ? null : ( -

    - - {tt( - 'witnesses_jsx.you_have_votes_remaining', - { count: witness_vote_count } - )}. - {' '} - {tt( - 'witnesses_jsx.you_can_vote_for_maximum_of_witnesses' - )}. -

    - )} -
    -
    - {current_proxy ? null : ( -
    -
    - - - - - - - - {witnesses.toArray()} -
    - {tt('witnesses_jsx.witness')} - {tt('witnesses_jsx.information')} -
    -
    -
    - )} - - {current_proxy ? null : ( -
    -
    -

    - {tt( - 'witnesses_jsx.if_you_want_to_vote_outside_of_top_enter_account_name' - )}. -

    -
    -
    - @ - -
    - -
    -
    -
    -
    - {addl_witnesses} -
    -
    -
    -
    - )} - -
    -
    -

    - {current_proxy - ? tt('witnesses_jsx.witness_set') - : tt('witnesses_jsx.set_witness_proxy')} -

    - {current_proxy ? ( -
    -
    - {tt('witnesses_jsx.witness_proxy_current')}:{' '} - {current_proxy} -
    - -
    -
    - -
    - -
    -
    -
    -
    - ) : ( -
    -
    - @ - { - this.setState({ - proxy: e.target.value, - }); - }} - /> -
    - -
    -
    -
    - )} - {this.state.proxyFailed && ( -

    - {tt('witnesses_jsx.proxy_update_error')}. -

    - )} -
    -
    -
    -
    + ); } } module.exports = { path: '/~witnesses(/:witness)', - component: connect( - state => { - const current_user = state.user.get('current'); - const username = current_user && current_user.get('username'); - const current_account = - current_user && state.global.getIn(['accounts', username]); - const witness_votes = - current_account && current_account.get('witness_votes').toSet(); - const current_proxy = - current_account && current_account.get('proxy'); - const witnesses = state.global.get('witnesses', List()); - const witnessVotesInProgress = state.global.get( - `transaction_witness_vote_active_${username}`, - Set() - ); - return { - head_block: state.global.getIn(['props', 'head_block_number']), - witnesses, - username, - witness_votes, - witnessVotesInProgress, - current_proxy, - }; - }, - dispatch => { - return { - accountWitnessVote: (username, witness, approve) => { - userActionRecord('account_witness_vote', { - username, - witness, - }); - dispatch( - transactionActions.broadcastOperation({ - type: 'account_witness_vote', - operation: { account: username, witness, approve }, - confirm: !approve - ? 'You are about to remove your vote for this witness' - : null, - }) - ); - }, - accountWitnessProxy: (account, proxy, stateCallback) => { - userActionRecord('account_witness_proxy', { - username: account, - proxy, - }); - dispatch( - transactionActions.broadcastOperation({ - type: 'account_witness_proxy', - operation: { account, proxy }, - confirm: proxy.length - ? 'Set proxy to: ' + proxy - : 'You are about to remove your proxy.', - successCallback: () => { - dispatch( - globalActions.updateAccountWitnessProxy({ - account, - proxy, - }) - ); - stateCallback({ - proxyFailed: false, - proxy: '', - }); - }, - errorCallback: e => { - console.log('error:', e); - stateCallback({ proxyFailed: true }); - }, - }) - ); - }, - setRouteTag: () => - dispatch( - appActions.setRouteTag({ routeTag: 'vote_to_witness' }) - ), - }; - } - )(Witnesses), + component: Witnesses, }; diff --git a/src/app/components/pages/Witnesses.scss b/src/app/components/pages/Witnesses.scss index 915b439fb..3cdaddbf4 100644 --- a/src/app/components/pages/Witnesses.scss +++ b/src/app/components/pages/Witnesses.scss @@ -2,19 +2,19 @@ .Witnesses { .extlink path { transition: 0.2s all ease-in-out; - @include themify($themes) { - fill: themed('textColorAccent'); - } + @include themify($themes) { + fill: themed('textColorAccent'); + } } a:hover .extlink path { transition: 0.2s all ease-in-out; - @include themify($themes) { - fill: themed('textColorAccentHover'); - } + @include themify($themes) { + fill: themed('textColorAccentHover'); + } } td > a { @extend .link; - @extend .link--primary; + @extend .link--primary; } .button { @@ -24,7 +24,12 @@ &:hover { background-color: $color-teal; } - } + } + .inline-text { + display: inline; + padding-left: 5px; + font-size: 100%; + } } diff --git a/src/app/help/en/faq.md b/src/app/help/en/faq.md index 3638b46ba..df0cea922 100644 --- a/src/app/help/en/faq.md +++ b/src/app/help/en/faq.md @@ -87,6 +87,7 @@ - How do I get more STEEM Power? - How long does it take STEEM or STEEM Power that I purchased to show up in my account? - What is powering up and down? +- What is a withdraw route? - What do the dollar amounts for pending payouts represent? - Will 1 Steem Dollar always be worth $1.00 USD? - How do Steem Dollar to STEEM conversions work? @@ -206,7 +207,7 @@ Steemit has redefined social media by building a living, breathing, and growing ^ ## How does Steemit work? -Steemit.com is one of the many websites (including [Busy.org](https://busy.org/), [DTube](https://d.tube/), and [Utopian.io](https://utopian.io/)) that are powered by the Steem blockchain and STEEM cryptocurrency. All of these websites read and write content to the Steem blockchain, which stores the content in an immutable blockchain ledger, and rewards users for their contributions with digital tokens called STEEM. +Steemit.com is one of the many websites (including [SteemPro](https://www.steempro.com/), [SteemX](https://steemx.org/), and [UPVU](https://upvu.org/)) that are powered by the Steem blockchain and STEEM cryptocurrency. All of these websites read and write content to the Steem blockchain, which stores the content in an immutable blockchain ledger, and rewards users for their contributions with digital tokens called STEEM. Every day, the Steem blockchain mints new STEEM tokens and adds them to a community's "rewards pool". These tokens are then awarded to users for their contributions, based on the votes that their content receives. Users who hold more tokens in their account as "Steem Power" will get to decide where a larger portion of the rewards pool is distributed. @@ -238,7 +239,33 @@ You can earn digital tokens on Steemit by: **Voting and curating** - If you discover a post and upvote it before it becomes popular, you can earn a curation reward. The reward amount will depend on the amount of Steem Power you have. -**Purchasing** - Users can purchase STEEM or Steem Dollar tokens directly through their Steemit wallet using bitcoin, Ether, or BitShares tokens. They are also available from other markets and exchanges including [Binance](https://www.binance.com/), [Bithumb](https://www.bithumb.com/), [BitShares](https://wallet.bitshares.org/), [Bittrex](https://bittrex.com), [Changelly](https://changelly.com), [GOBADA](https://www.gobaba.com/), [HitBTC](https://hitbtc.com/), [Huobi](https://www.huobi.pro/), [LocalBitcoinCash](https://www.localbitcoincash.org/), [Poloniex](https://poloniex.com), [Shapeshift.io](https://shapeshift.io), [UpBit](https://upbit.com/), and [Yensesa](https://yensesa.com). +**Purchasing** – Users can purchase **STEEM** or **Steem Dollar (SBD)** tokens directly through their **Steemit wallet** by buying them on exchanges and then trading in the internal market, or by directly buying them from exchanges. + +The **current exchanges that support STEEM and SBD** include: + +| # | Exchange | Supported Tokens | Link | +|----|---------------|------------------|------| +| 1 | HTX (Huobi) | STEEM / **SBD** | [htx.com](https://www.htx.com/) | +| 2 | Poloniex | STEEM | [poloniex.com](https://poloniex.com/) | +| 3 | CoinUp.io | STEEM | [coinup.io](https://coinup.io/) | +| 4 | MEXC | STEEM | [mexc.com](https://www.mexc.com/) | +| 5 | Binance | STEEM | [binance.com](https://www.binance.com/) | +| 6 | WhiteBIT | STEEM | [whitebit.com](https://whitebit.com/) | +| 7 | Upbit | STEEM | [upbit.com](https://upbit.com/) | +| 8 | BitKan | STEEM | [bitkan.com](https://bitkan.com/) | +| 9 | Bithumb | STEEM | [bithumb.com](https://www.bithumb.com/) | +| 10 | WEEX | STEEM | [weex.com](https://www.weex.com/) | +| 11 | Gate | STEEM | [gate.io](https://www.gate.io/) | +| 12 | Koinbay | STEEM | [koinbay.com](https://koinbay.com/) | +| 13 | Bitexen | STEEM | [bitexen.com](https://www.bitexen.com/) | +| 14 | ZKE | STEEM | [zkex.com](https://www.zke.com/) | +| 15 | DigiFinex | STEEM | [digifinex.com](https://www.digifinex.com/) | +| 16 | ProBit Global | STEEM | [probit.com](https://www.probit.com/) | +| 17 | CoinEx | STEEM | [coinex.com](https://www.coinex.com/) | +| 18 | ChangeNOW | STEEM | [changenow.io](https://changenow.io/) | +| 19 | CoinDCX | STEEM | [coindcx.com](https://coindcx.com/) | +| 20 | ONUS Pro | STEEM | [onus.pro](https://goonus.io/) | +| 21 | Pionex | STEEM | [pionex.com](https://www.pionex.com/) | **Vesting** - STEEM tokens that are powered up to Steem Power will earn a small amount of new tokens for holding. @@ -270,7 +297,7 @@ It is best to have realistic expectations, without focusing on rewards when you Click on the "Sign Up" link at the top of steemit.com to get started. -You will be asked to enter your email address and verify your phone number. After your information has been verified, you will be added to the waiting list to receive an account. You will be notified via email once your account is approved. +You will be asked to enter your email address and verify your phone number. After your information has been verified, you will be added to receive an account. You will be notified via email once your account is approved. After you receive notification that your account is approved, click on the link in the email to finish the account creation process. **Be sure to save and backup your username and password.** It is very important that you do not lose your password. There is no way to recover your password or access your account if it is lost. Once your password is saved and backed up, click on the "Create Account" button to create the account. @@ -635,21 +662,29 @@ Out of the new tokens that are generated: STEEM and SBD are listed on the following exchanges: -| Exchange | STEEM | SBD | -| ------------- |:-------------:| -----:| -| [Binance](https://www.binance.com/) | Y | N | -| [Bithumb](https://www.bithumb.com/) | Y | N | -| [BitShares](https://wallet.bitshares.org/) | Y | Y | -| [Bittrex](https://bittrex.com) | Y | Y | -| [Changelly](https://changelly.com) | Y | N | -| [GOBADA](https://www.gobaba.com/) | Y | N | -| [HitBTC](https://hitbtc.com/) | Y | Y | -| [Huobi](https://www.huobi.pro/) | Y | N | -| [LocalBitcoinCash](https://www.localbitcoincash.org/) | Y | N | -| [Poloniex](https://poloniex.com) | Y | Y | -| [Shapeshift.io](https://shapeshift.io) | Y | N | -| [UpBit](https://upbit.com/) | Y | Y | -| [Yensesa](https://yensesa.com) | Y | Y | +| # | Exchange | Supported Tokens | Link | +|----|---------------|------------------|------| +| 1 | HTX (Huobi) | STEEM / **SBD** | [htx.com](https://www.htx.com/) | +| 2 | Poloniex | STEEM | [poloniex.com](https://poloniex.com/) | +| 3 | CoinUp.io | STEEM | [coinup.io](https://coinup.io/) | +| 4 | MEXC | STEEM | [mexc.com](https://www.mexc.com/) | +| 5 | Binance | STEEM | [binance.com](https://www.binance.com/) | +| 6 | WhiteBIT | STEEM | [whitebit.com](https://whitebit.com/) | +| 7 | Upbit | STEEM | [upbit.com](https://upbit.com/) | +| 8 | BitKan | STEEM | [bitkan.com](https://bitkan.com/) | +| 9 | Bithumb | STEEM | [bithumb.com](https://www.bithumb.com/) | +| 10 | WEEX | STEEM | [weex.com](https://www.weex.com/) | +| 11 | Gate | STEEM | [gate.io](https://www.gate.io/) | +| 12 | Koinbay | STEEM | [koinbay.com](https://koinbay.com/) | +| 13 | Bitexen | STEEM | [bitexen.com](https://www.bitexen.com/) | +| 14 | ZKE | STEEM | [zkex.com](https://www.zke.com/) | +| 15 | DigiFinex | STEEM | [digifinex.com](https://www.digifinex.com/) | +| 16 | ProBit Global | STEEM | [probit.com](https://www.probit.com/) | +| 17 | CoinEx | STEEM | [coinex.com](https://www.coinex.com/) | +| 18 | ChangeNOW | STEEM | [changenow.io](https://changenow.io/) | +| 19 | CoinDCX | STEEM | [coindcx.com](https://coindcx.com/) | +| 20 | ONUS Pro | STEEM | [onus.pro](https://goonus.io/) | +| 21 | Pionex | STEEM | [pionex.com](https://www.pionex.com/) | ^ ## What is the reward pool? @@ -721,25 +756,18 @@ The price of STEEM is based on the supply and demand of the token, as determined ^ ## How do I get more Steem Power? -With STEEM tokens in your wallet, click "Power Up" to turn them into Steem Power. If you have Steem Dollars, you can convert them to STEEM from your wallet, and then power up the STEEM. +With **STEEM tokens** in your wallet, you can click **“Power Up”** to convert them into **Steem Power**. +If you hold **Steem Dollars (SBD)**, you may first convert them into **STEEM** within your wallet and then power up the STEEM, or alternatively, you can directly purchase STEEM through the internal market. If you do not already have STEEM or Steem Dollars in your wallet, they can be obtained from the exchanges that support these tokens. -If you don’t already have STEEM or Steem Dollars in your wallet, you can purchase them using bitcoin (BTC), Ether (ETH), Litecoin (LTC), or BitShares (BTS) tokens. You may purchase BTC on various exchanges, such as Coinbase.com or Localbitcoins.com. +Bitcoin can also be exchanged for **STEEM** on external markets such as [Binance](https://www.binance.com/), [Upbit](https://upbit.com/), [CoinEx](https://www.coinex.com/), [Poloniex](https://poloniex.com/), and [ChangeNOW](https://changenow.io/). -To buy: -- Click "Buy Steem" from the main menu in the top right corner of steemit.com, or from your wallet. -- Select the currency to deposit, and enter the amount of that currency you wish to use. -- Enter your Steemit account name (without the @) for "Your receive address". -- Click the "Get Deposit Address" button. -- Send the currency to the provided address. - -bitcoin can also be exchanged for STEEM on external markets such as [Binance](https://www.binance.com/), [Bithumb](https://www.bithumb.com/), [BitShares](https://wallet.bitshares.org/), [Bittrex](https://bittrex.com), [Changelly](https://changelly.com), [GOBADA](https://www.gobaba.com/), [HitBTC](https://hitbtc.com/), [Huobi](https://www.huobi.pro/), [LocalBitcoinCash](https://www.localbitcoincash.org/), [Poloniex](https://poloniex.com), [Shapeshift.io](https://shapeshift.io), [UpBit](https://upbit.com/), and [Yensesa](https://yensesa.com). ^ ## How long does it take STEEM or Steem Power that I purchased to show up in my account? Transactions on the Steem blockchain typically only take about three seconds to process, but when you are purchasing the STEEM tokens using bitcoin or some other token, then the transaction must wait for the transaction to be confirmed on the other network. This can take several hours, and sometimes even days. -If you paid using bitcoin, the third party website bitcoinfees.21.co can estimate the approximate wait time of the transaction based on the fees that were paid. The third party website blockchain.info will lookup the fees that were paid on a specific blockchain transaction. +If you paid using bitcoin, the third party website btc.network can estimate the approximate wait time of the transaction based on the fees that were paid. The third party website blockchain.info will lookup the fees that were paid on a specific blockchain transaction. ^ ## What is powering up and down? @@ -748,6 +776,13 @@ If you paid using bitcoin, the third party website ^ +## What is a withdraw route? + +A withdraw route on Steem is simply an instruction on your Steem account that tells the blockchain where your weekly power down payouts should go. When Steem Power (SP) is powered down, the withdrawn amount is divided according to the configured routes, if they exist, and each destination may receive either liquid STEEM or additional SP via automatic power up. Users can also remove routes at any time, regardless of whether the power down process is in progress. + +The number of permissible routes is defined by the blockchain. The account holder can allocate up to a total of 100% across all routes. If the configured routes sum to 100%, the entire payout is redirected to designated recipients; if the sum is below 100%, the remaining percentage normally returns to the owner’s account as STEEM, unless specified otherwise. + ^ ## What do the dollar amounts for pending payouts represent? @@ -810,15 +845,6 @@ It is recommended that you withdraw a small amount first, to verify it works bef #### Sell Steem Dollars via Poloniex https://steemit.com/steemit/@ash/steemit-how-to-sell-steem-dollars-via-poloniex-newbie-friendly -#### Withdraw Steem Dollars to a Bitcoin address -https://steemit.com/steem-help/@piedpiper/how-to-withdraw-your-steem-dollars-in-less-that-a-minute - -#### Convert Steem Dollars to a country’s currency and withdraw to a bank account -https://steemit.com/tutorial/@beanz/how-to-get-my-usdteemit-money-into-my-bank-account - -#### Convert STEEM to many other cryptocurrencies via ShapeShift -https://steemit.com/steemit/@shapeshiftio/official-announcement-shapeshift-has-added-steem-to-the-exchange - ^ ## Will I get a 1099 from Steemit? @@ -1049,7 +1075,6 @@ Normally everyone's bandwidth allowance is quite high, and users are able to use You can check how much bandwidth you currently have based on the current limit at: https://steemdb.io/@youraccount - If users are below their bandwidth limit, they will be unable to transact with the blockchain until their bandwidth recharges or their limit is raised. If you get an error that you have exceeded your bandwidth allowance, it is normally best to just wait and try again later (when it is less busy). Usually if you wait and try again later, the transaction will likley go through. @@ -1089,7 +1114,6 @@ The Steem blockchain schedules witnesses to produce a new block every 3 seconds. Yes. The blockchain data can be viewed in different ways with third-party tools such as steemdb.io - ^ ## Where can I find the information for the official launch of the blockchain? @@ -1257,7 +1281,7 @@ The Steem blockchain requires a set of people to create blocks and uses a consen ^ ## How can I vote for witnesses? -Visit https://steemit.com/~witnesses. +Visit https://steemitwallet.com/~witnesses, or https://steemitwallet.com/@youraccount/witnesses. ^ ## How many witnesses can I vote for? @@ -1279,7 +1303,7 @@ The SPS (DAO) is funded by 10% of the annual token inflation. These funds are he ^ ## How to create or cancel a proposal? -To submit a proposal to the Steem DAO, community members must complete the official form available through the Steemit Wallet interface. Required fields include the proposal title, daily requested amount in SBD, start and end dates, a valid proposal permlink (linking to a post that outlines the proposal), the creator’s username, and the intended receiver’s username. +To submit a proposal to the Steem DAO, community members must complete the official form available through the Steemit Wallet interface or their account dashboard. The required fields include the proposal title, the daily requested amount in SBD, the start and end dates, a valid proposal permlink (linking to the post that outlines the proposal), the creator’s username, and the intended receiver’s username. Proposal creation is subject to a submission fee. The exact amount is defined by the blockchain protocol and is displayed during the proposal creation process. Once submitted, the proposal becomes active and open for voting by Steem Power holders. @@ -1306,7 +1330,7 @@ No. Once submitted to the DAO, a proposal cannot be edited or updated. It can on ^ ## Where can community members view active proposals? -Active and votable proposals can be viewed on the official Steemit Wallet interface. +Active and votable proposals can be viewed on the official Steemit Wallet interface or on an account’s dashboard. ^ @@ -1314,7 +1338,6 @@ Active and votable proposals can be viewed on the official What third-party tools are there for Steemit? - ^ ## Is there an official Steemit Facebook page? @@ -1344,10 +1367,10 @@ https://steem.io/SteemWhitePaper.pdf ## Third Party References and User Links -Binance, bitcoinfees, Bitcointalk, Bithumb, BitShares, Bittrex, blockchain.info, Busy.org, Changelly, @cheetah, Coinbase, DTube, GOBADA, HitBTC, Huobi, LocalBitcoinCash, Localbitcoins, Markdown Cheatsheet, Pexels, Pixabay, Poloniex, Postimage, Shapeshift.io, Steemcleaners, SteemCreate, steemd, SteemStats, The Steemit Shop, UpBit, Utopian.io, Vessel, and Yensesa are third party applications/services, and are not owned or maintained by Steemit, Inc. Their listing here, as well as any other third party applications or websites that are listed, does not constitute and endorsement or recommendation on behalf of Steemit, Inc. +Binance, bitcoinfees, Bitcointalk, Bithumb, blockchain.info, DTube, Huobi (HTX), Markdown Cheatsheet, Pexels, Pixabay, Poloniex, Postimage, SteemCreate, SteemStats, UpBit, Vessel, WhiteBIT, MEXC, Gate, WEEX, BitKan, DigiFinex, CoinEx, ProBit Global, ChangeNOW, CoinDCX, Bitexen, ZKE, CoinUp.io, ONUS Pro, Koinbay, Pionex, SteemPro, SteemX, Boy, UPVU, as well as other third-party applications and services, are not owned or maintained by Steemit, Inc. Their listing here, as well as any other third-party applications or websites that are referenced, does not constitute an endorsement or recommendation on behalf of Steemit, Inc. All links to user posts were created by our users and do not necessarily represent the views of Steemit, Inc. or its management. Their listing here does not constitute and endorsement or recommendation on behalf of Steemit, Inc. Please use the third party tools and content at your own risk. -^ +^ \ No newline at end of file diff --git a/src/app/help/en/welcome.md b/src/app/help/en/welcome.md index bff04278c..12c9d43e8 100644 --- a/src/app/help/en/welcome.md +++ b/src/app/help/en/welcome.md @@ -272,7 +272,6 @@ Don't get discouraged if you don't earn much at first. Keep up the good work! ## Users to Follow - @steemitblog - Official Steemit Announcements -- @ned - Ned Scott, CEO and Co-Founder of Steemit, Inc. ^ ## Other Resources @@ -283,12 +282,9 @@ Don't get discouraged if you don't earn much at first. Keep up the good work! - [Apps Built on Steem] will come up soon - Directory of apps, sites and tools built by Steem community - [Steem Blockchain Explorer](https://Steemdb.io/) - Analysis pages for the Steem blockchain data - ^ ## Get Help - - New Member Support Community is a group of people dedicated to helping new users find their way around Steemit. You can find them in the [New Member Support Community](https://discord.gg/HYj4yvw) channel of Discord Chat. ^ diff --git a/src/app/locales/en.json b/src/app/locales/en.json index a9ca302bd..551109a56 100644 --- a/src/app/locales/en.json +++ b/src/app/locales/en.json @@ -85,6 +85,7 @@ "previous": "Previous", "price": "Price", "print": "Print", + "proposals": "Proposals", "promote": "Promote", "promoted": "Promoted", "re": "RE", @@ -597,7 +598,9 @@ "witness_proxy_current": "Your current proxy is", "witness_proxy_set": "Set proxy", "witness_proxy_clear": "Clear proxy", - "proxy_update_error": "Your proxy was not updated" + "proxy_update_error": "Your proxy was not updated", + "witnesses_description": "Witnesses are trusted accounts that run and secure the Steem blockchain.", + "witnesses_learn_more": "Learn more in the FAQ." }, "votesandcomments_jsx": { "no_responses_yet_click_to_respond": @@ -721,7 +724,11 @@ "This is a price feed conversion. The 3.5 day delay is necessary to prevent abuse from gaming the price feed average.", "convert_to_LIQUID_TOKEN": "Convert to %(LIQUID_TOKEN)s", "DEBT_TOKEN_will_be_unavailable": - "The conversion process takes 3.5 days to complete and cannot be canceled once initiated. During this period, the specified %(DEBT_TOKEN)s (SBD) amount becomes immediately unavailable. The final amount of STEEM received is not fixed, as it is determined by the median witness price feed averaged over the entire 3.5-day window. As a result, conversions carry risk, particularly during periods of price volatility. Due to this, exercising due diligence before initiating a conversion is strongly advised." + "The conversion process takes 3.5 days to complete and cannot be canceled once initiated. During this period, the specified %(DEBT_TOKEN)s (SBD) amount becomes immediately unavailable. The final amount of STEEM received is not fixed, as it is determined by the median witness price feed averaged over the entire 3.5-day window. As a result, conversions carry risk, particularly during periods of price volatility. Due to this, exercising due diligence before initiating a conversion is strongly advised.", + "current_conversions": "Current Conversions", + "no_conversion_data": "No conversion data available.", + "id": "ID", + "request_id": "Request ID" }, "tips_js": { "liquid_token": @@ -1040,6 +1047,8 @@ "power_down": "Power Down", "delegate": "Delegate", "advanced_routes": "Advanced Routes", + "set_advanced_routes": "Set Withdraw Route", + "advanced_routes_visit_faq":"Set or update withdraw routes. Visit the FAQ for more details.", "route_settings": "Route Settings", "market": "Market", "convert_to_LIQUID_TOKEN": "Convert to %(LIQUID_TOKEN)s", @@ -1055,6 +1064,8 @@ "estimated_account_value": "Estimated Account Value", "next_power_down_is_scheduled_to_happen": "The next power down is scheduled to happen", + "routed_to_single": "100%% routed to the following account: ", + "view_all_withdraw_routes": "View all withdraw routes", "transfers_are_temporary_disabled": "Transfers are temporary disabled.", "history": "HISTORY", "redeem_rewards": "Redeem Rewards (Transfer to Balance)", @@ -1063,6 +1074,26 @@ "incorrect_account_format": "Incorrect account format", "confirm_route_setup": "Are you sure you want to set up %(percent)s%% of your Steem Power withdrawal to go to @%(account)s?" }, + "advanced_routes": { + "from": "From", + "to": "To", + "percent": "Percentage", + "current_routes": "Current Withdraw Routes (%(accounts_number)s left)", + "current_withdraw_route": "Current Withdraw Routes", + "account": "Account", + "percentage": "Percentage", + "receive": "Receive As", + "receive_amount": "Receive", + "remove": "Remove", + "steem_power": "Steem Power", + "steem": "STEEM", + "no_routes": "No withdraw routes are set.", + "not_remaining_routes": "No additional routes can be added due to a blockchain limitation.", + "not_remaining_percentage": "All 100%% has already been distributed. No additional routes can be added.", + "withdraw_routes_detected": "Active withdraw routes detected on your account. Review now to prevent assets loss.", + "take_action": "Take Action", + "acknowledge_routes": "I have read and acknowledge the current withdraw routes." + }, "powerdown_jsx": { "power_down": "Power Down", "amount": "Amount", @@ -1075,6 +1106,12 @@ "Leaving less than %(AMOUNT)s %(VESTING_TOKEN)s in your account is not recommended and can leave your account in a unusable state.", "error": "Unable to power down (ERROR: %(MESSAGE)s)" }, + "powerdown_alert": { + "message": "%(percent)s%% of your Steem Power (%(amount)s SP) is currently being powered down.", + "high_risk_label": "High risk", + "learn_more": "Learn more", + "aria_label": "Power-down alert" + }, "checkloginowner_jsx": { "your_password_permissions_were_reduced": "Your password permissions were reduced", diff --git a/src/app/redux/GlobalReducer.js b/src/app/redux/GlobalReducer.js index 30ec262da..d1f2a7a3d 100644 --- a/src/app/redux/GlobalReducer.js +++ b/src/app/redux/GlobalReducer.js @@ -14,6 +14,7 @@ const RECEIVE_STATE = 'global/RECEIVE_STATE'; const RECEIVE_ACCOUNT = 'global/RECEIVE_ACCOUNT'; const RECEIVE_ACCOUNTS = 'global/RECEIVE_ACCOUNTS'; const UPDATE_ACCOUNT_WITNESS_VOTE = 'global/UPDATE_ACCOUNT_WITNESS_VOTE'; +const UPDATE_ACCOUNT_WITNESS_VOTES = 'global/UPDATE_ACCOUNT_WITNESS_VOTES'; const UPDATE_ACCOUNT_WITNESS_PROXY = 'global/UPDATE_ACCOUNT_WITNESS_PROXY'; const FETCHING_DATA = 'global/FETCHING_DATA'; const RECEIVE_DATA = 'global/RECEIVE_DATA'; @@ -130,6 +131,14 @@ export default function reducer(state = defaultState, action = {}) { ); } + case UPDATE_ACCOUNT_WITNESS_VOTES: { + const { account, witness_votes } = action.payload; + return state.setIn( + ['accounts', account, 'witness_votes'], + Set(witness_votes) + ); + } + case UPDATE_ACCOUNT_WITNESS_PROXY: { const { account, proxy } = payload; return state.setIn(['accounts', account, 'proxy'], proxy); @@ -349,6 +358,11 @@ export const updateAccountWitnessVote = payload => ({ payload, }); +export const updateAccountWitnessVotes = payload => ({ + type: UPDATE_ACCOUNT_WITNESS_VOTES, + payload, +}); + export const updateAccountWitnessProxy = payload => ({ type: UPDATE_ACCOUNT_WITNESS_PROXY, payload, diff --git a/src/app/redux/TransactionReducer.js b/src/app/redux/TransactionReducer.js index 7493cc9b7..2bb2e4ec5 100644 --- a/src/app/redux/TransactionReducer.js +++ b/src/app/redux/TransactionReducer.js @@ -19,6 +19,7 @@ export const UPDATE_PRICES = 'transaction/UPDATE_PRICES'; export const SET_PRICES = 'transaction/SET_PRICES'; // Saga-related export const RECOVER_ACCOUNT = 'transaction/RECOVER_ACCOUNT'; +export const FETCH_ACCOUNT_WITNESS_VOTES = 'transaction/FETCH_ACCOUNT_WITNESS_VOTES'; const defaultState = fromJS({ operations: [], status: { key: '', error: false, busy: false }, @@ -271,3 +272,8 @@ export const setPrices = payload => ({ type: SET_PRICES, payload, }); + +export const fetchAccountWitnessVotes = payload => ({ + type: FETCH_ACCOUNT_WITNESS_VOTES, + payload, +}); diff --git a/src/app/redux/TransactionSaga.js b/src/app/redux/TransactionSaga.js index 53239fe79..38ac0c9a9 100644 --- a/src/app/redux/TransactionSaga.js +++ b/src/app/redux/TransactionSaga.js @@ -33,6 +33,7 @@ export const transactionWatches = [ takeEvery(transactionActions.UPDATE_AUTHORITIES, updateAuthorities), takeEvery(transactionActions.RECOVER_ACCOUNT, recoverAccount), takeEvery(transactionActions.UPDATE_PRICES, updatePricesSaga), + takeEvery(transactionActions.FETCH_ACCOUNT_WITNESS_VOTES, refreshAccountWitnessVotes), ]; const hook = { @@ -169,6 +170,17 @@ function* error_account_witness_vote({ ); } +export function* refreshAccountWitnessVotes({ payload: { accountName } }) { + try { + const [account] = yield call([api, api.getAccountsAsync], [accountName]); + if (account) { + yield put(globalActions.updateAccountWitnessVotes({account: account.name, witness_votes: account.witness_votes || []})); + } + } catch (err) { + console.error('Error refreshing witness_votes:', err); + } +} + /** Keys, username, and password are not needed for the initial call. This will check the login and may trigger an action to prompt for the password / key. */ export function* broadcastOperation({ payload: { @@ -746,3 +758,4 @@ export function* updateAuthorities({ }; yield call(broadcastOperation, { payload }); } + diff --git a/src/app/redux/TransactionSaga.test.js b/src/app/redux/TransactionSaga.test.js index 0c0121c94..3ce195307 100644 --- a/src/app/redux/TransactionSaga.test.js +++ b/src/app/redux/TransactionSaga.test.js @@ -12,7 +12,8 @@ import { transactionWatches, broadcastOperation, updateAuthorities, - updatePricesSaga + updatePricesSaga, + refreshAccountWitnessVotes } from './TransactionSaga'; import { DEBT_TICKER } from 'app/client_config'; @@ -61,6 +62,7 @@ describe('TransactionSaga', () => { transactionActions.UPDATE_PRICES, updatePricesSaga ), + takeEvery(transactionActions.FETCH_ACCOUNT_WITNESS_VOTES, refreshAccountWitnessVotes), ]); }); }); diff --git a/src/app/redux/UserSaga.js b/src/app/redux/UserSaga.js index 8c7968e0c..1410b051a 100644 --- a/src/app/redux/UserSaga.js +++ b/src/app/redux/UserSaga.js @@ -86,7 +86,7 @@ export function* getWithdrawRoutes(action) { const highSecurityPages = [ /\/market/, - /\/@.+\/(transfers|permissions|password|communities|delegations)/, + /\/@.+\/(transfers|permissions|password|communities|delegations|proposals|witnesses)/, /\/~witnesses/, /\/proposals/, ]; diff --git a/src/app/utils/StateFunctions.js b/src/app/utils/StateFunctions.js index f59b6f2b5..af3dbac05 100644 --- a/src/app/utils/StateFunctions.js +++ b/src/app/utils/StateFunctions.js @@ -124,6 +124,13 @@ export function isFetchingOrRecentlyUpdated(global_status, order, category) { return false; } +export function getRedirectPagePath(location) { + if (location.match(/^\/(@[\w\.\d-]+)\/(witnesses)\/?$/)) { + return { location: '/~witnesses', search_account: location.replace('/witnesses', '/transfers') }; + } + return { location, search_account: false }; +} + export function contentStats(content) { if (!content) return {}; if (!(content instanceof Map)) content = fromJS(content); diff --git a/src/app/utils/VerifiedExchangeList.js b/src/app/utils/VerifiedExchangeList.js index 43379f9b8..db47311a9 100644 --- a/src/app/utils/VerifiedExchangeList.js +++ b/src/app/utils/VerifiedExchangeList.js @@ -2,7 +2,6 @@ const list = ` bittrex blocktrades changelly -deepcrypto8 gopax-deposit hitbtc-exchange poloniex @@ -17,7 +16,6 @@ gateiodeposit bingxsteem probitsteem coinexofficial -whitebit rudex cold.dunamu binance-hot2 @@ -31,7 +29,6 @@ hot5.dunamu hot1.dunamu polopw-01 htx-s8vznt82 -bittrex user.dunamu deepcrypto8 bithumbrecv1 diff --git a/src/app/utils/steemApi.js b/src/app/utils/steemApi.js index 17e96118f..0bfda856b 100644 --- a/src/app/utils/steemApi.js +++ b/src/app/utils/steemApi.js @@ -1,13 +1,19 @@ import { api } from '@steemit/steem-js'; - +import { getRedirectPagePath } from './StateFunctions' import stateCleaner from 'app/redux/stateCleaner'; +import Witnesses from '../components/modules/Witnesses'; export async function getStateAsync(url) { // strip off query string const path = url.split('?')[0]; - - const raw = await api.getStateAsync(path); - + let raw; + if (path.match(/^\/(@[\w\.\d-]+)\/(witnesses)\/?$/)) { + raw = await api.getStateAsync(path.replace('/witnesses', '/transfers')); + let witnesses = await api.getStateAsync('/~witnesses'); + raw.witnesses = witnesses ? witnesses.witnesses : raw.witnesses + } else { + raw = await api.getStateAsync(path); + } const cleansed = stateCleaner(raw); return cleansed;