Merge branch 'patf-pause-gif'

This commit is contained in:
Eugen Rochko 2017-04-18 01:58:14 +02:00
commit 93c13fe691
13 changed files with 95 additions and 30 deletions

View file

@ -78,7 +78,8 @@ const Item = React.createClass({
attachment: ImmutablePropTypes.map.isRequired, attachment: ImmutablePropTypes.map.isRequired,
index: React.PropTypes.number.isRequired, index: React.PropTypes.number.isRequired,
size: 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], mixins: [PureRenderMixin],
@ -158,15 +159,21 @@ const Item = React.createClass({
/> />
); );
} else if (attachment.get('type') === 'gifv') { } else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.props.autoPlayGif;
thumbnail = ( thumbnail = (
<video <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }} className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
src={attachment.get('url')} <video
onClick={this.handleClick} src={attachment.get('url')}
autoPlay={!isIOS()} onClick={this.handleClick}
loop={true} autoPlay={autoPlay}
muted={true} loop={true}
style={gifvThumbStyle} muted={true}
/> style={gifvThumbStyle}
/>
<span className='media-gallery__gifv__label'>GIF</span>
</div>
); );
} }
@ -192,7 +199,8 @@ const MediaGallery = React.createClass({
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
height: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired,
onOpenMedia: React.PropTypes.func.isRequired, onOpenMedia: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired intl: React.PropTypes.object.isRequired,
autoPlayGif: React.PropTypes.bool.isRequired
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
@ -227,7 +235,7 @@ const MediaGallery = React.createClass({
); );
} else { } else {
const size = media.take(4).size; 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 ( return (

View file

@ -29,6 +29,7 @@ const Status = React.createClass({
onBlock: React.PropTypes.func, onBlock: React.PropTypes.func,
me: React.PropTypes.number, me: React.PropTypes.number,
boostModal: React.PropTypes.bool, boostModal: React.PropTypes.bool,
autoPlayGif: React.PropTypes.bool,
muted: React.PropTypes.bool muted: React.PropTypes.bool
}, },
@ -79,7 +80,7 @@ const Status = React.createClass({
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />; media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
} else { } 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} />;
} }
} }

View file

@ -27,7 +27,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id), status: getStatus(state, props.id),
me: state.getIn(['meta', 'me']), 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; return mapStateToProps;

View file

