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.haml
This commit is contained in:
		
							parent
							
								
									a5d60195fc
								
							
						
					
					
						commit
						a54c272a1f
					
				
					 21 changed files with 61 additions and 13 deletions
				
			
		| 
						 | 
					@ -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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										34
									
								
								app/javascript/mastodon/features/ui/util/optional_motion.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/javascript/mastodon/features/ui/util/optional_motion.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue