diff options
author | Patrick Figel <patrick@figel.email> | 2017-04-17 12:14:03 +0200 |
---|---|---|
committer | Patrick Figel <patrick@figel.email> | 2017-04-17 12:14:03 +0200 |
commit | ffb99325cafb5b00ee652c6b3ed2811a4d643fc8 (patch) | |
tree | f2497c652aaffc64e84f7156c2cdaf2559d029f1 /app/assets/javascripts/components | |
parent | 1955a3f4448e1a13898c10a89e681395d6748ec3 (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
Diffstat (limited to 'app/assets/javascripts/components')
6 files changed, 52 insertions, 26 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} |