diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 8664320ab..196da7c99 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -118,6 +118,7 @@ class Status extends ImmutablePureComponent { unread: PropTypes.bool, showThread: PropTypes.bool, isQuotedPost: PropTypes.bool, + shouldHighlightOnMount: PropTypes.bool, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, cacheMediaWidth: PropTypes.func, @@ -567,6 +568,7 @@ class Status extends ImmutablePureComponent { 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'status--is-quote': isQuotedPost, 'status--has-quote': !!status.get('quote'), + 'status--highlighted-entry': this.props.shouldHighlightOnMount, }) } data-id={status.get('id')} diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 8bab174f6..fb8f3d81d 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -5,6 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { withRouter } from 'react-router-dom'; +import { difference } from 'lodash'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -150,6 +151,11 @@ class Status extends ImmutablePureComponent { fullscreen: false, showMedia: defaultMediaVisibility(this.props.status), loadedStatusId: undefined, + /** + * Holds the ids of newly added replies, excluding the initial load. + * Used to highlight newly added replies in the UI + */ + newRepliesIds: [], }; UNSAFE_componentWillMount () { @@ -462,6 +468,7 @@ class Status extends ImmutablePureComponent { previousId={i > 0 ? list[i - 1] : undefined} nextId={list[i + 1] || (ancestors && statusId)} rootId={statusId} + shouldHighlightOnMount={this.state.newRepliesIds.includes(id)} /> )); } @@ -495,11 +502,20 @@ class Status extends ImmutablePureComponent { } componentDidUpdate (prevProps) { - const { status, ancestorsIds } = this.props; + const { status, ancestorsIds, descendantsIds } = this.props; if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) { this._scrollStatusIntoView(); } + + // Only highlight replies after the initial load + if (prevProps.descendantsIds.length) { + const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds); + + if (newRepliesIds.length) { + this.setState({newRepliesIds}); + } + } } componentWillUnmount () { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index acfc906dc..079985c40 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1597,6 +1597,15 @@ } } } + + .no-reduce-motion &--highlighted-entry::before { + content: ''; + position: absolute; + inset: 0; + background: rgb(from $ui-highlight-color r g b / 20%); + opacity: 0; + animation: fade 0.7s reverse both 0.3s; + } } .status__relative-time {