diff options
-rw-r--r-- | app/javascript/mastodon/components/icon_button.js | 3 | ||||
-rw-r--r-- | app/javascript/mastodon/components/status.js | 47 | ||||
-rw-r--r-- | app/javascript/mastodon/components/status_action_bar.js | 3 | ||||
-rw-r--r-- | app/javascript/mastodon/locales/defaultMessages.json | 10 | ||||
-rw-r--r-- | app/javascript/mastodon/locales/en.json | 2 | ||||
-rw-r--r-- | app/javascript/styles/components.scss | 18 |
6 files changed, 71 insertions, 12 deletions
diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index ac734f5ad..f4cedb854 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -17,6 +17,7 @@ export default class IconButton extends React.PureComponent { disabled: PropTypes.bool, inverted: PropTypes.bool, animate: PropTypes.bool, + flip: PropTypes.bool, overlay: PropTypes.bool, }; @@ -69,7 +70,7 @@ export default class IconButton extends React.PureComponent { } return ( - <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> + <Motion defaultStyle={{ rotate: this.props.active ? (this.props.flip ? -180 : -360) : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? (this.props.flip ? -180 : -360) : 0, { stiffness: 120, damping: 7 }) : 0 }}> {({ rotate }) => <button aria-label={this.props.title} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 81196c82a..c78144715 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -3,18 +3,24 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Avatar from './avatar'; import AvatarOverlay from './avatar_overlay'; -import RelativeTimestamp from './relative_timestamp'; import DisplayName from './display_name'; import MediaGallery from './media_gallery'; import VideoPlayer from './video_player'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; -import { FormattedMessage } from 'react-intl'; +import IconButton from './icon_button'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import emojify from '../emoji'; import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; +const messages = defineMessages({ + collapse: { id: 'status.collapse', defaultMessage: 'Collapse' }, + uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' }, +}); + +@injectIntl export default class Status extends ImmutablePureComponent { static contextTypes = { @@ -37,12 +43,14 @@ export default class Status extends ImmutablePureComponent { autoPlayGif: PropTypes.bool, muted: PropTypes.bool, intersectionObserverWrapper: PropTypes.object, + intl: PropTypes.object.isRequired, }; state = { isExpanded: false, isIntersecting: true, // assume intersecting until told otherwise isHidden: false, // set to true in requestIdleCallback to trigger un-render + isCollapsed: false, } // Avoid checking props that are functions (and whose equality will always @@ -60,7 +68,11 @@ export default class Status extends ImmutablePureComponent { updateOnStates = ['isExpanded'] shouldComponentUpdate (nextProps, nextState) { - if (!nextState.isIntersecting && nextState.isHidden) { + if (nextState.isCollapsed !== this.state.isCollapsed) { + // If the collapsed state of the element has changed then we definitely + // need to re-update. + return true; + } else if (!nextState.isIntersecting && nextState.isHidden) { // It's only if we're not intersecting (i.e. offscreen) and isHidden is true // that either "isIntersecting" or "isHidden" matter, and then they're // the only things that matter. @@ -75,6 +87,8 @@ export default class Status extends ImmutablePureComponent { } componentDidMount () { + const node = this.node; + if (!this.props.intersectionObserverWrapper) { // TODO: enable IntersectionObserver optimization for notification statuses. // These are managed in notifications/index.js rather than status_list.js @@ -86,6 +100,8 @@ export default class Status extends ImmutablePureComponent { this.handleIntersection ); + if (node.clientHeight > 400) this.setState({ isCollapsed: true }); + this.componentMounted = true; } @@ -150,14 +166,18 @@ export default class Status extends ImmutablePureComponent { this.setState({ isExpanded: !this.state.isExpanded }); }; + handleCollapsedClick = () => { + this.setState({ isCollapsed: !this.state.isCollapsed }); + } + render () { let media = null; let statusAvatar; // Exclude intersectionObserverWrapper from `other` variable // because intersection is managed in here. - const { status, account, intersectionObserverWrapper, ...other } = this.props; - const { isExpanded, isIntersecting, isHidden } = this.state; + const { status, account, intersectionObserverWrapper, intl ...other } = this.props; + const { isExpanded, isIntersecting, isHidden, isCollapsed } = this.state; if (status === null) { return null; @@ -210,9 +230,17 @@ export default class Status extends ImmutablePureComponent { } return ( - <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}> + <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef}> <div className='status__info'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + + <IconButton + className='status__collapse-button' + animate flip + active={isCollapsed} + title={isCollapsed ? intl.formatMessage(messages.uncollapse) : intl.formatMessage(messages.collapse)} + icon='angle-double-down' + onClick={this.handleCollapsedClick} + /> <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'> <div className='status__avatar'> @@ -221,13 +249,14 @@ export default class Status extends ImmutablePureComponent { <DisplayName account={status.get('account')} /> </a> + </div> <StatusContent status={status} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} onHeightUpdate={this.saveHeight} /> - {media} + {isCollapsed ? null : media} - <StatusActionBar {...this.props} /> + {isCollapsed ? null : <StatusActionBar {...this.props} />} </div> ); } diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index edb2d6eb0..a1e1a135a 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -5,6 +5,7 @@ import IconButton from './icon_button'; import DropdownMenu from './dropdown_menu'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -145,6 +146,8 @@ export default class StatusActionBar extends ImmutablePureComponent { <div className='status__action-bar-dropdown'> <DropdownMenu items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' /> </div> + + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> </div> ); } diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 09cc7e1d3..dd790f659 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -191,6 +191,14 @@ { "defaultMessage": "{name} boosted", "id": "status.reblogged_by" + }, + { + "defaultMessage": "Collapse", + "id": "status.collapse" + }, + { + "defaultMessage": "Uncollapse", + "id": "status.uncollapse" } ], "path": "app/javascript/mastodon/components/status.json" @@ -1170,4 +1178,4 @@ ], "path": "app/javascript/mastodon/features/ui/components/video_modal.json" } -] \ No newline at end of file +] diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 608d911e9..8fb409618 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -143,6 +143,7 @@ "search.placeholder": "Search", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.cannot_reblog": "This post cannot be boosted", + "status.collapse": "Collapse", "status.delete": "Delete", "status.favourite": "Favourite", "status.load_more": "Load more", @@ -159,6 +160,7 @@ "status.sensitive_warning": "Sensitive content", "status.show_less": "Show less", "status.show_more": "Show more", + "status.uncollapse": "Uncollapse", "status.unmute_conversation": "Unmute conversation", "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 10ce06940..4ae371c44 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -459,6 +459,7 @@ word-wrap: break-word; font-weight: 400; overflow: hidden; + text-overflow: ellipsis; white-space: pre-wrap; .emojione { @@ -542,8 +543,10 @@ padding: 8px 10px; padding-left: 68px; position: relative; + height: auto; min-height: 48px; border-bottom: 1px solid lighten($ui-base-color, 8%); + overflow: hidden; cursor: default; @keyframes fade { @@ -598,6 +601,14 @@ } } } + + &.status-collapsed { + height: 48px; + + .status__content { + height: 20px; + } + } } .notification-favourite { @@ -611,8 +622,8 @@ } .status__relative-time { + margin-left: auto; color: lighten($ui-base-color, 26%); - float: right; font-size: 14px; } @@ -628,6 +639,11 @@ .status__info { font-size: 15px; + line-height: 28px; +} + +.status__collapse-button { + float: right; } .status-check-box { |