Skip to content
Snippets Groups Projects
Commit 4cec89ee authored by Gandalf's avatar Gandalf
Browse files

Merge branch 'witnesses-page' into 'autoclave'

Refresh Witnesses page design

See merge request !7
parents f56097a2 57f61ac4
No related branches found
No related tags found
2 merge requests!11merge autoclave into master,!7Refresh Witnesses page design
......@@ -57,6 +57,7 @@ const icons_map = {};
for (const i of icons) icons_map[i] = require(`assets/icons/${i}.svg`);
const rem_sizes = {
'0.7x': '0.7',
'1x': '1.12',
'1_5x': '1.5',
'2x': '2',
......@@ -69,7 +70,16 @@ const rem_sizes = {
export default class Icon extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
size: PropTypes.oneOf(['1x', '1_5x', '2x', '3x', '4x', '5x', '10x']),
size: PropTypes.oneOf([
'0.7x',
'1x',
'1_5x',
'2x',
'3x',
'4x',
'5x',
'10x',
]),
inverse: PropTypes.bool,
className: PropTypes.string,
};
......
import React, { Component } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import shouldComponentUpdate from 'app/utils/shouldComponentUpdate';
......@@ -20,7 +21,7 @@ class Userpic extends Component {
shouldComponentUpdate = shouldComponentUpdate(this, 'Userpic');
render() {
const { account, json_metadata, size } = this.props;
const { account, json_metadata, size, className = '' } = this.props;
const hideIfDefault = this.props.hideIfDefault || false;
const avSize = size && sizeList.indexOf(size) > -1 ? '/' + size : '';
......@@ -41,7 +42,9 @@ class Userpic extends Component {
'url(' + imageProxy() + `u/${account}/avatar${avSize})`,
};
return <div className="Userpic" style={style} />;
return (
<div className={classnames('Userpic', className)} style={style} />
);
}
}
......
......@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import Moment from 'moment';
import NumAbbr from 'number-abbreviate';
import tt from 'counterpart';
import Userpic from 'app/components/elements/Userpic';
import { numberWithCommas, vestsToHpf } from 'app/utils/StateFunctions';
import Icon from 'app/components/elements/Icon';
......@@ -59,11 +60,7 @@ export default function Proposal(props) {
</a>
</div>
<div className="proposals__avatar">
<img
src={`https://images.hive.blog/100x100/https://images.hive.blog/u/${creator}/avatar`}
alt={creator}
className="image__round"
/>
<Userpic account={creator} />
</div>
<div className="proposals__description">
<span>
......
......@@ -13,10 +13,6 @@
margin-left: auto;
margin-right: auto;
img.image__round {
border-radius: 50px;
}
.proposals__header {
flex-direction: row;
justify-content: center;
......@@ -86,6 +82,10 @@
width: 65px;
height: 65px;
padding-left: 15px;
@media only screen and (max-width: 768px) {
width: 90px;
}
}
.proposals__votes {
......
import React from 'react';
import Moment from 'moment';
import { api } from '@steemit/steem-js';
import classnames from 'classnames';
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 Userpic from 'app/components/elements/Userpic';
import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper';
import { formatLargeNumber } from 'app/utils/ParsersAndFormatters';
import ByteBuffer from 'bytebuffer';
import { is, Set, List } from 'immutable';
import * as globalActions from 'app/redux/GlobalReducer';
import { vestsToHpf } from 'app/utils/StateFunctions';
import tt from 'counterpart';
import _ from 'lodash';
const Long = ByteBuffer.Long;
const { string, func, object } = PropTypes;
......@@ -18,6 +26,7 @@ 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 < 60) return 'just now';
if (secs < 120) return 'recently';
const mins = Math.floor(secs / 60);
if (mins < 120) return mins + ' mins ago';
......@@ -42,7 +51,13 @@ class Witnesses extends React.Component {
constructor() {
super();
this.state = { customUsername: '', proxy: '', proxyFailed: false };
this.state = {
customUsername: '',
proxy: '',
proxyFailed: false,
witnessAccounts: {},
witnessToHighlight: '',
};
this.accountWitnessVote = (accountName, approve, e) => {
e.preventDefault();
const { username, accountWitnessVote } = this.props;
......@@ -64,6 +79,15 @@ class Witnesses extends React.Component {
};
}
componentDidMount() {
this.setState({
witnessToHighlight: this.props.location.query.highlight,
});
this.loadWitnessAccounts();
this.scrollToHighlightedWitness();
}
shouldComponentUpdate(np, ns) {
return (
!is(np.witness_votes, this.props.witness_votes) ||
......@@ -73,10 +97,75 @@ class Witnesses extends React.Component {
np.username !== this.props.username ||
ns.customUsername !== this.state.customUsername ||
ns.proxy !== this.state.proxy ||
ns.proxyFailed !== this.state.proxyFailed
ns.proxyFailed !== this.state.proxyFailed ||
ns.witnessAccounts !== this.state.witnessAccounts ||
ns.witnessToHighlight !== this.state.witnessToHighlight
);
}
async loadWitnessAccounts() {
const witnessAccounts = this.state.witnessAccounts;
const { witnesses } = this.props;
const witnessOwners = [[]];
let chunksCount = 0;
witnesses.map(item => {
if (witnessOwners[chunksCount].length >= 20) {
chunksCount += 1;
witnessOwners[chunksCount] = [];
}
witnessOwners[chunksCount].push(item.get('owner'));
return true;
});
for (let oi = 0; oi < witnessOwners.length; oi += 1) {
const owners = witnessOwners[oi];
const res = await api.getAccountsAsync(owners);
if (!(res && res.length > 0)) {
console.error(tt('g.account_not_found'));
return false;
}
for (let ri = 0; ri < res.length; ri += 1) {
const witnessAccount = res[ri];
let jsonMetadata = { witness_description: '' };
if (
witnessAccount.hasOwnProperty('json_metadata') &&
witnessAccount.json_metadata
) {
jsonMetadata = JSON.parse(witnessAccount.json_metadata);
}
witnessAccounts[witnessAccount.name] = jsonMetadata;
}
}
this.setState({ witnessAccounts: { ...witnessAccounts } });
return true;
}
scrollToHighlightedWitness() {
if (typeof document !== 'undefined') {
setTimeout(() => {
const highlightedWitnessElement = document.querySelector(
'.Witnesses__highlight'
);
if (highlightedWitnessElement) {
highlightedWitnessElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
}
}, 1000);
}
}
updateWitnessToHighlight(witness) {
this.setState({ witnessToHighlight: witness });
this.scrollToHighlightedWitness();
window.history.pushState('', '', `/~witnesses?highlight=${witness}`);
}
render() {
const {
props: {
......@@ -85,10 +174,16 @@ class Witnesses extends React.Component {
current_proxy,
head_block,
},
state: { customUsername, proxy },
state: {
customUsername,
proxy,
witnessAccounts,
witnessToHighlight,
},
accountWitnessVote,
accountWitnessProxy,
onWitnessChange,
updateWitnessToHighlight,
} = this;
const sorted_witnesses = this.props.witnesses.sort((a, b) =>
Long.fromString(String(b.get('votes'))).subtract(
......@@ -97,16 +192,71 @@ class Witnesses extends React.Component {
);
let witness_vote_count = 30;
let rank = 1;
let foundWitnessToHighlight = false;
let previousTotalVoteHpf = 0;
const witnesses = sorted_witnesses.map(item => {
const owner = item.get('owner');
const thread = item.get('url');
if (owner === witnessToHighlight) {
foundWitnessToHighlight = true;
}
const witnessDescription = _.get(
witnessAccounts[owner],
'profile.witness_description',
null
);
const totalVotesVests = item.get('votes');
const totalVotesHpf = vestsToHpf(
this.props.state,
`${totalVotesVests / 1000000} VESTS`
);
const totalVotesHp = formatLargeNumber(totalVotesHpf, 0);
let requiredHpToRankUp = '';
if (previousTotalVoteHpf !== 0) {
requiredHpToRankUp = (
<small>
{tt('witnesses_jsx.hp_required_to_rank_up', {
votehp: formatLargeNumber(
previousTotalVoteHpf - totalVotesHpf
),
})}
</small>
);
}
previousTotalVoteHpf = totalVotesHpf;
const thread = item.get('url').replace('steemit.com', 'hive.blog');
const myVote = witness_votes ? witness_votes.has(owner) : null;
const signingKey = item.get('signing_key');
const witnessCreated = item.get('created');
const accountBirthday = Moment.utc(`${witnessCreated}Z`).format(
'll'
);
const now = Moment();
const witnessAgeDays = now.diff(accountBirthday, 'days');
const witnessAgeWeeks = now.diff(accountBirthday, 'weeks');
const witnessAgeMonths = now.diff(accountBirthday, 'months');
const witnessAgeYears = now.diff(accountBirthday, 'years');
let witnessAge = `${witnessAgeDays} ${tt('g.days')}`;
if (witnessCreated === '1970-01-01T00:00:00') {
witnessAge = 'over 3 years';
} else if (witnessAgeYears > 0) {
witnessAge = `${witnessAgeYears} ${tt('g.years')}`;
} else if (witnessAgeMonths > 0) {
witnessAge = `${witnessAgeMonths} ${tt('g.months')}`;
} else if (witnessAgeWeeks > 0) {
witnessAge = `${witnessAgeWeeks} ${tt('g.weeks')}`;
}
const lastBlock = item.get('last_confirmed_block_num');
const runningVersion = item.get('running_version');
const sbdExchangeRate = item.get('sbd_exchange_rate');
const sbdExchangeUpdateDate = item.get('last_sbd_exchange_update');
const noBlock7days = (head_block - lastBlock) * 3 > 604800;
const isDisabled =
signingKey == DISABLED_SIGNING_KEY || noBlock7days;
const isDisabled = signingKey == DISABLED_SIGNING_KEY;
const votingActive = witnessVotesInProgress.has(owner);
const classUp =
'Voting__button Voting__button-up' +
......@@ -143,8 +293,13 @@ class Witnesses extends React.Component {
: {};
return (
<tr key={owner}>
<td width="75">
<tr
key={owner}
className={classnames({
Witnesses__highlight: witnessToHighlight === owner,
})}
>
<td className="Witnesses__rank">
{rank < 10 && '0'}
{rank++}
&nbsp;&nbsp;
......@@ -170,19 +325,110 @@ class Witnesses extends React.Component {
)}
</span>
</td>
<td>
<td className="Witnesses__info">
<Link to={'/@' + owner} style={ownerStyle}>
{owner}
<Userpic
account={owner}
size="small"
className={classnames({
disabled: isDisabled,
})}
/>
</Link>
{isDisabled && (
<small>
{' '}
({tt('witnesses_jsx.disabled')}{' '}
{_blockGap(head_block, lastBlock)})
</small>
)}
<div className="Witnesses__info">
<div>
<Link to={'/@' + owner} style={ownerStyle}>
{owner}
</Link>
<Link
to={`/~witnesses?highlight=${owner}`}
onClick={event => {
event.preventDefault();
updateWitnessToHighlight.apply(this, [
owner,
]);
}}
>
<Icon
name="chain"
size="0.7x"
className="Witnesses__permlink"
/>
</Link>
</div>
<div>
<small>
{noBlock7days && (
<div>
<strong>
<span
role="img"
aria-label={tt(
'witnesses_jsx.not_produced_over_a_week'
)}
>
⚠️
</span>
{tt(
'witnesses_jsx.not_produced_over_a_week'
)}
</strong>
</div>
)}
<div>
{witnessDescription && (
<div className="Witnesses__description">
{witnessDescription}
</div>
)}
{tt('witnesses_jsx.last_block')}{' '}
<Link
to={`https://hiveblocks.com/b/${lastBlock}`}
target="_blank"
>
#{lastBlock}
</Link>{' '}
{_blockGap(head_block, lastBlock)} on v{
runningVersion
}
</div>
{isDisabled && (
<div>
{`${tt(
'witnesses_jsx.disabled'
)} ${_blockGap(
head_block,
lastBlock
)}`}
</div>
)}
{!isDisabled && (
<div>
{`${tt(
'witnesses_jsx.witness_age'
)}: ${witnessAge}`}
</div>
)}
</small>
</div>
{!isDisabled && (
<div className="witness__thread">
<small>{witness_link}</small>
</div>
)}
</div>
</td>
<td>
{`${totalVotesHp} HP`}
{!isDisabled && <div>{requiredHpToRankUp}</div>}
</td>
<td>
${parseFloat(sbdExchangeRate.get('base'))}
<br />
<small>
<TimeAgoWrapper date={sbdExchangeUpdateDate} />
</small>
</td>
<td>{witness_link}</td>
</tr>
);
});
......@@ -246,17 +492,19 @@ class Witnesses extends React.Component {
<div className="column">
<h2>{tt('witnesses_jsx.top_witnesses')}</h2>
{current_proxy && current_proxy.length ? null : (
<p>
<strong>
<div>
<p>
<strong>
{tt(
'witnesses_jsx.you_have_votes_remaining',
{ count: witness_vote_count }
)}.
</strong>{' '}
{tt(
'witnesses_jsx.you_have_votes_remaining',
{ count: witness_vote_count }
'witnesses_jsx.you_can_vote_for_maximum_of_witnesses'
)}.
</strong>{' '}
{tt(
'witnesses_jsx.you_can_vote_for_maximum_of_witnesses'
)}.
</p>
</p>
</div>
)}
</div>
</div>
......@@ -266,11 +514,12 @@ class Witnesses extends React.Component {
<table>
<thead>
<tr>
<th />
<th>Rank</th>
<th>{tt('witnesses_jsx.witness')}</th>
<th>
{tt('witnesses_jsx.information')}
<th className="Witnesses__votes">
{tt('witnesses_jsx.votes_received')}
</th>
<th>Price feed</th>
</tr>
</thead>
<tbody>{witnesses.toArray()}</tbody>
......@@ -280,7 +529,13 @@ class Witnesses extends React.Component {
)}
{current_proxy ? null : (
<div className="row">
<div
className={classnames('row', {
Witnesses__highlight:
witnessToHighlight &&
foundWitnessToHighlight === false,
})}
>
<div className="column">
<p>
{tt(
......@@ -298,7 +553,11 @@ class Witnesses extends React.Component {
width: '75%',
maxWidth: '20rem',
}}
value={customUsername}
value={
foundWitnessToHighlight === true
? customUsername
: witnessToHighlight
}
onChange={onWitnessChange}
/>
<div className="input-group-button">
......@@ -438,6 +697,7 @@ module.exports = {
witness_votes,
witnessVotesInProgress,
current_proxy,
state,
};
},
dispatch => {
......
.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');
}
}
td > a {
@extend .link;
@extend .link--primary;
@include themify($themes) {
fill: themed('textColorAccentHover');
}
}
table tbody tr.Witnesses__highlight {
background-color: pink;
}
td.Witnesses__info {
display: flex;
flex-direction: row;
}
td.Witnesses__rank {
white-space: nowrap;
}
td.Witnesses__info .Userpic {
@media only screen and (max-width: 779px) {
display: none;
}
}
td.Witnesses__info .Userpic.disabled {
opacity: 0.4;
}
.button {
background-color: $color-text-hive-red;
text-shadow: 0 1px 0 rgba(0,0,0,0.20);
......@@ -24,7 +36,43 @@
&:hover {
background-color: $color-hive-red;
}
}
}
div.Witnesses__info {
display: flex;
flex-direction: column;
margin-left: 15px;
@media only screen and (max-width: 779px) {
margin-left: 0;
}
}
div.Witnesses__info a:first-child {
font-weight: bold;
}
div.Witnesses__info .witness__thread a {
@extend .link;
@extend .link--primary;
}
div.Witnesses__highlight form input.input-group-field {
box-shadow: 0px 0px 3px 3px pink;
}
div.Witnesses__description {
display: block;
width: calc(100% - 20px);
max-width: 660px;
font-style: italic;
max-height: 5em;
overflow-x: hidden;
overflow-y: auto;
margin-left: 20px;
margin-bottom: 10px;
border-bottom: 1px dotted #dcdcdc;
}
.Icon.Witnesses__permlink svg {
width: 0.7rem;
height: 0.7rem;
margin-left: 10px;
}
}
......@@ -252,7 +252,11 @@
"warning":
"Legitimate users, including employees of Hive, will never ask you for a private key or master password.",
"checkbox": "I understand"
}
},
"days": "days",
"weeks": "weeks",
"months": "months",
"years": "years"
},
"navigation": {
"about": "About Hive",
......@@ -475,9 +479,9 @@
}
},
"witnesses_jsx": {
"witness_thread": "witness thread",
"external_site": "external site",
"disabled": "disabled",
"witness_thread": "Open witness annoucement",
"external_site": "Open external site",
"disabled": "Disabled",
"top_witnesses": "Witness Voting",
"you_have_votes_remaining": {
"zero": "You have no votes remaining",
......@@ -487,7 +491,7 @@
"you_can_vote_for_maximum_of_witnesses":
"You can vote for a maximum of 30 witnesses",
"witness": "Witness",
"information": "Information",
"information": "More Information",
"if_you_want_to_vote_outside_of_top_enter_account_name":
"If you would like to vote for a witness outside of the top 100, enter the account name below to cast a vote",
"set_witness_proxy":
......@@ -497,7 +501,14 @@
"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",
"missed_blocks": "Missed blocks",
"votes_received": "Votes received",
"hp_required_to_rank_up": "Needs %(votehp)s to level up",
"witness_age": "Witness age",
"not_produced_over_a_week":
"Has not produced any blocks for over a week.",
"last_block": "Last block"
},
"votesandcomments_jsx": {
"no_responses_yet_click_to_respond":
......
......@@ -77,7 +77,32 @@ export function countDecimals(amount) {
const parts = amount.split('.');
return parts.length > 2
? undefined
: parts.length === 1 ? 0 : parts[1].length;
: parts.length === 1
? 0
: parts[1].length;
}
export function formatLargeNumber(number, decimals) {
const symbols = [
{ value: 1, symbol: '' },
{ value: 1e3, symbol: 'k' },
{ value: 1e6, symbol: 'M' },
{ value: 1e9, symbol: 'G' },
{ value: 1e12, symbol: 'T' },
{ value: 1e15, symbol: 'P' },
{ value: 1e18, symbol: 'E' },
];
const regexp = /\.0+$|(\.[0-9]*[1-9])0+$/;
for (let i = symbols.length - 1; i > 0; i--) {
if (number >= symbols[i].value) {
return (
(number / symbols[i].value)
.toFixed(decimals)
.replace(regexp, '$1') + symbols[i].symbol
);
}
}
}
// this function searches for right translation of provided error (usually from back-end)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment