Browse Source

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.
master
Eugen Rochko 7 years ago
parent
commit
caf5b8e975
17 changed files with 325 additions and 137 deletions
  1. +6
    -1
      app/assets/javascripts/components/actions/accounts.jsx
  2. +21
    -0
      app/assets/javascripts/components/components/extended_video_player.jsx
  3. +153
    -79
      app/assets/javascripts/components/components/media_gallery.jsx
  4. +2
    -2
      app/assets/javascripts/components/components/status.jsx
  5. +2
    -2
      app/assets/javascripts/components/components/video_player.jsx
  6. +1
    -1
      app/assets/javascripts/components/features/status/components/detailed_status.jsx
  7. +15
    -7
      app/assets/javascripts/components/features/ui/containers/modal_container.jsx
  8. +25
    -6
      app/assets/stylesheets/stream_entries.scss
  9. +45
    -24
      app/models/media_attachment.rb
  10. +2
    -2
      app/views/api/v1/media/create.rabl
  11. +3
    -3
      app/views/stream_entries/_detailed_status.html.haml
  12. +4
    -0
      app/views/stream_entries/_media.html.haml
  13. +8
    -7
      app/views/stream_entries/_simple_status.html.haml
  14. +3
    -2
      config/application.rb
  15. +12
    -0
      db/migrate/20170304202101_add_type_to_media_attachments.rb
  16. +2
    -1
      db/schema.rb
  17. +21
    -0
      lib/paperclip/gif_transcoder.rb

+ 6
- 1
app/assets/javascripts/components/actions/accounts.jsx View File

@@ -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));
}); });


+ 21
- 0
app/assets/javascripts/components/components/extended_video_player.jsx View File

@@ -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;

+ 153
- 79
app/assets/javascripts/components/components/media_gallery.jsx View File

@@ -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) {
if (e.button === 0) {
e.preventDefault();
this.props.onOpenMedia(this.props.media, index);
}

e.stopPropagation();
handleOpen (e) {
this.setState({ visible: !this.state.visible });
}, },


handleOpen () {
this.setState({ visible: !this.state.visible });
handleClick (index) {
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 = (
<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>
);
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else { } else {
children = (
<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>
);
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
} }

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) => {
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>
);
});
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
} }


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>
); );


+ 2
- 2
app/assets/javascripts/components/components/status.jsx View File

@@ -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') {
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />;
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])} 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} />;
} }


+ 2
- 2
app/assets/javascripts/components/components/video_player.jsx View File

@@ -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>
); );
} }


+ 1
- 1
app/assets/javascripts/components/features/status/components/detailed_status.jsx View File

@@ -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} />;


+ 15
- 7
app/assets/javascripts/components/features/ui/containers/modal_container.jsx View File

@@ -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 (
<Lightbox {...other}>
{leftNav}

if (attachment.get('type') === 'image') {
content = (
<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>
); );


+ 25
- 6
app/assets/stylesheets/stream_entries.scss View File

@@ -104,8 +104,12 @@
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
height: 110px;
display: flex;
position: relative;

.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;
display: flex;
position: relative;

.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;


+ 45
- 24
app/models/media_attachment.rb View File

@@ -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 },
processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] },
styles: ->(f) { file_styles f },
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: '400x400>',
}
else
{
small: {
small: IMAGE_STYLES[:small],
original: {
format: 'webm',
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

+ 2
- 2
app/views/api/v1/media/create.rabl View File

@@ -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(:preview_url) { |media| full_asset_url(media.file.url( :small)) }
node(:url) { |media| full_asset_url(media.file.url(:original)) }
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) }

+ 3
- 3
app/views/stream_entries/_detailed_status.html.haml View File

@@ -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|
.media-item
= 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'}"
.status__attachments__inner
- status.media_attachments.each do |media|
= 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 }


+ 4
- 0
app/views/stream_entries/_media.html.haml View File

@@ -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 }/

+ 8
- 7
app/views/stream_entries/_simple_status.html.haml View File

@@ -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
= 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__play
= fa_icon('play')
.status__attachments__inner
.video-item
= 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__play
= fa_icon('play')
- else - else
- status.media_attachments.each do |media|
.media-item
= 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'}"
.status__attachments__inner
- status.media_attachments.each do |media|
= render partial: 'stream_entries/media', locals: { media: media }

+ 3
- 2
config/application.rb View File

@@ -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


+ 12
- 0
db/migrate/20170304202101_add_type_to_media_attachments.rb View File

@@ -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

+ 2
- 1
db/schema.rb View File

@@ -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


+ 21
- 0
lib/paperclip/gif_transcoder.rb View File

@@ -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

Loading…
Cancel
Save