@ -5,6 +5,7 @@ import escapeTextContentForBrowser from 'escape-html';
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, spring } from 'react-motion'; import { Motion, spring } from 'react-motion';
import { connect } from 'react-redux';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -12,10 +13,19 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' } 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({ const Avatar = React.createClass({
propTypes: { propTypes: {
account: ImmutablePropTypes.map.isRequired account: ImmutablePropTypes.map.isRequired,
autoPlayGif: React.PropTypes.bool.isRequired
}, },
getInitialState () { getInitialState () {
@ -37,7 +47,7 @@ const Avatar = React.createClass({
}, },
render () { render () {
const { account } = this.props; const { account, autoPlayGif } = this.props;
const { isHovered } = this.state; const { isHovered } = this.state;
return ( return (
@ -48,13 +58,12 @@ const Avatar = React.createClass({
className='account__header__avatar' className='account__header__avatar'
target='_blank' target='_blank'
rel='noopener' rel='noopener'
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden', backgroundSize: '90px 90px', backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
onMouseOver={this.handleMouseOver} onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut} onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver} onFocus={this.handleMouseOver}
onBlur={this.handleMouseOut}> onBlur={this.handleMouseOut}
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} /> />
</a>
} }
</Motion> </Motion>
); );
@ -68,7 +77,8 @@ const Header = React.createClass({
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
me: React.PropTypes.number.isRequired, me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func.isRequired, onFollow: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired intl: React.PropTypes.object.isRequired,
autoPlayGif: React.PropTypes.bool.isRequired
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
@ -119,7 +129,7 @@ const Header = React.createClass({
return ( return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div style={{ padding: '20px 10px' }}> <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 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> <span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
@ -134,4 +144,4 @@ const Header = React.createClass({
}); });
export default injectIntl(Header); export default connect(makeMapStateToProps)(injectIntl(Header));

View file

@ -19,6 +19,7 @@ const DetailedStatus = React.createClass({
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
onOpenMedia: React.PropTypes.func.isRequired, onOpenMedia: React.PropTypes.func.isRequired,
onOpenVideo: React.PropTypes.func.isRequired, onOpenVideo: React.PropTypes.func.isRequired,
autoPlayGif: React.PropTypes.bool,
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
@ -42,7 +43,7 @@ const DetailedStatus = React.createClass({
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { 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 />; media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
} else { } 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 { } else {
media = <CardContainer statusId={status.get('id')} />; media = <CardContainer statusId={status.get('id')} />;

View file

@ -39,7 +39,8 @@ const makeMapStateToProps = () => {
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]), ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]), descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
me: state.getIn(['meta', 'me']), 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; return mapStateToProps;
@ -57,7 +58,8 @@ const Status = React.createClass({
ancestorsIds: ImmutablePropTypes.list, ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list,
me: React.PropTypes.number, me: React.PropTypes.number,
boostModal: React.PropTypes.bool boostModal: React.PropTypes.bool,
autoPlayGif: React.PropTypes.bool
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
@ -126,7 +128,7 @@ const Status = React.createClass({
render () { render () {
let ancestors, descendants; let ancestors, descendants;
const { status, ancestorsIds, descendantsIds, me } = this.props; const { status, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props;
if (status === null) { if (status === null) {
return ( return (
@ -155,7 +157,7 @@ const Status = React.createClass({
<div className='scrollable'> <div className='scrollable'>
{ancestors} {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} /> <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} />
{descendants} {descendants}

View file

@ -2315,3 +2315,34 @@ button.icon-button.active i.fa-retweet {
top: 0; top: 0;
left: 0; left: 0;
} }
.media-gallery__gifv__label {
display: block;
position: absolute;
color: $color5;
background: rgba($color8, 0.5);
bottom: 6px;
left: 6px;
padding: 2px 6px;
border-radius: 2px;
font-size: 11px;
font-weight: 600;
z-index: 1;
pointer-events: none;
opacity: 0.9;
transition: opacity 0.1s ease;
}
.media-gallery__gifv {
&.autoplay {
.media-gallery__gifv__label {
display: none;
}
}
&:hover {
.media-gallery__gifv__label {
opacity: 1;
}
}
}

View file

@ -23,9 +23,10 @@ class Settings::PreferencesController < ApplicationController
} }
current_user.settings['default_privacy'] = user_params[:setting_default_privacy] current_user.settings['default_privacy'] = user_params[:setting_default_privacy]
current_user.settings['boost_modal'] = user_params[:setting_boost_modal] == '1' 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') redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
else else
render action: :show render action: :show
@ -35,6 +36,6 @@ class Settings::PreferencesController < ApplicationController
private private
def user_params 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
end end

View file

@ -30,4 +30,8 @@ class User < ApplicationRecord
def setting_boost_modal def setting_boost_modal
settings.boost_modal settings.boost_modal
end end
def setting_auto_play_gif
settings.auto_play_gif
end
end end

View file

@ -9,6 +9,7 @@ node(:meta) do
me: current_account.id, me: current_account.id,
admin: @admin.try(:id), admin: @admin.try(:id),
boost_modal: current_account.user.setting_boost_modal, boost_modal: current_account.user.setting_boost_modal,
auto_play_gif: current_account.user.setting_auto_play_gif,
} }
end end

View file

@ -25,5 +25,8 @@
.fields-group .fields-group
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
.actions .actions
= f.button :button, t('generic.save_changes'), type: :submit = f.button :button, t('generic.save_changes'), type: :submit

View file

@ -28,6 +28,7 @@ en:
note: Bio note: Bio
otp_attempt: Two-factor code otp_attempt: Two-factor code
password: Password password: Password
setting_auto_play_gif: Auto-play animated GIFs
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
severity: Severity severity: Severity

View file

@ -15,6 +15,7 @@ defaults: &defaults
open_registrations: true open_registrations: true
closed_registrations_message: '' closed_registrations_message: ''
boost_modal: false boost_modal: false
auto_play_gif: true
notification_emails: notification_emails:
follow: false follow: false
reblog: false reblog: false