From f87ce13afc25b4ea56b6d7e4a85eac4cee5591da Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Feb 2022 12:23:57 +0100 Subject: Refactor dropdown and action modal code slightly Simplify it a bit and make it closer to upstream --- .../flavours/glitch/components/dropdown_menu.js | 11 ++--- .../glitch/containers/dropdown_menu_container.js | 3 +- .../features/compose/components/dropdown_menu.js | 8 +--- .../glitch/features/ui/components/actions_modal.js | 48 ++++++++-------------- 4 files changed, 25 insertions(+), 45 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 023fecb9a..a4d0dfc50 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -116,7 +116,7 @@ class DropdownMenu extends React.PureComponent { if (typeof action === 'function') { e.preventDefault(); - action(); + action(e); } else if (to) { e.preventDefault(); this.context.router.history.push(to); @@ -128,11 +128,11 @@ class DropdownMenu extends React.PureComponent { return
  • ; } - const { text, href = '#' } = option; + const { text, href = '#', target = '_blank', method } = option; return (
  • - + {text}
  • @@ -149,7 +149,7 @@ class DropdownMenu extends React.PureComponent { // It should not be transformed when mounting because the resulting // size will be used to determine the coordinate of the menu by // react-overlays -
    +
      @@ -236,7 +236,8 @@ export default class Dropdown extends React.PureComponent { } } - handleItemClick = (i, e) => { + handleItemClick = e => { + const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; this.handleClose(); diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index 1c0385b74..d18e640a4 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -18,11 +18,12 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ (item, i) => item ? { ...item, name: `${item.text}-${i}`, - onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null, } : null ), + onClick: onItemClick, }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); }, + onClose(id) { dispatch(closeModal('ACTIONS')); dispatch(closeDropdownMenu(id)); diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index bee06e64c..c2446c6dd 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -154,13 +154,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent const active = (name === (this.props.value || this.state.value)); - const computedClass = classNames('composer--options--dropdown--content--item', { - active, - lengthy: meta, - 'toggled-off': !on && on !== null && typeof on !== 'undefined', - 'toggled-on': on, - 'with-icon': icon, - }); + const computedClass = classNames('composer--options--dropdown--content--item', { active }); let prefix = null; diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js index 24169036c..4ae3a4766 100644 --- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js @@ -8,13 +8,13 @@ import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import DisplayName from 'flavours/glitch/components/display_name'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; -import Link from 'flavours/glitch/components/link'; import Toggle from 'react-toggle'; export default class ActionsModal extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, + onClick: PropTypes.func, actions: PropTypes.arrayOf(PropTypes.shape({ active: PropTypes.bool, href: PropTypes.string, @@ -46,43 +46,27 @@ export default class ActionsModal extends ImmutablePureComponent { return (
    • - - {function () { - - // We render a `` if we were provided an `on` - // property, and otherwise show an `` if available. - switch (true) { - case on !== null && typeof on !== 'undefined': - return ( - - ); - case !!icon: - return ( - - ); - default: - return null; - } - }()} + + {on !== null && typeof on !== 'undefined' && ( + + )} + {icon && ( + + )} {meta ? (
      {text} {meta}
      ) :
      {text}
      } - +
    • ); } -- cgit From f03dc97070ea78a0f96f51f99b78d66b9c52517d Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Feb 2022 13:41:17 +0100 Subject: Some more refactoring --- .../features/compose/components/dropdown_menu.js | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index c2446c6dd..16eb1ef9d 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -77,14 +77,16 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent document.removeEventListener('touchend', this.handleDocumentClick, withPassive); } - handleClick = (name, e) => { + handleClick = (e) => { + const i = Number(e.currentTarget.getAttribute('data-index')); + const { onChange, onClose, items, } = this.props; - const { on } = this.props.items.find(item => item.name === name); + const { on, name } = this.props.items[i]; e.preventDefault(); // Prevents change in focus on click if ((on === null || typeof on === 'undefined')) { onClose(); @@ -101,11 +103,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } } - handleKeyDown = (name, e) => { + handleKeyDown = (e) => { + const index = Number(e.currentTarget.getAttribute('data-index')); const { items } = this.props; - const index = items.findIndex(item => { - return (item.name === name); - }); let element = null; switch(e.key) { @@ -139,7 +139,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent if (element) { element.focus(); - this.handleChange(element.getAttribute('data-index')); + this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); e.preventDefault(); e.stopPropagation(); } @@ -149,7 +149,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent this.focusedItem = c; } - renderItem = (item) => { + renderItem = (item, i) => { const { name, icon, meta, on, text } = item; const active = (name === (this.props.value || this.state.value)); @@ -159,7 +159,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent let prefix = null; if (on !== null && typeof on !== 'undefined') { - prefix = ; + prefix = ; } else if (icon) { prefix = } @@ -167,12 +167,12 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent return (
      {prefix} @@ -223,7 +223,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, }} > - {!!items && items.map(item => this.renderItem(item))} + {!!items && items.map((item, i) => this.renderItem(item, i))}
      )} -- cgit From bc2eaf3581735e94be0cee3ef6b7241a5650245c Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Feb 2022 13:49:49 +0100 Subject: Remove unused noModal prop --- .../flavours/glitch/features/compose/components/dropdown.js | 5 ++--- .../flavours/glitch/features/compose/components/privacy_dropdown.js | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index abf7cbba1..f8fe819a5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -31,7 +31,6 @@ export default class ComposerOptionsDropdown extends React.PureComponent { title: PropTypes.string, value: PropTypes.string, onChange: PropTypes.func, - noModal: PropTypes.bool, container: PropTypes.func, }; @@ -44,10 +43,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent { // Toggles opening and closing the dropdown. handleToggle = ({ target, type }) => { - const { onModalOpen, noModal } = this.props; + const { onModalOpen } = this.props; const { open } = this.state; - if (!noModal && isUserTouching()) { + if (isUserTouching()) { if (this.state.open) { this.props.onModalClose(); } else { diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js index 39f7c7bd1..d5d29a751 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js @@ -53,13 +53,12 @@ class PrivacyDropdown extends React.PureComponent { value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, noDirect: PropTypes.bool, - noModal: PropTypes.bool, container: PropTypes.func, intl: PropTypes.object.isRequired, }; render () { - const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props; + const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl } = this.props; // We predefine our privacy items so that we can easily pick the // dropdown icon later. @@ -106,7 +105,6 @@ class PrivacyDropdown extends React.PureComponent { onModalOpen={onModalOpen} title={intl.formatMessage(messages.change_privacy)} container={container} - noModal={noModal} value={value} /> ); -- cgit From e1a4590bcae65275304046e8887f70592196c003 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Feb 2022 14:39:12 +0100 Subject: Rework actions modal to bring it closer to upstream and fix modal stacking issue --- .../glitch/containers/dropdown_menu_container.js | 7 +- .../glitch/features/compose/components/dropdown.js | 72 +++++++++------------ .../features/compose/components/dropdown_menu.js | 36 ++++++----- .../glitch/features/compose/components/options.js | 75 ++++++++++++++++------ .../compose/components/privacy_dropdown.js | 65 ++++++------------- .../glitch/features/ui/components/actions_modal.js | 59 ++++++----------- 6 files changed, 149 insertions(+), 165 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index d18e640a4..0c4a2b50f 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -14,12 +14,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ onOpen(id, onItemClick, dropdownPlacement, keyboard) { dispatch(isUserTouching() ? openModal('ACTIONS', { status, - actions: items.map( - (item, i) => item ? { - ...item, - name: `${item.text}-${i}`, - } : null - ), + actions: items, onClick: onItemClick, }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); }, diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index f8fe819a5..4708e2ece 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -21,10 +21,9 @@ export default class ComposerOptionsDropdown extends React.PureComponent { icon: PropTypes.string, items: PropTypes.arrayOf(PropTypes.shape({ icon: PropTypes.string, - meta: PropTypes.node, + meta: PropTypes.string, name: PropTypes.string.isRequired, - on: PropTypes.bool, - text: PropTypes.node, + text: PropTypes.string, })).isRequired, onModalOpen: PropTypes.func, onModalClose: PropTypes.func, @@ -32,10 +31,15 @@ export default class ComposerOptionsDropdown extends React.PureComponent { value: PropTypes.string, onChange: PropTypes.func, container: PropTypes.func, + renderItemContents: PropTypes.func, + closeOnChange: PropTypes.bool, + }; + + static defaultProps = { + closeOnChange: true, }; state = { - needsModalUpdate: false, open: false, openedViaKeyboard: undefined, placement: 'bottom', @@ -106,6 +110,23 @@ export default class ComposerOptionsDropdown extends React.PureComponent { this.setState({ open: false }); } + handleItemClick = (e) => { + const { + items, + onChange, + onModalClose, + closeOnChange, + } = this.props; + + const i = Number(e.currentTarget.getAttribute('data-index')); + + const { name } = this.props.items[i]; + + e.preventDefault(); // Prevents focus from changing + if (closeOnChange) onModalClose(); + onChange(name); + }; + // Creates an action modal object. handleMakeModal = () => { const component = this; @@ -124,6 +145,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent { // The object. return { + renderItemContents: this.props.renderItemContents, + onClick: this.handleItemClick, actions: items.map( ({ name, @@ -132,48 +155,11 @@ export default class ComposerOptionsDropdown extends React.PureComponent { ...rest, active: value && name === value, name, - onClick (e) { - e.preventDefault(); // Prevents focus from changing - onModalClose(); - onChange(name); - }, - onPassiveClick (e) { - e.preventDefault(); // Prevents focus from changing - onChange(name); - component.setState({ needsModalUpdate: true }); - }, }) ), }; } - // If our modal is open and our props update, we need to also update - // the modal. - handleUpdate = () => { - const { onModalOpen } = this.props; - const { needsModalUpdate } = this.state; - - // Gets our modal object. - const modal = this.handleMakeModal(); - - // Reopens the modal with the new object. - if (needsModalUpdate && modal && onModalOpen) { - onModalOpen(modal); - } - } - - // Updates our modal as necessary. - componentDidUpdate (prevProps) { - const { items } = this.props; - const { needsModalUpdate } = this.state; - if (needsModalUpdate && items.find( - (item, i) => item.on !== prevProps.items[i].on - )) { - this.handleUpdate(); - this.setState({ needsModalUpdate: false }); - } - } - // Rendering. render () { const { @@ -185,6 +171,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent { onChange, value, container, + renderItemContents, + closeOnChange, } = this.props; const { open, placement } = this.state; const computedClass = classNames('composer--options--dropdown', { @@ -225,10 +213,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent { >
    diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index 16eb1ef9d..0649fe1ca 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import spring from 'react-motion/lib/spring'; -import Toggle from 'react-toggle'; import ImmutablePureComponent from 'react-immutable-pure-component'; import classNames from 'classnames'; @@ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent icon: PropTypes.string, meta: PropTypes.node, name: PropTypes.string.isRequired, - on: PropTypes.bool, text: PropTypes.node, })), onChange: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, style: PropTypes.object, value: PropTypes.string, + renderItemContents: PropTypes.func, openedViaKeyboard: PropTypes.bool, + closeOnChange: PropTypes.bool, }; static defaultProps = { style: {}, + closeOnChange: true, }; state = { @@ -83,12 +84,13 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent const { onChange, onClose, + closeOnChange, items, } = this.props; - const { on, name } = this.props.items[i]; + const { name } = this.props.items[i]; e.preventDefault(); // Prevents change in focus on click - if ((on === null || typeof on === 'undefined')) { + if (closeOnChange) { onClose(); } onChange(name); @@ -150,18 +152,25 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } renderItem = (item, i) => { - const { name, icon, meta, on, text } = item; + const { name, icon, meta, text } = item; const active = (name === (this.props.value || this.state.value)); const computedClass = classNames('composer--options--dropdown--content--item', { active }); - let prefix = null; + let contents = this.props.renderItemContents && this.props.renderItemContents(item, i); - if (on !== null && typeof on !== 'undefined') { - prefix = ; - } else if (icon) { - prefix = + if (!contents) { + contents = ( + + {icon && } + +
    + {text} + {meta} +
    +
    + ); } return ( @@ -175,12 +184,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent data-index={i} ref={active ? this.setFocusRef : null} > - {prefix} - -
    - {text} - {meta} -
    + {contents}
    ); } diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index f9212bbae..d30fb2a98 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -2,8 +2,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import spring from 'react-motion/lib/spring'; +import Toggle from 'react-toggle'; +import { connect } from 'react-redux'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; @@ -80,6 +82,36 @@ const messages = defineMessages({ }, }); +@connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) })) +class ToggleOption extends ImmutablePureComponent { + + static propTypes = { + name: PropTypes.string.isRequired, + checked: PropTypes.bool, + onChangeAdvancedOption: PropTypes.func.isRequired, + }; + + handleChange = () => { + this.props.onChangeAdvancedOption(this.props.name); + }; + + render() { + const { name, meta, text, checked } = this.props; + + return ( + + + +
    + {text} + {meta} +
    +
    + ); + } + +} + export default @injectIntl class ComposerOptions extends ImmutablePureComponent { @@ -141,6 +173,13 @@ class ComposerOptions extends ImmutablePureComponent { this.fileElement = fileElement; } + renderToggleItemContents = (item, index) => { + const { onChangeAdvancedOption } = this.props; + const { name, meta, text } = item; + + return ; + }; + // Rendering. render () { const { @@ -152,7 +191,6 @@ class ComposerOptions extends ImmutablePureComponent { hasMedia, allowPoll, hasPoll, - intl, onChangeAdvancedOption, onChangeContentType, onChangeVisibility, @@ -164,23 +202,24 @@ class ComposerOptions extends ImmutablePureComponent { resetFileKey, spoiler, showContentTypeChoice, + intl: { formatMessage }, } = this.props; const contentTypeItems = { plain: { icon: 'file-text', name: 'text/plain', - text: , + text: formatMessage(messages.plain), }, html: { icon: 'code', name: 'text/html', - text: , + text: formatMessage(messages.html), }, markdown: { icon: 'arrow-circle-down', name: 'text/markdown', - text: , + text: formatMessage(messages.markdown), }, }; @@ -204,18 +243,18 @@ class ComposerOptions extends ImmutablePureComponent { { icon: 'cloud-upload', name: 'upload', - text: , + text: formatMessage(messages.upload), }, { icon: 'paint-brush', name: 'doodle', - text: , + text: formatMessage(messages.doodle), }, ]} onChange={this.handleClickAttach} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.attach)} + title={formatMessage(messages.attach)} /> {!!pollLimits && ( )}
    @@ -252,7 +291,7 @@ class ComposerOptions extends ImmutablePureComponent { onChange={onChangeContentType} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.content_type)} + title={formatMessage(messages.content_type)} value={contentType} /> )} @@ -262,7 +301,7 @@ class ComposerOptions extends ImmutablePureComponent { ariaControls='glitch.composer.spoiler.input' label='CW' onClick={onToggleSpoiler} - title={intl.formatMessage(messages.spoiler)} + title={formatMessage(messages.spoiler)} /> )} , + meta: formatMessage(messages.local_only_long), name: 'do_not_federate', - on: advancedOptions.get('do_not_federate'), - text: , + text: formatMessage(messages.local_only_short), }, { - meta: , + meta: formatMessage(messages.threaded_mode_long), name: 'threaded_mode', - on: advancedOptions.get('threaded_mode'), - text: , + text: formatMessage(messages.threaded_mode_short), }, ] : null} onChange={onChangeAdvancedOption} + renderItemContents={this.renderToggleItemContents} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.advanced_options_icon_title)} + title={formatMessage(messages.advanced_options_icon_title)} + closeOnChange={false} />
    ); diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js index d5d29a751..4113e4061 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js @@ -5,42 +5,15 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import Dropdown from './dropdown'; const messages = defineMessages({ - change_privacy: { - defaultMessage: 'Adjust status privacy', - id: 'privacy.change', - }, - direct_long: { - defaultMessage: 'Visible for mentioned users only', - id: 'privacy.direct.long', - }, - direct_short: { - defaultMessage: 'Direct', - id: 'privacy.direct.short', - }, - private_long: { - defaultMessage: 'Visible for followers only', - id: 'privacy.private.long', - }, - private_short: { - defaultMessage: 'Followers-only', - id: 'privacy.private.short', - }, - public_long: { - defaultMessage: 'Visible for all, shown in public timelines', - id: 'privacy.public.long', - }, - public_short: { - defaultMessage: 'Public', - id: 'privacy.public.short', - }, - unlisted_long: { - defaultMessage: 'Visible for all, but not in public timelines', - id: 'privacy.unlisted.long', - }, - unlisted_short: { - defaultMessage: 'Unlisted', - id: 'privacy.unlisted.short', - }, + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); export default @injectIntl @@ -58,34 +31,34 @@ class PrivacyDropdown extends React.PureComponent { }; render () { - const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl } = this.props; + const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl: { formatMessage } } = this.props; // We predefine our privacy items so that we can easily pick the // dropdown icon later. const privacyItems = { direct: { icon: 'envelope', - meta: , + meta: formatMessage(messages.direct_long), name: 'direct', - text: , + text: formatMessage(messages.direct_short), }, private: { icon: 'lock', - meta: , + meta: formatMessage(messages.private_long), name: 'private', - text: , + text: formatMessage(messages.private_short), }, public: { icon: 'globe', - meta: , + meta: formatMessage(messages.public_long), name: 'public', - text: , + text: formatMessage(messages.public_short), }, unlisted: { icon: 'unlock', - meta: , + meta: formatMessage(messages.unlisted_long), name: 'unlisted', - text: , + text: formatMessage(messages.unlisted_short), }, }; @@ -103,7 +76,7 @@ class PrivacyDropdown extends React.PureComponent { onChange={onChange} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.change_privacy)} + title={formatMessage(messages.change_privacy)} container={container} value={value} /> diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js index 4ae3a4766..aae2e4426 100644 --- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js @@ -7,8 +7,7 @@ import Avatar from 'flavours/glitch/components/avatar'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import DisplayName from 'flavours/glitch/components/display_name'; import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import Toggle from 'react-toggle'; +import IconButton from 'flavours/glitch/components/icon_button'; export default class ActionsModal extends ImmutablePureComponent { @@ -19,12 +18,11 @@ export default class ActionsModal extends ImmutablePureComponent { active: PropTypes.bool, href: PropTypes.string, icon: PropTypes.string, - meta: PropTypes.node, + meta: PropTypes.string, name: PropTypes.string, - on: PropTypes.bool, - onPassiveClick: PropTypes.func, - text: PropTypes.node, + text: PropTypes.string, })), + renderItemContents: PropTypes.func, }; renderAction = (action, i) => { @@ -32,40 +30,25 @@ export default class ActionsModal extends ImmutablePureComponent { return
  • ; } - const { - active, - href, - icon, - meta, - name, - on, - onClick, - onPassiveClick, - text, - } = action; + const { icon = null, text, meta = null, active = false, href = '#' } = action; + let contents = this.props.renderItemContents && this.props.renderItemContents(action, i); + + if (!contents) { + contents = ( + + {icon && } +
    +
    {text}
    +
    {meta}
    +
    +
    + ); + } return ( -
  • - - {on !== null && typeof on !== 'undefined' && ( - - )} - {icon && ( - - )} - {meta ? ( -
    - {text} - {meta} -
    - ) :
    {text}
    } +
  • + + {contents}
  • ); -- cgit From 0bb3d445ab0167ac33bd804333c7f9741c7526ae Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Feb 2022 17:15:36 +0100 Subject: Please Codeclimate --- .../flavours/glitch/features/compose/components/dropdown.js | 3 +-- app/javascript/flavours/glitch/features/compose/components/options.js | 4 ++-- .../flavours/glitch/features/compose/components/privacy_dropdown.js | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index 4708e2ece..9f70d6b79 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -120,7 +120,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { const i = Number(e.currentTarget.getAttribute('data-index')); - const { name } = this.props.items[i]; + const { name } = items[i]; e.preventDefault(); // Prevents focus from changing if (closeOnChange) onModalClose(); @@ -129,7 +129,6 @@ export default class ComposerOptionsDropdown extends React.PureComponent { // Creates an action modal object. handleMakeModal = () => { - const component = this; const { items, onChange, diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index d30fb2a98..085da18ea 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -96,7 +96,7 @@ class ToggleOption extends ImmutablePureComponent { }; render() { - const { name, meta, text, checked } = this.props; + const { meta, text, checked } = this.props; return ( @@ -173,7 +173,7 @@ class ComposerOptions extends ImmutablePureComponent { this.fileElement = fileElement; } - renderToggleItemContents = (item, index) => { + renderToggleItemContents = (item) => { const { onChangeAdvancedOption } = this.props; const { name, meta, text } = item; diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js index 4113e4061..3bf25b3a4 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import Dropdown from './dropdown'; const messages = defineMessages({ -- cgit From 44b06c4d96b5211725ecfc9483c0fb21198f18cf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 9 Feb 2022 01:17:07 +0100 Subject: [Glitch] Add edit history to web UI Port fd3a45e3482e86dad3c1dfc069144864c4ff0b0b to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/actions/history.js | 37 ++++ .../flavours/glitch/components/dropdown_menu.js | 149 ++++++++++---- .../containers/dropdown_menu_container.js | 27 +++ .../glitch/components/edited_timestamp/index.js | 70 +++++++ .../flavours/glitch/components/inline_account.js | 34 ++++ .../glitch/components/loading_indicator.js | 27 ++- .../glitch/components/relative_timestamp.js | 23 ++- .../features/status/components/detailed_status.js | 5 +- .../ui/components/compare_history_modal.js | 79 ++++++++ .../glitch/features/ui/components/modal_root.js | 4 +- app/javascript/flavours/glitch/reducers/history.js | 28 +++ app/javascript/flavours/glitch/reducers/index.js | 2 + .../flavours/glitch/styles/components/index.scss | 225 +++++++++++++++------ .../flavours/glitch/styles/components/modal.scss | 3 +- .../flavours/glitch/util/async-components.js | 4 + 15 files changed, 596 insertions(+), 121 deletions(-) create mode 100644 app/javascript/flavours/glitch/actions/history.js create mode 100644 app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js create mode 100644 app/javascript/flavours/glitch/components/edited_timestamp/index.js create mode 100644 app/javascript/flavours/glitch/components/inline_account.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/compare_history_modal.js create mode 100644 app/javascript/flavours/glitch/reducers/history.js (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/actions/history.js b/app/javascript/flavours/glitch/actions/history.js new file mode 100644 index 000000000..c47057261 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/history.js @@ -0,0 +1,37 @@ +import api from 'flavours/glitch/util/api'; +import { importFetchedAccounts } from './importer'; + +export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST'; +export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS'; +export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL'; + +export const fetchHistory = statusId => (dispatch, getState) => { + const loading = getState().getIn(['history', statusId, 'loading']); + + if (loading) { + return; + } + + dispatch(fetchHistoryRequest(statusId)); + + api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => { + dispatch(importFetchedAccounts(data.map(x => x.account))); + dispatch(fetchHistorySuccess(statusId, data)); + }).catch(error => dispatch(fetchHistoryFail(error))); +}; + +export const fetchHistoryRequest = statusId => ({ + type: HISTORY_FETCH_REQUEST, + statusId, +}); + +export const fetchHistorySuccess = (statusId, history) => ({ + type: HISTORY_FETCH_SUCCESS, + statusId, + history, +}); + +export const fetchHistoryFail = error => ({ + type: HISTORY_FETCH_FAIL, + error, +}); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index a4d0dfc50..bd6bc4ffd 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay'; import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { supportsPassiveEvents } from 'detect-passive-events'; +import classNames from 'classnames'; +import { CircularProgress } from 'mastodon/components/loading_indicator'; const listenerOptions = supportsPassiveEvents ? { passive: true } : false; let id = 0; @@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent { }; static propTypes = { - items: PropTypes.array.isRequired, + items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired, + loading: PropTypes.bool, + scrollable: PropTypes.bool, onClose: PropTypes.func.isRequired, style: PropTypes.object, placement: PropTypes.string, arrowOffsetLeft: PropTypes.string, arrowOffsetTop: PropTypes.string, openedViaKeyboard: PropTypes.bool, + renderItem: PropTypes.func, + renderHeader: PropTypes.func, + onItemClick: PropTypes.func.isRequired, }; static defaultProps = { @@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem && this.props.openedViaKeyboard) { this.focusedItem.focus({ preventScroll: true }); } + this.setState({ mounted: true }); } @@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent { } handleKeyDown = e => { - const items = Array.from(this.node.getElementsByTagName('a')); + const items = Array.from(this.node.querySelectorAll('a, button')); const index = items.indexOf(document.activeElement); let element = null; @@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent { } handleClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - this.props.onClose(); - - if (typeof action === 'function') { - e.preventDefault(); - action(e); - } else if (to) { - e.preventDefault(); - this.context.router.history.push(to); - } + const { onItemClick } = this.props; + onItemClick(e); } - renderItem (option, i) { + renderItem = (option, i) => { if (option === null) { return
  • ; } @@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent { } render () { - const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; + const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props; const { mounted } = this.state; + let renderItem = this.props.renderItem || this.renderItem; + return ( {({ opacity, scaleX, scaleY }) => ( @@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
    -
      - {items.map((option, i) => this.renderItem(option, i))} -
    +
    + {loading && ( + + )} + + {!loading && renderHeader && ( +
    + {renderHeader(items)} +
    + )} + + {!loading && ( +
      + {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))} +
    + )} +
    )} @@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent { }; static propTypes = { - icon: PropTypes.string.isRequired, - items: PropTypes.array.isRequired, - size: PropTypes.number.isRequired, + children: PropTypes.node, + icon: PropTypes.string, + items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired, + loading: PropTypes.bool, + size: PropTypes.number, title: PropTypes.string, disabled: PropTypes.bool, + scrollable: PropTypes.bool, status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, onOpen: PropTypes.func.isRequired, @@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent { dropdownPlacement: PropTypes.string, openDropdownId: PropTypes.number, openedViaKeyboard: PropTypes.bool, + renderItem: PropTypes.func, + renderHeader: PropTypes.func, + onItemClick: PropTypes.func, }; static defaultProps = { @@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent { } handleItemClick = e => { + const { onItemClick } = this.props; const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; + const item = this.props.items[i]; this.handleClose(); - if (typeof action === 'function') { + if (typeof onItemClick === 'function') { + e.preventDefault(); + onItemClick(item, i); + } else if (item && typeof item.action === 'function') { e.preventDefault(); - action(); - } else if (to) { + item.action(); + } else if (item && item.to) { e.preventDefault(); - this.context.router.history.push(to); + this.context.router.history.push(item.to); } } @@ -265,29 +290,67 @@ export default class Dropdown extends React.PureComponent { } } + close = () => { + this.handleClose(); + } + render () { - const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; + const { + icon, + items, + size, + title, + disabled, + loading, + scrollable, + dropdownPlacement, + openDropdownId, + openedViaKeyboard, + children, + renderItem, + renderHeader, + } = this.props; + const open = this.state.id === openDropdownId; + const button = children ? React.cloneElement(React.Children.only(children), { + ref: this.setTargetRef, + onClick: this.handleClick, + onMouseDown: this.handleMouseDown, + onKeyDown: this.handleButtonKeyDown, + onKeyPress: this.handleKeyPress, + }) : ( + + ); + return ( -
    - + + {button} - + -
    + ); } diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js new file mode 100644 index 000000000..8b73663d4 --- /dev/null +++ b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; +import { fetchHistory } from 'flavours/glitch/actions/history'; +import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; + +const mapStateToProps = (state, { statusId }) => ({ + dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), + openDropdownId: state.getIn(['dropdown_menu', 'openId']), + openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), + items: state.getIn(['history', statusId, 'items']), + loading: state.getIn(['history', statusId, 'loading']), +}); + +const mapDispatchToProps = (dispatch, { statusId }) => ({ + + onOpen (id, onItemClick, dropdownPlacement, keyboard) { + dispatch(fetchHistory(statusId)); + dispatch(openDropdownMenu(id, dropdownPlacement, keyboard)); + }, + + onClose (id) { + dispatch(closeDropdownMenu(id)); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/index.js b/app/javascript/flavours/glitch/components/edited_timestamp/index.js new file mode 100644 index 000000000..9648133af --- /dev/null +++ b/app/javascript/flavours/glitch/components/edited_timestamp/index.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; +import DropdownMenu from './containers/dropdown_menu_container'; +import { connect } from 'react-redux'; +import { openModal } from 'flavours/glitch/actions/modal'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import InlineAccount from 'flavours/glitch/components/inline_account'; + +const mapDispatchToProps = (dispatch, { statusId }) => ({ + + onItemClick (index) { + dispatch(openModal('COMPARE_HISTORY', { index, statusId })); + }, + +}); + +export default @connect(null, mapDispatchToProps) +@injectIntl +class EditedTimestamp extends React.PureComponent { + + static propTypes = { + statusId: PropTypes.string.isRequired, + timestamp: PropTypes.string.isRequired, + intl: PropTypes.object.isRequired, + onItemClick: PropTypes.func.isRequired, + }; + + handleItemClick = (item, i) => { + const { onItemClick } = this.props; + onItemClick(i); + }; + + renderHeader = items => { + return ( + + ); + } + + renderItem = (item, index, { onClick, onKeyPress }) => { + const formattedDate = ; + const formattedName = ; + + const label = item.get('original') ? ( + + ) : ( + + ); + + return ( +
  • + +
  • + ); + } + + render () { + const { timestamp, intl, statusId } = this.props; + + return ( + + + + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/inline_account.js b/app/javascript/flavours/glitch/components/inline_account.js new file mode 100644 index 000000000..2ef1f52cc --- /dev/null +++ b/app/javascript/flavours/glitch/components/inline_account.js @@ -0,0 +1,34 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import Avatar from 'flavours/glitch/components/avatar'; + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { accountId }) => ({ + account: getAccount(state, accountId), + }); + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) +class InlineAccount extends React.PureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + }; + + render () { + const { account } = this.props; + + return ( + + {account.get('username')} + + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/loading_indicator.js b/app/javascript/flavours/glitch/components/loading_indicator.js index d6a5adb6f..59f721c50 100644 --- a/app/javascript/flavours/glitch/components/loading_indicator.js +++ b/app/javascript/flavours/glitch/components/loading_indicator.js @@ -1,10 +1,31 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export const CircularProgress = ({ size, strokeWidth }) => { + const viewBox = `0 0 ${size} ${size}`; + const radius = (size - strokeWidth) / 2; + + return ( + + + + ); +}; + +CircularProgress.propTypes = { + size: PropTypes.number.isRequired, + strokeWidth: PropTypes.number.isRequired, +}; const LoadingIndicator = () => (
    -
    - +
    ); diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.js b/app/javascript/flavours/glitch/components/relative_timestamp.js index 711181dcd..512480339 100644 --- a/app/javascript/flavours/glitch/components/relative_timestamp.js +++ b/app/javascript/flavours/glitch/components/relative_timestamp.js @@ -5,10 +5,15 @@ import PropTypes from 'prop-types'; const messages = defineMessages({ today: { id: 'relative_time.today', defaultMessage: 'today' }, just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, + just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, + seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, + minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' }, hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, + hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' }, days: { id: 'relative_time.days', defaultMessage: '{number}d' }, + days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' }, moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, @@ -66,7 +71,7 @@ const getUnitDelay = units => { } }; -export const timeAgoString = (intl, date, now, year, timeGiven = true) => { +export const timeAgoString = (intl, date, now, year, timeGiven, short) => { const delta = now - date.getTime(); let relativeTime; @@ -74,16 +79,16 @@ export const timeAgoString = (intl, date, now, year, timeGiven = true) => { if (delta < DAY && !timeGiven) { relativeTime = intl.formatMessage(messages.today); } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.just_now); + relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full); } else if (delta < 7 * DAY) { if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) }); + relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) }); } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) }); + relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) }); } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) }); + relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) }); } else { - relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) }); + relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) }); } } else if (date.getFullYear() === year) { relativeTime = intl.formatDate(date, shortDateFormatOptions); @@ -124,6 +129,7 @@ class RelativeTimestamp extends React.Component { timestamp: PropTypes.string.isRequired, year: PropTypes.number.isRequired, futureDate: PropTypes.bool, + short: PropTypes.bool, }; state = { @@ -132,6 +138,7 @@ class RelativeTimestamp extends React.Component { static defaultProps = { year: (new Date()).getFullYear(), + short: true, }; shouldComponentUpdate (nextProps, nextState) { @@ -176,11 +183,11 @@ class RelativeTimestamp extends React.Component { } render () { - const { timestamp, intl, year, futureDate } = this.props; + const { timestamp, intl, year, futureDate, short } = this.props; const timeGiven = timestamp.includes('T'); const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven); + const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short); return (