about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPatrick Figel <patrick@figel.email>2017-04-17 12:14:03 +0200
committerPatrick Figel <patrick@figel.email>2017-04-17 12:14:03 +0200
commitffb99325cafb5b00ee652c6b3ed2811a4d643fc8 (patch)
treef2497c652aaffc64e84f7156c2cdaf2559d029f1
parent1955a3f4448e1a13898c10a89e681395d6748ec3 (diff)
Add gif auto-play/pause preference
This introduces a new per-user preference called
"Auto-play animated GIFs", which is enabled by default. When a
user disables this setting, gifs in toots become click-to-play.

Previews of animated gifs were changed to display the video play
button so that users can distinguish them from regular images.

This setting also affects account avatars in the detailed account
view, which was changed to use the same hover-to-play mechanism
that is used for animated avatars in timelines.

Fixes #1652
-rw-r--r--app/assets/javascripts/components/components/media_gallery.jsx36
-rw-r--r--app/assets/javascripts/components/components/status.jsx3
-rw-r--r--app/assets/javascripts/components/containers/status_container.jsx3
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx23
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx3
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx10
-rw-r--r--app/controllers/settings/preferences_controller.rb5
-rw-r--r--app/models/user.rb4
-rw-r--r--app/views/home/initial_state.json.rabl1
-rw-r--r--app/views/settings/preferences/show.html.haml3
-rw-r--r--config/locales/simple_form.en.yml1
-rw-r--r--config/settings.yml1
12 files changed, 65 insertions, 28 deletions
diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx
index 325fd8157..c6c726a4e 100644
--- a/app/assets/javascripts/components/components/media_gallery.jsx
+++ b/app/assets/javascripts/components/components/media_gallery.jsx
@@ -78,7 +78,8 @@ const Item = React.createClass({
     attachment: ImmutablePropTypes.map.isRequired,
     index: React.PropTypes.number.isRequired,
     size: React.PropTypes.number.isRequired,
-    onClick: React.PropTypes.func.isRequired
+    onClick: React.PropTypes.func.isRequired,
+    autoPlayGif: React.PropTypes.bool.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -158,16 +159,24 @@ const Item = React.createClass({
         />
       );
     } else if (attachment.get('type') === 'gifv') {
-      thumbnail = (
-        <video
-          src={attachment.get('url')}
-          onClick={this.handleClick}
-          autoPlay={!isIOS()}
-          loop={true}
-          muted={true}
-          style={gifvThumbStyle}
-        />
-      );
+      if (isIOS() || !this.props.autoPlayGif) {
+        return (
+          <div  key={attachment.get('id')} style={{ ...itemStyle, background: `url(${attachment.get('preview_url')}) no-repeat center`, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }} onClick={this.handleClick}>
+            <div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
+          </div>
+        );
+      } else {
+        thumbnail = (
+            <video
+              src={attachment.get('url')}
+              onClick={this.handleClick}
+              autoPlay
+              loop={true}
+              muted={true}
+              style={gifvThumbStyle}
+            />
+        );
+      }
     }
 
     return (
@@ -192,7 +201,8 @@ const MediaGallery = React.createClass({
     media: ImmutablePropTypes.list.isRequired,
     height: React.PropTypes.number.isRequired,
     onOpenMedia: React.PropTypes.func.isRequired,
-    intl: React.PropTypes.object.isRequired
+    intl: React.PropTypes.object.isRequired,
+    autoPlayGif: React.PropTypes.bool.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -227,7 +237,7 @@ const MediaGallery = React.createClass({
       );
     } else {
       const size = media.take(4).size;
-      children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
+      children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
     }
 
     return (
diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx
index d2d2aaf20..abc123f26 100644
--- a/app/assets/javascripts/components/components/status.jsx
+++ b/app/assets/javascripts/components/components/status.jsx
@@ -29,6 +29,7 @@ const Status = React.createClass({
     onBlock: React.PropTypes.func,
     me: React.PropTypes.number,
     boostModal: React.PropTypes.bool,
+    autoPlayGif: React.PropTypes.bool,
     muted: React.PropTypes.bool
   },
 
@@ -79,7 +80,7 @@ const Status = React.createClass({
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
         media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
       } else {
-        media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
+        media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
       }
     }
 
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index f704ac722..df091de04 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -27,7 +27,8 @@ const makeMapStateToProps = () => {
   const mapStateToProps = (state, props) => ({
     status: getStatus(state, props.id),
     me: state.getIn(['meta', 'me']),
-    boostModal: state.getIn(['meta', 'boost_modal'])
+    boostModal: state.getIn(['meta', 'boost_modal']),
+    autoPlayGif: state.getIn(['meta', 'auto_play_gif'])
   });
 
   return mapStateToProps;
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx
index c4619a3c7..c097fbbd6 100644
--- a/app/assets/javascripts/components/features/account/components/header.jsx
+++ b/app/assets/javascripts/components/features/account/components/header.jsx
@@ -5,6 +5,7 @@ import escapeTextContentForBrowser from 'escape-html';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import IconButton from '../../../components/icon_button';
 import { Motion, spring } from 'react-motion';
+import { connect } from 'react-redux';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -12,10 +13,19 @@ const messages = defineMessages({
   requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
 });
 
+const makeMapStateToProps = () => {
+  const mapStateToProps = (state, props) => ({
+    autoPlayGif: state.getIn(['meta', 'auto_play_gif'])
+  });
+
+  return mapStateToProps;
+};
+
 const Avatar = React.createClass({
 
   propTypes: {
-    account: ImmutablePropTypes.map.isRequired
+    account: ImmutablePropTypes.map.isRequired,
+    autoPlayGif: React.PropTypes.bool.isRequired
   },
 
   getInitialState () {
@@ -37,7 +47,7 @@ const Avatar = React.createClass({
   },
 
   render () {
-    const { account }   = this.props;
+    const { account, autoPlayGif }   = this.props;
     const { isHovered } = this.state;
 
     return (
@@ -53,7 +63,7 @@ const Avatar = React.createClass({
             onMouseOut={this.handleMouseOut}
             onFocus={this.handleMouseOver}
             onBlur={this.handleMouseOut}>
-            <img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
+            <img src={autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
           </a>
         }
       </Motion>
@@ -68,7 +78,8 @@ const Header = React.createClass({
     account: ImmutablePropTypes.map,
     me: React.PropTypes.number.isRequired,
     onFollow: React.PropTypes.func.isRequired,
-    intl: React.PropTypes.object.isRequired
+    intl: React.PropTypes.object.isRequired,
+    autoPlayGif: React.PropTypes.bool.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -119,7 +130,7 @@ const Header = React.createClass({
     return (
       <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
         <div style={{ padding: '20px 10px' }}>
-          <Avatar account={account} />
+          <Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
 
           <span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
           <span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
@@ -134,4 +145,4 @@ const Header = React.createClass({
 
 });
 
-export default injectIntl(Header);
+export default connect(makeMapStateToProps)(injectIntl(Header));
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
index ceafc1a32..bd386b251 100644
--- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx
+++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
@@ -19,6 +19,7 @@ const DetailedStatus = React.createClass({
     status: ImmutablePropTypes.map.isRequired,
     onOpenMedia: React.PropTypes.func.isRequired,
     onOpenVideo: React.PropTypes.func.isRequired,
+    autoPlayGif: React.PropTypes.bool,
   },
 
   mixins: [PureRenderMixin],
@@ -42,7 +43,7 @@ const DetailedStatus = React.createClass({
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
         media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
       } else {
-        media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
+        media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
       }
     } else {
       media = <CardContainer statusId={status.get('id')} />;
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 7ead68807..ca6e08cdc 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -39,7 +39,8 @@ const makeMapStateToProps = () => {
     ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
     descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
     me: state.getIn(['meta', 'me']),
-    boostModal: state.getIn(['meta', 'boost_modal'])
+    boostModal: state.getIn(['meta', 'boost_modal']),
+    autoPlayGif: state.getIn(['meta', 'auto_play_gif'])
   });
 
   return mapStateToProps;
@@ -57,7 +58,8 @@ const Status = React.createClass({
     ancestorsIds: ImmutablePropTypes.list,
     descendantsIds: ImmutablePropTypes.list,
     me: React.PropTypes.number,
-    boostModal: React.PropTypes.bool
+    boostModal: React.PropTypes.bool,
+    autoPlayGif: React.PropTypes.bool
   },
 
   mixins: [PureRenderMixin],
@@ -126,7 +128,7 @@ const Status = React.createClass({
 
   render () {
     let ancestors, descendants;
-    const { status, ancestorsIds, descendantsIds, me } = this.props;
+    const { status, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props;
 
     if (status === null) {
       return (
@@ -155,7 +157,7 @@ const Status = React.createClass({
           <div className='scrollable'>
             {ancestors}
 
-            <DetailedStatus status={status} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} />
+            <DetailedStatus status={status} autoPlayGif={autoPlayGif} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} />
             <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} />
 
             {descendants}
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index c758e4ef2..f66eb9752 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -24,8 +24,9 @@ class Settings::PreferencesController < ApplicationController
 
     current_user.settings['default_privacy'] = user_params[:setting_default_privacy]
     current_user.settings['boost_modal'] = user_params[:setting_boost_modal] == '1'
+    current_user.settings['auto_play_gif'] = user_params[:setting_auto_play_gif] == '1'
 
-    if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy, :setting_boost_modal))
+    if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy, :setting_boost_modal, :setting_auto_play_gif))
       redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
     else
       render action: :show
@@ -35,6 +36,6 @@ class Settings::PreferencesController < ApplicationController
   private
 
   def user_params
-    params.require(:user).permit(:locale, :setting_default_privacy, :setting_boost_modal, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following])
+    params.require(:user).permit(:locale, :setting_default_privacy, :setting_boost_modal, :setting_auto_play_gif, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following])
   end
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index 27a38674e..d50101baf 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -32,4 +32,8 @@ class User < ApplicationRecord
   def setting_boost_modal
     settings.boost_modal
   end
+
+  def setting_auto_play_gif
+    settings.auto_play_gif
+  end
 end
diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl
index ce7bfbd44..a2ab2d060 100644
--- a/app/views/home/initial_state.json.rabl
+++ b/app/views/home/initial_state.json.rabl
@@ -9,6 +9,7 @@ node(:meta) do
     me: current_account.id,
     admin: @admin.try(:id),
     boost_modal: current_account.user.setting_boost_modal,
+    auto_play_gif: current_account.user.setting_auto_play_gif,
   }
 end
 
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index e819429b6..3fdcca041 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -25,5 +25,8 @@
   .fields-group
     = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
 
+  .fields-group
+    = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
+
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index c25407f2b..5335b0927 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -28,6 +28,7 @@ en:
         note: Bio
         otp_attempt: Two-factor code
         password: Password
+        setting_auto_play_gif: Auto-play animated GIFs
         setting_boost_modal: Show confirmation dialog before boosting
         setting_default_privacy: Post privacy
         severity: Severity
diff --git a/config/settings.yml b/config/settings.yml
index 04213fd0b..9813963b2 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -15,6 +15,7 @@ defaults: &defaults
   open_registrations: true
   closed_registrations_message: ''
   boost_modal: false
+  auto_play_gif: true
   notification_emails:
     follow: false
     reblog: false