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
@@ -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) { | |||||
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> | ||||
); | ); | ||||
@@ -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} />; | ||||
} | } | ||||
@@ -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 ( | |||||
<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> | ||||
); | ); | ||||
@@ -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; | ||||
@@ -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 |
@@ -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) } |
@@ -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 } | ||||
@@ -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 | |||||
= 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 } |
@@ -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 |