From c4f47f59cfa1cc2c5dc349af71deb9011e51e818 Mon Sep 17 00:00:00 2001 From: mayaeh Date: Mon, 6 Jul 2020 19:17:33 +0900 Subject: Fix restored words from "toot" to "status" (#14242) --- app/javascript/mastodon/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 175320a66..1212f2b36 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -372,7 +372,7 @@ "status.bookmark": "Bookmark", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", - "status.copy": "Copy link to status", + "status.copy": "Copy link to toot", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", -- cgit From cb2adaaf9d6c3147de9060132b69933df734d5dc Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Mon, 6 Jul 2020 19:27:32 +0700 Subject: Replace shortNumberFormat with (#14061) This commit introduces new utility component - ShortNumber. It should work almost the same way as original shortNumberFormat function, though it also localizes units and accepts one more prop - renderer. Renderer is a function that takes rendered short formatted number and also ready-to-pluralize number to format display result accordingly. Ready-to-pluralize number allows to correctly select plural for compactly notated numbers, respecting thousands and other units. Issue #12451 accurately describes the issue with using raw numbers when replacing counter with short version. In short, it doesn't work with languages such as Russian, that require different plurals, according to the unit number was compacted to. All previous usages of shortNumberFormat were replaced with new function, and as it became unused, it was removed to avoid misleading. --- .../mastodon/components/autosuggest_hashtag.js | 24 ++- .../mastodon/components/common_counter.js | 62 ++++++++ app/javascript/mastodon/components/hashtag.js | 49 +++++- app/javascript/mastodon/components/short_number.js | 117 ++++++++++++++ .../mastodon/features/account/components/header.js | 18 ++- .../features/directory/components/account_card.js | 171 ++++++++++++++++----- app/javascript/mastodon/utils/numbers.js | 85 ++++++++-- 7 files changed, 459 insertions(+), 67 deletions(-) create mode 100644 app/javascript/mastodon/components/common_counter.js create mode 100644 app/javascript/mastodon/components/short_number.js (limited to 'app') diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.js b/app/javascript/mastodon/components/autosuggest_hashtag.js index e2f4e320d..9e9d888f8 100644 --- a/app/javascript/mastodon/components/autosuggest_hashtag.js +++ b/app/javascript/mastodon/components/autosuggest_hashtag.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { shortNumberFormat } from 'mastodon/utils/numbers'; +import ShortNumber from 'mastodon/components/short_number'; import { FormattedMessage } from 'react-intl'; export default class AutosuggestHashtag extends React.PureComponent { @@ -13,14 +13,28 @@ export default class AutosuggestHashtag extends React.PureComponent { }).isRequired, }; - render () { + render() { const { tag } = this.props; - const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0)); + const weeklyUses = tag.history && ( + total + day.uses * 1, 0)} + /> + ); return (
-
#{tag.name}
- {tag.history !== undefined &&
} +
+ #{tag.name} +
+ {tag.history !== undefined && ( +
+ +
+ )}
); } diff --git a/app/javascript/mastodon/components/common_counter.js b/app/javascript/mastodon/components/common_counter.js new file mode 100644 index 000000000..4fdf3babf --- /dev/null +++ b/app/javascript/mastodon/components/common_counter.js @@ -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) => {displayNumber} + : (displayNumber) => displayNumber; + + switch (counterType) { + case 'statuses': { + return (displayNumber, pluralReady) => ( + + ); + } + case 'following': { + return (displayNumber, pluralReady) => ( + + ); + } + case 'followers': { + return (displayNumber, pluralReady) => ( + + ); + } + default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`); + } +} diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js index 62d613262..d766ca90d 100644 --- a/app/javascript/mastodon/components/hashtag.js +++ b/app/javascript/mastodon/components/hashtag.js @@ -1,26 +1,65 @@ +// @ts-check import React from 'react'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; -import { shortNumberFormat } from '../utils/numbers'; +import ShortNumber from 'mastodon/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) => ( + {displayNumber}, + }} + /> +); const Hashtag = ({ hashtag }) => (
- + #{hashtag.get('name')} - {shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)} }} /> +
- {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)} +
- day.get('uses')).toArray()}> + day.get('uses')) + .toArray()} + >
diff --git a/app/javascript/mastodon/components/short_number.js b/app/javascript/mastodon/components/short_number.js new file mode 100644 index 000000000..535c17727 --- /dev/null +++ b/app/javascript/mastodon/components/short_number.js @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/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 = ; + + // 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('../utils/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 = ( + + ); + + let values = { count, rawNumber }; + + switch (unit) { + case DECIMAL_UNITS.THOUSAND: { + return ( + + ); + } + case DECIMAL_UNITS.MILLION: { + return ( + + ); + } + case DECIMAL_UNITS.BILLION: { + return ( + + ); + } + // Not sure if we should go farther - @Sasha-Sorokin + default: return count; + } +} + +ShortNumberCounter.propTypes = { + value: PropTypes.arrayOf(PropTypes.number), +}; + +export default React.memo(ShortNumber); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index eca0b7901..144f6bd94 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -8,7 +8,8 @@ import { autoPlayGif, me, isStaff } from 'mastodon/initial_state'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import Avatar from 'mastodon/components/avatar'; -import { shortNumberFormat } from 'mastodon/utils/numbers'; +import { counterRenderer } from 'mastodon/components/common_counter'; +import ShortNumber from 'mastodon/components/short_number'; import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; @@ -328,15 +329,24 @@ class Header extends ImmutablePureComponent {
- {shortNumberFormat(account.get('statuses_count'))} + - {shortNumberFormat(account.get('following_count'))} + - {shortNumberFormat(account.get('followers_count'))} +
diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index cb47d9db4..419ab9e11 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -11,8 +11,14 @@ import RelativeTimestamp from 'mastodon/components/relative_timestamp'; import IconButton from 'mastodon/components/icon_button'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state'; -import { shortNumberFormat } from 'mastodon/utils/numbers'; -import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts'; +import ShortNumber from 'mastodon/components/short_number'; +import { + followAccount, + unfollowAccount, + blockAccount, + unblockAccount, + unmuteAccount, +} from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; import { initMuteModal } from 'mastodon/actions/mutes'; @@ -22,7 +28,10 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, + unfollowConfirm: { + id: 'confirmations.unfollow.confirm', + defaultMessage: 'Unfollow', + }, }); const makeMapStateToProps = () => { @@ -36,15 +45,25 @@ const makeMapStateToProps = () => { }; const mapDispatchToProps = (dispatch, { intl }) => ({ - - onFollow (account) { - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + onFollow(account) { + if ( + account.getIn(['relationship', 'following']) || + account.getIn(['relationship', 'requested']) + ) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - })); + dispatch( + openModal('CONFIRM', { + message: ( + @{account.get('acct')} }} + /> + ), + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }), + ); } else { dispatch(unfollowAccount(account.get('id'))); } @@ -53,7 +72,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onBlock (account) { + onBlock(account) { if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); } else { @@ -61,17 +80,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onMute (account) { + onMute(account) { if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); } else { dispatch(initMuteModal(account)); } }, - }); -export default @injectIntl +export default +@injectIntl @connect(makeMapStateToProps, mapDispatchToProps) class AccountCard extends ImmutablePureComponent { @@ -83,7 +102,7 @@ class AccountCard extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, }; - _updateEmojis () { + _updateEmojis() { const node = this.node; if (!node || autoPlayGif) { @@ -104,68 +123,113 @@ class AccountCard extends ImmutablePureComponent { } } - componentDidMount () { + componentDidMount() { this._updateEmojis(); } - componentDidUpdate () { + componentDidUpdate() { this._updateEmojis(); } handleEmojiMouseEnter = ({ target }) => { target.src = target.getAttribute('data-original'); - } + }; handleEmojiMouseLeave = ({ target }) => { target.src = target.getAttribute('data-static'); - } + }; handleFollow = () => { this.props.onFollow(this.props.account); - } + }; handleBlock = () => { this.props.onBlock(this.props.account); - } + }; handleMute = () => { this.props.onMute(this.props.account); - } + }; setRef = (c) => { this.node = c; - } + }; - render () { + render() { const { account, intl } = this.props; 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 requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); if (requested) { - buttons = ; + buttons = ( + + ); } else if (blocking) { - buttons = ; + buttons = ( + + ); } else if (muting) { - buttons = ; + buttons = ( + + ); } else if (!account.get('moved') || following) { - buttons = ; + buttons = ( + + ); } } return (
- +
- + @@ -176,13 +240,44 @@ class AccountCard extends ImmutablePureComponent {
-
+
-
{shortNumberFormat(account.get('statuses_count'))}
-
{shortNumberFormat(account.get('followers_count'))}
-
{account.get('last_status_at') === null ? : }
+
+ + + + +
+
+ {' '} + + + +
+
+ {account.get('last_status_at') === null ? ( + + ) : ( + + )}{' '} + + + +
); diff --git a/app/javascript/mastodon/utils/numbers.js b/app/javascript/mastodon/utils/numbers.js index af18dcfdd..6f2505cae 100644 --- a/app/javascript/mastodon/utils/numbers.js +++ b/app/javascript/mastodon/utils/numbers.js @@ -1,16 +1,71 @@ -import React, { Fragment } from 'react'; -import { FormattedNumber } from 'react-intl'; - -export const shortNumberFormat = number => { - if (number < 1000) { - return ; - } else if (number < 10000) { - return K; - } else if (number < 1000000) { - return K; - } else if (number < 10000000) { - return M; - } else { - return M; +// @ts-check + +export const DECIMAL_UNITS = Object.freeze({ + ONE: 1, + TEN: 10, + HUNDRED: Math.pow(10, 2), + THOUSAND: Math.pow(10, 3), + MILLION: Math.pow(10, 6), + BILLION: Math.pow(10, 9), + TRILLION: Math.pow(10, 12), +}); + +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; +} -- cgit From c3187411c26aa00eb5844e4a7f9889f2cb19867e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 7 Jul 2020 01:24:03 +0200 Subject: Change design of account notes in web UI (#14208) * Change design of account notes in web UI * Fix `for` -> `htmlFor` --- app/javascript/mastodon/actions/account_notes.js | 36 +--- .../features/account/components/account_note.js | 183 ++++++++++++++------- .../mastodon/features/account/components/header.js | 11 +- .../account/containers/account_note_container.js | 29 +--- .../containers/header_container.js | 5 - .../mastodon/locales/defaultMessages.json | 20 +-- app/javascript/mastodon/locales/en.json | 9 +- app/javascript/mastodon/reducers/account_notes.js | 44 ----- app/javascript/mastodon/reducers/index.js | 2 - app/javascript/styles/mastodon/components.scss | 76 ++++----- app/serializers/rest/relationship_serializer.rb | 2 +- 11 files changed, 182 insertions(+), 235 deletions(-) delete mode 100644 app/javascript/mastodon/reducers/account_notes.js (limited to 'app') diff --git a/app/javascript/mastodon/actions/account_notes.js b/app/javascript/mastodon/actions/account_notes.js index 059ed9e80..d17441000 100644 --- a/app/javascript/mastodon/actions/account_notes.js +++ b/app/javascript/mastodon/actions/account_notes.js @@ -4,19 +4,12 @@ export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST'; export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS'; export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL'; -export const ACCOUNT_NOTE_INIT_EDIT = 'ACCOUNT_NOTE_INIT_EDIT'; -export const ACCOUNT_NOTE_CANCEL = 'ACCOUNT_NOTE_CANCEL'; - -export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT'; - -export function submitAccountNote() { +export function submitAccountNote(id, value) { return (dispatch, getState) => { dispatch(submitAccountNoteRequest()); - const id = getState().getIn(['account_notes', 'edit', 'account_id']); - api(getState).post(`/api/v1/accounts/${id}/note`, { - comment: getState().getIn(['account_notes', 'edit', 'comment']), + comment: value, }).then(response => { dispatch(submitAccountNoteSuccess(response.data)); }).catch(error => dispatch(submitAccountNoteFail(error))); @@ -42,28 +35,3 @@ export function submitAccountNoteFail(error) { error, }; }; - -export function initEditAccountNote(account) { - return (dispatch, getState) => { - const comment = getState().getIn(['relationships', account.get('id'), 'note']); - - dispatch({ - type: ACCOUNT_NOTE_INIT_EDIT, - account, - comment, - }); - }; -}; - -export function cancelAccountNote() { - return { - type: ACCOUNT_NOTE_CANCEL, - }; -}; - -export function changeAccountNoteComment(comment) { - return { - type: ACCOUNT_NOTE_CHANGE_COMMENT, - comment, - }; -}; diff --git a/app/javascript/mastodon/features/account/components/account_note.js b/app/javascript/mastodon/features/account/components/account_note.js index 832a96a6a..1787ce1ab 100644 --- a/app/javascript/mastodon/features/account/components/account_note.js +++ b/app/javascript/mastodon/features/account/components/account_note.js @@ -3,99 +3,166 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Icon from 'mastodon/components/icon'; import Textarea from 'react-textarea-autosize'; +import { is } from 'immutable'; const messages = defineMessages({ - placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' }, + placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' }, }); +class InlineAlert extends React.PureComponent { + + static propTypes = { + show: PropTypes.bool, + }; + + state = { + mountMessage: false, + }; + + static TRANSITION_DELAY = 200; + + componentWillReceiveProps (nextProps) { + if (!this.props.show && nextProps.show) { + this.setState({ mountMessage: true }); + } else if (this.props.show && !nextProps.show) { + setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY); + } + } + + render () { + const { show } = this.props; + const { mountMessage } = this.state; + + return ( + + {mountMessage && } + + ); + } + +} + export default @injectIntl -class Header extends ImmutablePureComponent { +class AccountNote extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - isEditing: PropTypes.bool, - isSubmitting: PropTypes.bool, - accountNote: PropTypes.string, - onEditAccountNote: PropTypes.func.isRequired, - onCancelAccountNote: PropTypes.func.isRequired, - onSaveAccountNote: PropTypes.func.isRequired, - onChangeAccountNote: PropTypes.func.isRequired, + value: PropTypes.string, + onSave: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; - handleChangeAccountNote = (e) => { - this.props.onChangeAccountNote(e.target.value); + state = { + value: null, + saving: false, + saved: false, }; + componentWillMount () { + this._reset(); + } + + componentWillReceiveProps (nextProps) { + const accountWillChange = !is(this.props.account, nextProps.account); + const newState = {}; + + if (accountWillChange && this._isDirty()) { + this._save(false); + } + + if (accountWillChange || nextProps.value === this.state.value) { + newState.saving = false; + } + + if (this.props.value !== nextProps.value) { + newState.value = nextProps.value; + } + + this.setState(newState); + } + componentWillUnmount () { - if (this.props.isEditing) { - this.props.onCancelAccountNote(); + if (this._isDirty()) { + this._save(false); } } + setTextareaRef = c => { + this.textarea = c; + } + + handleChange = e => { + this.setState({ value: e.target.value, saving: false }); + }; + handleKeyDown = e => { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.props.onSaveAccountNote(); + e.preventDefault(); + + this._save(); + + if (this.textarea) { + this.textarea.blur(); + } } else if (e.keyCode === 27) { - this.props.onCancelAccountNote(); + e.preventDefault(); + + this._reset(() => { + if (this.textarea) { + this.textarea.blur(); + } + }); } } + handleBlur = () => { + if (this._isDirty()) { + this._save(); + } + } + + _save (showMessage = true) { + this.setState({ saving: true }, () => this.props.onSave(this.state.value)); + + if (showMessage) { + this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000)); + } + } + + _reset (callback) { + this.setState({ value: this.props.value }, callback); + } + + _isDirty () { + return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value; + } + render () { - const { account, accountNote, isEditing, isSubmitting, intl } = this.props; + const { account, intl } = this.props; + const { value, saved } = this.state; - if (!account || (!accountNote && !isEditing)) { + if (!account) { return null; } - let action_buttons = null; - if (isEditing) { - action_buttons = ( -
- -
- -
- ); - } + return ( +
+ - let note_container = null; - if (isEditing) { - note_container = (