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.
This commit is contained in:
		@@ -75,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
 | 
			
		||||
 | 
			
		||||
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));
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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 () {
 | 
			
		||||
@@ -61,17 +196,12 @@ const MediaGallery = React.createClass({
 | 
			
		||||
 | 
			
		||||
  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 () {
 | 
			
		||||
@@ -80,87 +210,31 @@ const MediaGallery = React.createClass({
 | 
			
		||||
    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>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -74,8 +74,8 @@ const Status = React.createClass({
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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} />;
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -175,7 +175,7 @@ const VideoPlayer = React.createClass({
 | 
			
		||||
        );
 | 
			
		||||
      } 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>
 | 
			
		||||
@@ -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' }}>
 | 
			
		||||
        {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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({
 | 
			
		||||
    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} />;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import ImageLoader from 'react-imageloader';
 | 
			
		||||
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']),
 | 
			
		||||
@@ -131,27 +132,34 @@ const Modal = React.createClass({
 | 
			
		||||
      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>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,12 @@
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      height: 110px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      position: relative;
 | 
			
		||||
 | 
			
		||||
      .status__attachments__inner {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        height: 214px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -184,8 +188,12 @@
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      height: 300px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      position: relative;
 | 
			
		||||
 | 
			
		||||
      .status__attachments__inner {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        height: 360px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .video-player {
 | 
			
		||||
@@ -231,11 +239,19 @@
 | 
			
		||||
      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;
 | 
			
		||||
    }
 | 
			
		||||
@@ -258,6 +274,9 @@
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,32 @@
 | 
			
		||||
# 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
 | 
			
		||||
@@ -27,45 +44,45 @@ class MediaAttachment < ApplicationRecord
 | 
			
		||||
    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
 | 
			
		||||
@@ -80,4 +97,8 @@ class MediaAttachment < ApplicationRecord
 | 
			
		||||
      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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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) }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,9 +22,9 @@
 | 
			
		||||
      .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 }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								app/views/stream_entries/_media.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/views/stream_entries/_media.html.haml
									
									
									
									
									
										Normal 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 }/
 | 
			
		||||
@@ -22,11 +22,12 @@
 | 
			
		||||
      - 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 }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,13 @@ require_relative 'boot'
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								db/migrate/20170304202101_add_type_to_media_attachments.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20170304202101_add_type_to_media_attachments.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
#
 | 
			
		||||
# 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"
 | 
			
		||||
@@ -98,6 +98,7 @@ ActiveRecord::Schema.define(version: 20170303212857) do
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								lib/paperclip/gif_transcoder.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								lib/paperclip/gif_transcoder.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
		Reference in New Issue
	
	Block a user