[Glitch] Replace shortNumberFormat with <ShortNumber>
Port 55969e3bc2
to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
lolsob-rspec
parent
0d4c3cfb77
commit
1144767ca8
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
|
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default class AutosuggestHashtag extends React.PureComponent {
|
export default class AutosuggestHashtag extends React.PureComponent {
|
||||||
|
@ -15,12 +15,26 @@ export default class AutosuggestHashtag extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tag } = this.props;
|
const { tag } = this.props;
|
||||||
const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
|
const weeklyUses = tag.history && (
|
||||||
|
<ShortNumber
|
||||||
|
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-hashtag'>
|
<div className='autosuggest-hashtag'>
|
||||||
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
|
<div className='autosuggest-hashtag__name'>
|
||||||
{tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>}
|
#<strong>{tag.name}</strong>
|
||||||
|
</div>
|
||||||
|
{tag.history !== undefined && (
|
||||||
|
<div className='autosuggest-hashtag__uses'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='autosuggest_hashtag.per_week'
|
||||||
|
defaultMessage='{count} per week'
|
||||||
|
values={{ count: weeklyUses }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
// @ts-check
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns custom renderer for one of the common counter types
|
||||||
|
*
|
||||||
|
* @param {"statuses" | "following" | "followers"} counterType
|
||||||
|
* Type of the counter
|
||||||
|
* @param {boolean} isBold Whether display number must be displayed in bold
|
||||||
|
* @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||||
|
* Renderer function
|
||||||
|
* @throws If counterType is not covered by this function
|
||||||
|
*/
|
||||||
|
export function counterRenderer(counterType, isBold = true) {
|
||||||
|
/**
|
||||||
|
* @type {(displayNumber: JSX.Element) => JSX.Element}
|
||||||
|
*/
|
||||||
|
const renderCounter = isBold
|
||||||
|
? (displayNumber) => <strong>{displayNumber}</strong>
|
||||||
|
: (displayNumber) => displayNumber;
|
||||||
|
|
||||||
|
switch (counterType) {
|
||||||
|
case 'statuses': {
|
||||||
|
return (displayNumber, pluralReady) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.statuses_counter'
|
||||||
|
defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: renderCounter(displayNumber),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'following': {
|
||||||
|
return (displayNumber, pluralReady) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.following_counter'
|
||||||
|
defaultMessage='{count, plural, other {{counter} Following}}'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: renderCounter(displayNumber),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'followers': {
|
||||||
|
return (displayNumber, pluralReady) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.followers_counter'
|
||||||
|
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: renderCounter(displayNumber),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,65 @@
|
||||||
|
// @ts-check
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
|
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to render counter of how much people are talking about hashtag
|
||||||
|
*
|
||||||
|
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||||
|
*/
|
||||||
|
const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='trends.counter_by_accounts'
|
||||||
|
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
|
||||||
|
values={{
|
||||||
|
count: pluralReady,
|
||||||
|
counter: <strong>{displayNumber}</strong>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const Hashtag = ({ hashtag }) => (
|
const Hashtag = ({ hashtag }) => (
|
||||||
<div className='trends__item'>
|
<div className='trends__item'>
|
||||||
<div className='trends__item__name'>
|
<div className='trends__item__name'>
|
||||||
<Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
|
<Permalink
|
||||||
|
href={hashtag.get('url')}
|
||||||
|
to={`/timelines/tag/${hashtag.get('name')}`}
|
||||||
|
>
|
||||||
#<span>{hashtag.get('name')}</span>
|
#<span>{hashtag.get('name')}</span>
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} />
|
<ShortNumber
|
||||||
|
value={
|
||||||
|
hashtag.getIn(['history', 0, 'accounts']) * 1 +
|
||||||
|
hashtag.getIn(['history', 1, 'accounts']) * 1
|
||||||
|
}
|
||||||
|
renderer={accountsCountRenderer}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__current'>
|
<div className='trends__item__current'>
|
||||||
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)}
|
<ShortNumber
|
||||||
|
value={
|
||||||
|
hashtag.getIn(['history', 0, 'uses']) * 1 +
|
||||||
|
hashtag.getIn(['history', 1, 'uses']) * 1
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__sparkline'>
|
<div className='trends__item__sparkline'>
|
||||||
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
|
<Sparklines
|
||||||
|
width={50}
|
||||||
|
height={28}
|
||||||
|
data={hashtag
|
||||||
|
.get('history')
|
||||||
|
.reverse()
|
||||||
|
.map((day) => day.get('uses'))
|
||||||
|
.toArray()}
|
||||||
|
>
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../util/numbers';
|
||||||
|
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback ShortNumberRenderer
|
||||||
|
* @param {JSX.Element} displayNumber Number to display
|
||||||
|
* @param {number} pluralReady Number used for pluralization
|
||||||
|
* @returns {JSX.Element} Final render of number
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ShortNumberProps
|
||||||
|
* @property {number} value Number to display in short variant
|
||||||
|
* @property {ShortNumberRenderer} [renderer]
|
||||||
|
* Custom renderer for numbers, provided as a prop. If another renderer
|
||||||
|
* passed as a child of this component, this prop won't be used.
|
||||||
|
* @property {ShortNumberRenderer} [children]
|
||||||
|
* Custom renderer for numbers, provided as a child. If another renderer
|
||||||
|
* passed as a prop of this component, this one will be used instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders short big number to a shorter version
|
||||||
|
*
|
||||||
|
* @param {ShortNumberProps} param0 Props for the component
|
||||||
|
* @returns {JSX.Element} Rendered number
|
||||||
|
*/
|
||||||
|
function ShortNumber({ value, renderer, children }) {
|
||||||
|
const shortNumber = toShortNumber(value);
|
||||||
|
const [, division] = shortNumber;
|
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (children != null && renderer != null) {
|
||||||
|
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
const customRenderer = children != null ? children : renderer;
|
||||||
|
|
||||||
|
const displayNumber = <ShortNumberCounter value={shortNumber} />;
|
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
return customRenderer != null
|
||||||
|
? customRenderer(displayNumber, pluralReady(value, division))
|
||||||
|
: displayNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortNumber.propTypes = {
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
renderer: PropTypes.func,
|
||||||
|
children: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ShortNumberCounterProps
|
||||||
|
* @property {import('../util/number').ShortNumber} value Short number
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders short number into corresponding localizable react fragment
|
||||||
|
*
|
||||||
|
* @param {ShortNumberCounterProps} param0 Props for the component
|
||||||
|
* @returns {JSX.Element} FormattedMessage ready to be embedded in code
|
||||||
|
*/
|
||||||
|
function ShortNumberCounter({ value }) {
|
||||||
|
const [rawNumber, unit, maxFractionDigits = 0] = value;
|
||||||
|
|
||||||
|
const count = (
|
||||||
|
<FormattedNumber
|
||||||
|
value={rawNumber}
|
||||||
|
maximumFractionDigits={maxFractionDigits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
let values = { count, rawNumber };
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case DECIMAL_UNITS.THOUSAND: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='units.short.thousand'
|
||||||
|
defaultMessage='{count}K'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case DECIMAL_UNITS.MILLION: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='units.short.million'
|
||||||
|
defaultMessage='{count}M'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case DECIMAL_UNITS.BILLION: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='units.short.billion'
|
||||||
|
defaultMessage='{count}B'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Not sure if we should go farther - @Sasha-Sorokin
|
||||||
|
default: return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortNumberCounter.propTypes = {
|
||||||
|
value: PropTypes.arrayOf(PropTypes.number),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ShortNumber);
|
|
@ -9,7 +9,6 @@ import classNames from 'classnames';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import Avatar from 'flavours/glitch/components/avatar';
|
import Avatar from 'flavours/glitch/components/avatar';
|
||||||
import Button from 'flavours/glitch/components/button';
|
import Button from 'flavours/glitch/components/button';
|
||||||
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
|
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||||
import AccountNoteContainer from '../containers/account_note_container';
|
import AccountNoteContainer from '../containers/account_note_container';
|
||||||
|
|
|
@ -11,8 +11,14 @@ import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||||
import IconButton from 'flavours/glitch/components/icon_button';
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state';
|
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state';
|
||||||
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
|
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||||
import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'flavours/glitch/actions/accounts';
|
import {
|
||||||
|
followAccount,
|
||||||
|
unfollowAccount,
|
||||||
|
blockAccount,
|
||||||
|
unblockAccount,
|
||||||
|
unmuteAccount,
|
||||||
|
} from 'flavours/glitch/actions/accounts';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
|
|
||||||
|
@ -22,7 +28,10 @@ const messages = defineMessages({
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: {
|
||||||
|
id: 'confirmations.unfollow.confirm',
|
||||||
|
defaultMessage: 'Unfollow',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
|
@ -36,15 +45,25 @@ const makeMapStateToProps = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onFollow(account) {
|
onFollow(account) {
|
||||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
if (
|
||||||
|
account.getIn(['relationship', 'following']) ||
|
||||||
|
account.getIn(['relationship', 'requested'])
|
||||||
|
) {
|
||||||
if (unfollowModal) {
|
if (unfollowModal) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(
|
||||||
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
openModal('CONFIRM', {
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.unfollow.message'
|
||||||
|
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||||
|
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
confirm: intl.formatMessage(messages.unfollowConfirm),
|
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(unfollowAccount(account.get('id')));
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
@ -68,10 +87,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(initMuteModal(account));
|
dispatch(initMuteModal(account));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default
|
||||||
|
@injectIntl
|
||||||
@connect(makeMapStateToProps, mapDispatchToProps)
|
@connect(makeMapStateToProps, mapDispatchToProps)
|
||||||
class AccountCard extends ImmutablePureComponent {
|
class AccountCard extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -114,58 +133,103 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleEmojiMouseEnter = ({ target }) => {
|
handleEmojiMouseEnter = ({ target }) => {
|
||||||
target.src = target.getAttribute('data-original');
|
target.src = target.getAttribute('data-original');
|
||||||
}
|
};
|
||||||
|
|
||||||
handleEmojiMouseLeave = ({ target }) => {
|
handleEmojiMouseLeave = ({ target }) => {
|
||||||
target.src = target.getAttribute('data-static');
|
target.src = target.getAttribute('data-static');
|
||||||
}
|
};
|
||||||
|
|
||||||
handleFollow = () => {
|
handleFollow = () => {
|
||||||
this.props.onFollow(this.props.account);
|
this.props.onFollow(this.props.account);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleBlock = () => {
|
handleBlock = () => {
|
||||||
this.props.onBlock(this.props.account);
|
this.props.onBlock(this.props.account);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleMute = () => {
|
handleMute = () => {
|
||||||
this.props.onMute(this.props.account);
|
this.props.onMute(this.props.account);
|
||||||
}
|
};
|
||||||
|
|
||||||
setRef = (c) => {
|
setRef = (c) => {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { account, intl } = this.props;
|
const { account, intl } = this.props;
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
|
|
||||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
if (
|
||||||
|
account.get('id') !== me &&
|
||||||
|
account.get('relationship', null) !== null
|
||||||
|
) {
|
||||||
const following = account.getIn(['relationship', 'following']);
|
const following = account.getIn(['relationship', 'following']);
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
const blocking = account.getIn(['relationship', 'blocking']);
|
const blocking = account.getIn(['relationship', 'blocking']);
|
||||||
const muting = account.getIn(['relationship', 'muting']);
|
const muting = account.getIn(['relationship', 'muting']);
|
||||||
|
|
||||||
if (requested) {
|
if (requested) {
|
||||||
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
|
buttons = (
|
||||||
|
<IconButton
|
||||||
|
disabled
|
||||||
|
icon='hourglass'
|
||||||
|
title={intl.formatMessage(messages.requested)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (blocking) {
|
} else if (blocking) {
|
||||||
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
buttons = (
|
||||||
|
<IconButton
|
||||||
|
active
|
||||||
|
icon='unlock'
|
||||||
|
title={intl.formatMessage(messages.unblock, {
|
||||||
|
name: account.get('username'),
|
||||||
|
})}
|
||||||
|
onClick={this.handleBlock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (muting) {
|
} else if (muting) {
|
||||||
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
buttons = (
|
||||||
|
<IconButton
|
||||||
|
active
|
||||||
|
icon='volume-up'
|
||||||
|
title={intl.formatMessage(messages.unmute, {
|
||||||
|
name: account.get('username'),
|
||||||
|
})}
|
||||||
|
onClick={this.handleMute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (!account.get('moved') || following) {
|
} else if (!account.get('moved') || following) {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
buttons = (
|
||||||
|
<IconButton
|
||||||
|
icon={following ? 'user-times' : 'user-plus'}
|
||||||
|
title={intl.formatMessage(
|
||||||
|
following ? messages.unfollow : messages.follow,
|
||||||
|
)}
|
||||||
|
onClick={this.handleFollow}
|
||||||
|
active={following}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='directory__card'>
|
<div className='directory__card'>
|
||||||
<div className='directory__card__img'>
|
<div className='directory__card__img'>
|
||||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' />
|
<img
|
||||||
|
src={
|
||||||
|
autoPlayGif ? account.get('header') : account.get('header_static')
|
||||||
|
}
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='directory__card__bar'>
|
<div className='directory__card__bar'>
|
||||||
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
<Permalink
|
||||||
|
className='directory__card__bar__name'
|
||||||
|
href={account.get('url')}
|
||||||
|
to={`/accounts/${account.get('id')}`}
|
||||||
|
>
|
||||||
<Avatar account={account} size={48} />
|
<Avatar account={account} size={48} />
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
@ -176,13 +240,44 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='directory__card__extra' ref={this.setRef}>
|
<div className='directory__card__extra' ref={this.setRef}>
|
||||||
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
|
<div
|
||||||
|
className='account__header__content'
|
||||||
|
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='directory__card__extra'>
|
<div className='directory__card__extra'>
|
||||||
<div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
|
<div className='accounts-table__count'>
|
||||||
<div className='accounts-table__count'>{account.get('followers_count') < 0 ? '-' : shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
|
<ShortNumber value={account.get('statuses_count')} />
|
||||||
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
|
<small>
|
||||||
|
<FormattedMessage id='account.posts' defaultMessage='Toots' />
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div className='accounts-table__count'>
|
||||||
|
{account.get('followers_count') < 0 ? '-' : <ShortNumber value={account.get('followers_count')} />}{' '}
|
||||||
|
<small>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.followers'
|
||||||
|
defaultMessage='Followers'
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div className='accounts-table__count'>
|
||||||
|
{account.get('last_status_at') === null ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.never_active'
|
||||||
|
defaultMessage='Never'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RelativeTimestamp timestamp={account.get('last_status_at')} />
|
||||||
|
)}{' '}
|
||||||
|
<small>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.last_status'
|
||||||
|
defaultMessage='Last active'
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,16 +1,71 @@
|
||||||
import React, { Fragment } from 'react';
|
// @ts-check
|
||||||
import { FormattedNumber } from 'react-intl';
|
|
||||||
|
|
||||||
export const shortNumberFormat = number => {
|
export const DECIMAL_UNITS = Object.freeze({
|
||||||
if (number < 1000) {
|
ONE: 1,
|
||||||
return <FormattedNumber value={number} />;
|
TEN: 10,
|
||||||
} else if (number < 10000) {
|
HUNDRED: Math.pow(10, 2),
|
||||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
|
THOUSAND: Math.pow(10, 3),
|
||||||
} else if (number < 1000000) {
|
MILLION: Math.pow(10, 6),
|
||||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={0} />K</Fragment>;
|
BILLION: Math.pow(10, 9),
|
||||||
} else if (number < 10000000) {
|
TRILLION: Math.pow(10, 12),
|
||||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>;
|
});
|
||||||
} else {
|
|
||||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={0} />M</Fragment>;
|
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
|
||||||
|
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {[number, number, number]} ShortNumber
|
||||||
|
* Array of: shorten number, unit of shorten number and maximum fraction digits
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} sourceNumber Number to convert to short number
|
||||||
|
* @returns {ShortNumber} Calculated short number
|
||||||
|
* @example
|
||||||
|
* shortNumber(5936);
|
||||||
|
* // => [5.936, 1000, 1]
|
||||||
|
*/
|
||||||
|
export function toShortNumber(sourceNumber) {
|
||||||
|
if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
|
||||||
|
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||||
|
} else if (sourceNumber < DECIMAL_UNITS.MILLION) {
|
||||||
|
return [
|
||||||
|
sourceNumber / DECIMAL_UNITS.THOUSAND,
|
||||||
|
DECIMAL_UNITS.THOUSAND,
|
||||||
|
sourceNumber < TEN_THOUSAND ? 1 : 0,
|
||||||
|
];
|
||||||
|
} else if (sourceNumber < DECIMAL_UNITS.BILLION) {
|
||||||
|
return [
|
||||||
|
sourceNumber / DECIMAL_UNITS.MILLION,
|
||||||
|
DECIMAL_UNITS.MILLION,
|
||||||
|
sourceNumber < TEN_MILLIONS ? 1 : 0,
|
||||||
|
];
|
||||||
|
} else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
|
||||||
|
return [
|
||||||
|
sourceNumber / DECIMAL_UNITS.BILLION,
|
||||||
|
DECIMAL_UNITS.BILLION,
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} sourceNumber Original number that is shortened
|
||||||
|
* @param {number} division The scale in which short number is displayed
|
||||||
|
* @returns {number} Number that can be used for plurals when short form used
|
||||||
|
* @example
|
||||||
|
* pluralReady(1793, DECIMAL_UNITS.THOUSAND)
|
||||||
|
* // => 1790
|
||||||
|
*/
|
||||||
|
export function pluralReady(sourceNumber, division) {
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (division == null || division < DECIMAL_UNITS.HUNDRED) {
|
||||||
|
return sourceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
let closestScale = division / DECIMAL_UNITS.TEN;
|
||||||
|
|
||||||
|
return Math.trunc(sourceNumber / closestScale) * closestScale;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
Loading…
Reference in New Issue