Fix #431 - convert gif to webm during upload. Web UI treats them like it did
before. In the API, attachments now can be either image, video or gifv. Gifv is to be treated like images in terms of behaviour, but are videos by file type.gh/stable
parent
4cbeb9a7eb
commit
caf5b8e975
|
@ -75,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
||||||
|
|
||||||
export function fetchAccount(id) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
dispatch(fetchRelationships([id]));
|
||||||
|
|
||||||
|
if (getState().getIn(['accounts', id], null) !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchAccountRequest(id));
|
dispatch(fetchAccountRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||||
dispatch(fetchAccountSuccess(response.data));
|
dispatch(fetchAccountSuccess(response.data));
|
||||||
dispatch(fetchRelationships([id]));
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchAccountFail(id, error));
|
dispatch(fetchAccountFail(id, error));
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
|
||||||
|
const ExtendedVideoPlayer = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
src: React.PropTypes.string.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<video src={this.props.src} autoPlay muted loop />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ExtendedVideoPlayer;
|
|
@ -43,6 +43,141 @@ const spoilerButtonStyle = {
|
||||||
zIndex: '100'
|
zIndex: '100'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const itemStyle = {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
position: 'relative',
|
||||||
|
float: 'left',
|
||||||
|
border: 'none',
|
||||||
|
display: 'block'
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbStyle = {
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
cursor: 'zoom-in'
|
||||||
|
};
|
||||||
|
|
||||||
|
const gifvThumbStyle = {
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: '1',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
cursor: 'zoom-in'
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
attachment: ImmutablePropTypes.map.isRequired,
|
||||||
|
index: React.PropTypes.number.isRequired,
|
||||||
|
size: React.PropTypes.number.isRequired,
|
||||||
|
onClick: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleClick (e) {
|
||||||
|
const { index, onClick } = this.props;
|
||||||
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { attachment, index, size } = this.props;
|
||||||
|
|
||||||
|
let width = 50;
|
||||||
|
let height = 100;
|
||||||
|
let top = 'auto';
|
||||||
|
let left = 'auto';
|
||||||
|
let bottom = 'auto';
|
||||||
|
let right = 'auto';
|
||||||
|
|
||||||
|
if (size === 1) {
|
||||||
|
width = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 4 || (size === 3 && index > 0)) {
|
||||||
|
height = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 2) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 3) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else if (index > 0) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else if (index > 1) {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 4) {
|
||||||
|
if (index === 0 || index === 2) {
|
||||||
|
right = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1 || index === 3) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 2) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumbnail = '';
|
||||||
|
|
||||||
|
if (attachment.get('type') === 'image') {
|
||||||
|
thumbnail = (
|
||||||
|
<a
|
||||||
|
href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
target='_blank'
|
||||||
|
style={{ background: `url(${attachment.get('preview_url')}) no-repeat center`, ...thumbStyle }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
|
thumbnail = (
|
||||||
|
<video
|
||||||
|
src={attachment.get('url')}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
autoPlay={true}
|
||||||
|
loop={true}
|
||||||
|
muted={true}
|
||||||
|
style={gifvThumbStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={attachment.get('id')} style={{ ...itemStyle, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||||
|
{thumbnail}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
const MediaGallery = React.createClass({
|
const MediaGallery = React.createClass({
|
||||||
|
|
||||||
getInitialState () {
|
getInitialState () {
|
||||||
|
@ -61,17 +196,12 @@ const MediaGallery = React.createClass({
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
handleClick (index, e) {
|
handleOpen (e) {
|
||||||
if (e.button === 0) {
|
this.setState({ visible: !this.state.visible });
|
||||||
e.preventDefault();
|
|
||||||
this.props.onOpenMedia(this.props.media, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleOpen () {
|
handleClick (index) {
|
||||||
this.setState({ visible: !this.state.visible });
|
this.props.onOpenMedia(this.props.media, index);
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -80,87 +210,31 @@ const MediaGallery = React.createClass({
|
||||||
let children;
|
let children;
|
||||||
|
|
||||||
if (!this.state.visible) {
|
if (!this.state.visible) {
|
||||||
|
let warning;
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
children = (
|
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
children = (
|
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
children = (
|
||||||
|
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||||
|
<span style={spoilerSpanStyle}>{warning}</span>
|
||||||
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} 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) => {
|
|
||||||
let width = 50;
|
|
||||||
let height = 100;
|
|
||||||
let top = 'auto';
|
|
||||||
let left = 'auto';
|
|
||||||
let bottom = 'auto';
|
|
||||||
let right = 'auto';
|
|
||||||
|
|
||||||
if (size === 1) {
|
|
||||||
width = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size === 4 || (size === 3 && i > 0)) {
|
|
||||||
height = 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size === 2) {
|
|
||||||
if (i === 0) {
|
|
||||||
right = '2px';
|
|
||||||
} else {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
} else if (size === 3) {
|
|
||||||
if (i === 0) {
|
|
||||||
right = '2px';
|
|
||||||
} else if (i > 0) {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === 1) {
|
|
||||||
bottom = '2px';
|
|
||||||
} else if (i > 1) {
|
|
||||||
top = '2px';
|
|
||||||
}
|
|
||||||
} else if (size === 4) {
|
|
||||||
if (i === 0 || i === 2) {
|
|
||||||
right = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === 1 || i === 3) {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < 2) {
|
|
||||||
bottom = '2px';
|
|
||||||
} else {
|
|
||||||
top = '2px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={attachment.get('id')} style={{ boxSizing: 'border-box', position: 'relative', left: left, top: top, right: right, bottom: bottom, float: 'left', border: 'none', display: 'block', width: `${width}%`, height: `${height}%` }}>
|
|
||||||
<a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, i)} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
||||||
<div style={spoilerButtonStyle} >
|
<div style={spoilerButtonStyle}>
|
||||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
|
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -74,8 +74,8 @@ const Status = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0 && !this.props.muted) {
|
if (status.get('media_attachments').size > 0 && !this.props.muted) {
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) {
|
||||||
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />;
|
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} autoplay={status.getIn(['media_attachments', 0, 'type']) === 'gifv'} sensitive={status.get('sensitive')} />;
|
||||||
} 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} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,7 +175,7 @@ const VideoPlayer = React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleOpen}>
|
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
|
@ -197,7 +197,7 @@ const VideoPlayer = React.createClass({
|
||||||
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
{muteButton}
|
{muteButton}
|
||||||
<video ref={this.setRef} src={media.get('url')} autoPlay='true' loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
<video ref={this.setRef} src={media.get('url')} autoPlay={true} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0) {
|
if (status.get('media_attachments').size > 0) {
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) {
|
||||||
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />;
|
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} 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} />;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import ImageLoader from 'react-imageloader';
|
||||||
import LoadingIndicator from '../../../components/loading_indicator';
|
import LoadingIndicator from '../../../components/loading_indicator';
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ExtendedVideoPlayer from '../../../components/extended_video_player';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
media: state.getIn(['modal', 'media']),
|
media: state.getIn(['modal', 'media']),
|
||||||
|
@ -131,27 +132,34 @@ const Modal = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = media.get(index).get('url');
|
const attachment = media.get(index);
|
||||||
|
const url = attachment.get('url');
|
||||||
|
|
||||||
let leftNav, rightNav;
|
let leftNav, rightNav, content;
|
||||||
|
|
||||||
leftNav = rightNav = '';
|
leftNav = rightNav = content = '';
|
||||||
|
|
||||||
if (media.size > 1) {
|
if (media.size > 1) {
|
||||||
leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||||
rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (attachment.get('type') === 'image') {
|
||||||
<Lightbox {...other}>
|
content = (
|
||||||
{leftNav}
|
|
||||||
|
|
||||||
<ImageLoader
|
<ImageLoader
|
||||||
src={url}
|
src={url}
|
||||||
preloader={preloader}
|
preloader={preloader}
|
||||||
imgProps={{ style: imageStyle }}
|
imgProps={{ style: imageStyle }}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
|
content = <ExtendedVideoPlayer src={url} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Lightbox {...other}>
|
||||||
|
{leftNav}
|
||||||
|
{content}
|
||||||
{rightNav}
|
{rightNav}
|
||||||
</Lightbox>
|
</Lightbox>
|
||||||
);
|
);
|
||||||
|
|
|
@ -104,8 +104,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 110px;
|
position: relative;
|
||||||
display: flex;
|
|
||||||
|
.status__attachments__inner {
|
||||||
|
display: flex;
|
||||||
|
height: 214px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,8 +188,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 300px;
|
position: relative;
|
||||||
display: flex;
|
|
||||||
|
.status__attachments__inner {
|
||||||
|
display: flex;
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player {
|
||||||
|
@ -231,11 +239,19 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-item {
|
.video-item {
|
||||||
max-width: 196px;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -258,6 +274,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -1,15 +1,32 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class MediaAttachment < ApplicationRecord
|
class MediaAttachment < ApplicationRecord
|
||||||
|
self.inheritance_column = nil
|
||||||
|
|
||||||
|
enum type: [:image, :gifv, :video]
|
||||||
|
|
||||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||||
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
|
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
|
||||||
|
|
||||||
|
IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
|
||||||
|
VIDEO_STYLES = {
|
||||||
|
small: {
|
||||||
|
convert_options: {
|
||||||
|
output: {
|
||||||
|
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format: 'png',
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
}.freeze
|
||||||
|
|
||||||
belongs_to :account, inverse_of: :media_attachments
|
belongs_to :account, inverse_of: :media_attachments
|
||||||
belongs_to :status, inverse_of: :media_attachments
|
belongs_to :status, inverse_of: :media_attachments
|
||||||
|
|
||||||
has_attached_file :file,
|
has_attached_file :file,
|
||||||
styles: -> (f) { file_styles f },
|
styles: ->(f) { file_styles f },
|
||||||
processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] },
|
processors: ->(f) { file_processors f },
|
||||||
convert_options: { all: '-quality 90 -strip' }
|
convert_options: { all: '-quality 90 -strip' }
|
||||||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
|
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
|
||||||
validates_attachment_size :file, less_than: 8.megabytes
|
validates_attachment_size :file, less_than: 8.megabytes
|
||||||
|
@ -27,45 +44,45 @@ class MediaAttachment < ApplicationRecord
|
||||||
self.file = URI.parse(url)
|
self.file = URI.parse(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def image?
|
|
||||||
IMAGE_MIME_TYPES.include? file_content_type
|
|
||||||
end
|
|
||||||
|
|
||||||
def video?
|
|
||||||
VIDEO_MIME_TYPES.include? file_content_type
|
|
||||||
end
|
|
||||||
|
|
||||||
def type
|
|
||||||
image? ? 'image' : 'video'
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
shortcode
|
shortcode
|
||||||
end
|
end
|
||||||
|
|
||||||
before_create :set_shortcode
|
before_create :set_shortcode
|
||||||
|
before_post_process :set_type
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
private
|
private
|
||||||
|
|
||||||
def file_styles(f)
|
def file_styles(f)
|
||||||
if f.instance.image?
|
if f.instance.file_content_type == 'image/gif'
|
||||||
{
|
{
|
||||||
original: '1280x1280>',
|
small: IMAGE_STYLES[:small],
|
||||||
small: '400x400>',
|
original: {
|
||||||
}
|
format: 'webm',
|
||||||
else
|
|
||||||
{
|
|
||||||
small: {
|
|
||||||
convert_options: {
|
convert_options: {
|
||||||
output: {
|
output: {
|
||||||
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
|
'c:v' => 'libvpx',
|
||||||
|
'crf' => 6,
|
||||||
|
'b:v' => '500K',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
format: 'png',
|
|
||||||
time: 1,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
|
||||||
|
IMAGE_STYLES
|
||||||
|
else
|
||||||
|
VIDEO_STYLES
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_processors(f)
|
||||||
|
if f.file_content_type == 'image/gif'
|
||||||
|
[:gif_transcoder]
|
||||||
|
elsif VIDEO_MIME_TYPES.include? f.file_content_type
|
||||||
|
[:transcoder]
|
||||||
|
else
|
||||||
|
[:thumbnail]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -80,4 +97,8 @@ class MediaAttachment < ApplicationRecord
|
||||||
break if MediaAttachment.find_by(shortcode: shortcode).nil?
|
break if MediaAttachment.find_by(shortcode: shortcode).nil?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_type
|
||||||
|
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
object @media
|
object @media
|
||||||
attribute :id, :type
|
attribute :id, :type
|
||||||
node(:url) { |media| full_asset_url(media.file.url( :original)) }
|
node(:url) { |media| full_asset_url(media.file.url(:original)) }
|
||||||
node(:preview_url) { |media| full_asset_url(media.file.url( :small)) }
|
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
|
||||||
node(:text_url) { |media| medium_url(media) }
|
node(:text_url) { |media| medium_url(media) }
|
||||||
|
|
|
@ -22,9 +22,9 @@
|
||||||
.detailed-status__attachments
|
.detailed-status__attachments
|
||||||
- if status.sensitive?
|
- if status.sensitive?
|
||||||
= render partial: 'stream_entries/content_spoiler'
|
= render partial: 'stream_entries/content_spoiler'
|
||||||
- status.media_attachments.each do |media|
|
.status__attachments__inner
|
||||||
.media-item
|
- status.media_attachments.each do |media|
|
||||||
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}"
|
= render partial: 'stream_entries/media', locals: { media: media }
|
||||||
|
|
||||||
%div.detailed-status__meta
|
%div.detailed-status__meta
|
||||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.media-item
|
||||||
|
= link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : "", target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do
|
||||||
|
- unless media.image?
|
||||||
|
%video{ src: media.file.url(:original), autoplay: true, loop: true }/
|
|
@ -22,11 +22,12 @@
|
||||||
- if status.sensitive?
|
- if status.sensitive?
|
||||||
= render partial: 'stream_entries/content_spoiler'
|
= render partial: 'stream_entries/content_spoiler'
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
.video-item
|
.status__attachments__inner
|
||||||
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do
|
.video-item
|
||||||
.video-item__play
|
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do
|
||||||
= fa_icon('play')
|
.video-item__play
|
||||||
|
= fa_icon('play')
|
||||||
- else
|
- else
|
||||||
- status.media_attachments.each do |media|
|
.status__attachments__inner
|
||||||
.media-item
|
- status.media_attachments.each do |media|
|
||||||
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}"
|
= render partial: 'stream_entries/media', locals: { media: media }
|
||||||
|
|
|
@ -2,12 +2,13 @@ require_relative 'boot'
|
||||||
|
|
||||||
require 'rails/all'
|
require 'rails/all'
|
||||||
|
|
||||||
require_relative '../app/lib/exceptions'
|
|
||||||
|
|
||||||
# Require the gems listed in Gemfile, including any gems
|
# Require the gems listed in Gemfile, including any gems
|
||||||
# you've limited to :test, :development, or :production.
|
# you've limited to :test, :development, or :production.
|
||||||
Bundler.require(*Rails.groups)
|
Bundler.require(*Rails.groups)
|
||||||
|
|
||||||
|
require_relative '../app/lib/exceptions'
|
||||||
|
require_relative '../lib/paperclip/gif_transcoder'
|
||||||
|
|
||||||
Dotenv::Railtie.load
|
Dotenv::Railtie.load
|
||||||
|
|
||||||
module Mastodon
|
module Mastodon
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
class AddTypeToMediaAttachments < ActiveRecord::Migration[5.0]
|
||||||
|
def up
|
||||||
|
add_column :media_attachments, :type, :integer, default: 0, null: false
|
||||||
|
|
||||||
|
MediaAttachment.where(file_content_type: MediaAttachment::IMAGE_MIME_TYPES).update_all(type: MediaAttachment.types[:image])
|
||||||
|
MediaAttachment.where(file_content_type: MediaAttachment::VIDEO_MIME_TYPES).update_all(type: MediaAttachment.types[:video])
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :media_attachments, :type
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20170303212857) do
|
ActiveRecord::Schema.define(version: 20170304202101) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -98,6 +98,7 @@ ActiveRecord::Schema.define(version: 20170303212857) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "shortcode"
|
t.string "shortcode"
|
||||||
|
t.integer "type", default: 0, null: false
|
||||||
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree
|
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree
|
||||||
t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree
|
t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Paperclip
|
||||||
|
# This transcoder is only to be used for the MediaAttachment model
|
||||||
|
# to convert animated gifs to webm
|
||||||
|
class GifTranscoder < Paperclip::Processor
|
||||||
|
def make
|
||||||
|
num_frames = identify('-format %n :file', file: file.path).to_i
|
||||||
|
|
||||||
|
return file unless options[:style] == :original && num_frames > 1
|
||||||
|
|
||||||
|
final_file = Paperclip::Transcoder.make(file, options, attachment)
|
||||||
|
|
||||||
|
attachment.instance.file_file_name = 'media.webm'
|
||||||
|
attachment.instance.file_content_type = 'video/webm'
|
||||||
|
attachment.instance.type = MediaAttachment.types[:gifv]
|
||||||
|
|
||||||
|
final_file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue