Add option to reduce motion (#5393)
* Add option to reduce motion * Use HOC to wrap all Motion calls * fix case-sensitive issue * Avoid updating too frequently * Get rid of unnecessary change to _simple_status.html.hamlgh/stable
parent
981e20b03a
commit
fa0be3f834
|
@ -39,6 +39,7 @@ class Settings::PreferencesController < ApplicationController
|
||||||
:setting_boost_modal,
|
:setting_boost_modal,
|
||||||
:setting_delete_modal,
|
:setting_delete_modal,
|
||||||
:setting_auto_play_gif,
|
:setting_auto_play_gif,
|
||||||
|
:setting_reduce_motion,
|
||||||
:setting_system_font_ui,
|
:setting_system_font_ui,
|
||||||
:setting_noindex,
|
:setting_noindex,
|
||||||
:setting_theme,
|
:setting_theme,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../features/ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import IconButton from './icon_button';
|
import IconButton from './icon_button';
|
||||||
import Overlay from 'react-overlays/lib/Overlay';
|
import Overlay from 'react-overlays/lib/Overlay';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../features/ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import detectPassiveEvents from 'detect-passive-events';
|
import detectPassiveEvents from 'detect-passive-events';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../features/ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import Overlay from 'react-overlays/lib/Overlay';
|
import Overlay from 'react-overlays/lib/Overlay';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import detectPassiveEvents from 'detect-passive-events';
|
import detectPassiveEvents from 'detect-passive-events';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import Overlay from 'react-overlays/lib/Overlay';
|
import Overlay from 'react-overlays/lib/Overlay';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
|
|
||||||
export default class Warning extends React.PureComponent {
|
export default class Warning extends React.PureComponent {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import { changeComposeSensitivity } from '../../../actions/compose';
|
import { changeComposeSensitivity } from '../../../actions/compose';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { mountCompose, unmountCompose } from '../../actions/compose';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
import SearchContainer from './containers/search_container';
|
import SearchContainer from './containers/search_container';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import SearchResultsContainer from './containers/search_results_container';
|
import SearchResultsContainer from './containers/search_results_container';
|
||||||
import { changeComposing } from '../../actions/compose';
|
import { changeComposing } from '../../actions/compose';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Like react-motion's Motion, but checks to see if the user prefers
|
||||||
|
// reduced motion and uses a cross-fade in those cases.
|
||||||
|
|
||||||
|
import Motion from 'react-motion/lib/Motion';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
const stylesToKeep = ['opacity', 'backgroundOpacity'];
|
||||||
|
|
||||||
|
const extractValue = (value) => {
|
||||||
|
// This is either an object with a "val" property or it's a number
|
||||||
|
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const reduceMotion = state.getIn(['meta', 'reduce_motion']);
|
||||||
|
|
||||||
|
if (reduceMotion) {
|
||||||
|
const { style, defaultStyle } = ownProps;
|
||||||
|
|
||||||
|
Object.keys(style).forEach(key => {
|
||||||
|
if (stylesToKeep.includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If it's setting an x or height or scale or some other value, we need
|
||||||
|
// to preserve the end-state value without actually animating it
|
||||||
|
style[key] = defaultStyle[key] = extractValue(style[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { style, defaultStyle };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Motion);
|
|
@ -23,6 +23,7 @@ class UserSettingsDecorator
|
||||||
user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal')
|
user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal')
|
||||||
user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal')
|
user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal')
|
||||||
user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif')
|
user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif')
|
||||||
|
user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion')
|
||||||
user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui')
|
user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui')
|
||||||
user.settings['noindex'] = noindex_preference if change?('setting_noindex')
|
user.settings['noindex'] = noindex_preference if change?('setting_noindex')
|
||||||
user.settings['theme'] = theme_preference if change?('setting_theme')
|
user.settings['theme'] = theme_preference if change?('setting_theme')
|
||||||
|
@ -64,6 +65,10 @@ class UserSettingsDecorator
|
||||||
boolean_cast_setting 'setting_auto_play_gif'
|
boolean_cast_setting 'setting_auto_play_gif'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reduce_motion_preference
|
||||||
|
boolean_cast_setting 'setting_reduce_motion'
|
||||||
|
end
|
||||||
|
|
||||||
def noindex_preference
|
def noindex_preference
|
||||||
boolean_cast_setting 'setting_noindex'
|
boolean_cast_setting 'setting_noindex'
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,6 +102,10 @@ class User < ApplicationRecord
|
||||||
settings.auto_play_gif
|
settings.auto_play_gif
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_reduce_motion
|
||||||
|
settings.reduce_motion
|
||||||
|
end
|
||||||
|
|
||||||
def setting_system_font_ui
|
def setting_system_font_ui
|
||||||
settings.system_font_ui
|
settings.system_font_ui
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,6 +25,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:boost_modal] = object.current_account.user.setting_boost_modal
|
store[:boost_modal] = object.current_account.user.setting_boost_modal
|
||||||
store[:delete_modal] = object.current_account.user.setting_delete_modal
|
store[:delete_modal] = object.current_account.user.setting_delete_modal
|
||||||
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
|
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
|
||||||
|
store[:reduce_motion] = object.current_account.user.setting_reduce_motion
|
||||||
end
|
end
|
||||||
|
|
||||||
store
|
store
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
|
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
|
||||||
|
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||||
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
%div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}
|
%div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}
|
||||||
- else
|
- else
|
||||||
%div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
|
%div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
|
||||||
- elsif status.preview_cards.first
|
- elsif status.preview_cards.first
|
||||||
%div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}
|
%div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ en:
|
||||||
otp_attempt: Two-factor code
|
otp_attempt: Two-factor code
|
||||||
password: Password
|
password: Password
|
||||||
setting_auto_play_gif: Auto-play animated GIFs
|
setting_auto_play_gif: Auto-play animated GIFs
|
||||||
|
setting_reduce_motion: Reduce motion in animations
|
||||||
setting_boost_modal: Show confirmation dialog before boosting
|
setting_boost_modal: Show confirmation dialog before boosting
|
||||||
setting_default_privacy: Post privacy
|
setting_default_privacy: Post privacy
|
||||||
setting_default_sensitive: Always mark media as sensitive
|
setting_default_sensitive: Always mark media as sensitive
|
||||||
|
|
|
@ -22,6 +22,7 @@ defaults: &defaults
|
||||||
boost_modal: false
|
boost_modal: false
|
||||||
delete_modal: true
|
delete_modal: true
|
||||||
auto_play_gif: false
|
auto_play_gif: false
|
||||||
|
reduce_motion: false
|
||||||
system_font_ui: false
|
system_font_ui: false
|
||||||
noindex: false
|
noindex: false
|
||||||
theme: 'default'
|
theme: 'default'
|
||||||
|
|
Reference in New Issue