export function fetchAccount(id) {
return (dispatch, getState) => {
+ dispatch(fetchRelationships([id]));
+
+ if (getState().getIn(['accounts', id], null) !== null) {
+ return;
+ }
+
dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(fetchAccountSuccess(response.data));
- dispatch(fetchRelationships([id]));
}).catch(error => {
dispatch(fetchAccountFail(id, error));
});
--- /dev/null
+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;
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({
getInitialState () {
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 () {
let children;
if (!this.state.visible) {
+ let warning;
+
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 {
- 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 {
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 (
<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} />
</div>
+
{children}
</div>
);
}
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 {
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
}
);
} else {
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}
<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 style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
{spoilerButton}
{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>
);
}
let applicationLink = '';
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 />;
} else {
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
import LoadingIndicator from '../../../components/loading_indicator';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ExtendedVideoPlayer from '../../../components/extended_video_player';
const mapStateToProps = state => ({
media: state.getIn(['modal', 'media']),
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) {
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>;
}
- return (
- <Lightbox {...other}>
- {leftNav}
-
+ if (attachment.get('type') === 'image') {
+ content = (
<ImageLoader
src={url}
preloader={preloader}
imgProps={{ style: imageStyle }}
/>
+ );
+ } else if (attachment.get('type') === 'gifv') {
+ content = <ExtendedVideoPlayer src={url} />;
+ }
+ return (
+ <Lightbox {...other}>
+ {leftNav}
+ {content}
{rightNav}
</Lightbox>
);
overflow: hidden;
width: 100%;
box-sizing: border-box;
- height: 110px;
- display: flex;
+ position: relative;
+
+ .status__attachments__inner {
+ display: flex;
+ height: 214px;
+ }
}
}
overflow: hidden;
width: 100%;
box-sizing: border-box;
- height: 300px;
- display: flex;
+ position: relative;
+
+ .status__attachments__inner {
+ display: flex;
+ height: 360px;
+ }
}
.video-player {
text-decoration: none;
cursor: zoom-in;
}
+
+ video {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ top: 50%;
+ transform: translateY(-50%);
+ }
}
.video-item {
- max-width: 196px;
-
a {
cursor: pointer;
}
width: 100%;
height: 100%;
cursor: pointer;
+ position: absolute;
+ top: 0;
+ left: 0;
display: flex;
align-items: center;
justify-content: center;
# frozen_string_literal: true
class MediaAttachment < ApplicationRecord
+ self.inheritance_column = nil
+
+ enum type: [:image, :gifv, :video]
+
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].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 :status, inverse_of: :media_attachments
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' }
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
validates_attachment_size :file, less_than: 8.megabytes
self.file = URI.parse(url)
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
shortcode
end
before_create :set_shortcode
+ before_post_process :set_type
class << self
private
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: {
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
break if MediaAttachment.find_by(shortcode: shortcode).nil?
end
end
+
+ def set_type
+ self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
+ end
end
object @media
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) }
.detailed-status__attachments
- if status.sensitive?
= 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
%data.dt-published{ value: status.created_at.to_time.iso8601 }
--- /dev/null
+.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 }/
- if status.sensitive?
= render partial: 'stream_entries/content_spoiler'
- 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
- - 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 }
require 'rails/all'
-require_relative '../app/lib/exceptions'
-
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
+require_relative '../app/lib/exceptions'
+require_relative '../lib/paperclip/gif_transcoder'
+
Dotenv::Railtie.load
module Mastodon
--- /dev/null
+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
#
# 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
enable_extension "plpgsql"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
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 ["status_id"], name: "index_media_attachments_on_status_id", using: :btree
end
--- /dev/null
+# 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