Importing vanillin from upstream ;) ;)
This commit is contained in:
		
							
								
								
									
										230
									
								
								app/javascript/mastodon/components/media_gallery.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								app/javascript/mastodon/components/media_gallery.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import IconButton from './icon_button';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { isIOS } from '../is_mobile';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class Item extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    attachment: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    index: PropTypes.number.isRequired,
 | 
			
		||||
    size: PropTypes.number.isRequired,
 | 
			
		||||
    onClick: PropTypes.func.isRequired,
 | 
			
		||||
    autoPlayGif: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
    autoPlayGif: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleMouseEnter = (e) => {
 | 
			
		||||
    if (this.hoverToPlay()) {
 | 
			
		||||
      e.target.play();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMouseLeave = (e) => {
 | 
			
		||||
    if (this.hoverToPlay()) {
 | 
			
		||||
      e.target.pause();
 | 
			
		||||
      e.target.currentTime = 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hoverToPlay () {
 | 
			
		||||
    const { attachment, autoPlayGif } = this.props;
 | 
			
		||||
    return !autoPlayGif && attachment.get('type') === 'gifv';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = (e) => {
 | 
			
		||||
    const { index, onClick } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (this.context.router && 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') {
 | 
			
		||||
      const previewUrl = attachment.get('preview_url');
 | 
			
		||||
      const previewWidth = attachment.getIn(['meta', 'small', 'width']);
 | 
			
		||||
 | 
			
		||||
      const originalUrl = attachment.get('url');
 | 
			
		||||
      const originalWidth = attachment.getIn(['meta', 'original', 'width']);
 | 
			
		||||
 | 
			
		||||
      const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
 | 
			
		||||
 | 
			
		||||
      const srcSet = hasSize && `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
 | 
			
		||||
      const sizes = hasSize && `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
 | 
			
		||||
 | 
			
		||||
      thumbnail = (
 | 
			
		||||
        <a
 | 
			
		||||
          className='media-gallery__item-thumbnail'
 | 
			
		||||
          href={attachment.get('remote_url') || originalUrl}
 | 
			
		||||
          onClick={this.handleClick}
 | 
			
		||||
          target='_blank'
 | 
			
		||||
        >
 | 
			
		||||
          <img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' />
 | 
			
		||||
        </a>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (attachment.get('type') === 'gifv') {
 | 
			
		||||
      const autoPlay = !isIOS() && this.props.autoPlayGif;
 | 
			
		||||
 | 
			
		||||
      thumbnail = (
 | 
			
		||||
        <div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
 | 
			
		||||
          <video
 | 
			
		||||
            className='media-gallery__item-gifv-thumbnail'
 | 
			
		||||
            role='application'
 | 
			
		||||
            src={attachment.get('url')}
 | 
			
		||||
            onClick={this.handleClick}
 | 
			
		||||
            onMouseEnter={this.handleMouseEnter}
 | 
			
		||||
            onMouseLeave={this.handleMouseLeave}
 | 
			
		||||
            autoPlay={autoPlay}
 | 
			
		||||
            loop
 | 
			
		||||
            muted
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <span className='media-gallery__gifv__label'>GIF</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
 | 
			
		||||
        {thumbnail}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@injectIntl
 | 
			
		||||
export default class MediaGallery extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    sensitive: PropTypes.bool,
 | 
			
		||||
    media: ImmutablePropTypes.list.isRequired,
 | 
			
		||||
    height: PropTypes.number.isRequired,
 | 
			
		||||
    onOpenMedia: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    autoPlayGif: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
    autoPlayGif: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    visible: !this.props.sensitive,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleOpen = () => {
 | 
			
		||||
    this.setState({ visible: !this.state.visible });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = (index) => {
 | 
			
		||||
    this.props.onOpenMedia(this.props.media, index);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { media, intl, sensitive } = this.props;
 | 
			
		||||
 | 
			
		||||
    let children;
 | 
			
		||||
 | 
			
		||||
    if (!this.state.visible) {
 | 
			
		||||
      let warning;
 | 
			
		||||
 | 
			
		||||
      if (sensitive) {
 | 
			
		||||
        warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
 | 
			
		||||
      } else {
 | 
			
		||||
        warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      children = (
 | 
			
		||||
        <div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}>
 | 
			
		||||
          <span className='media-spoiler__warning'>{warning}</span>
 | 
			
		||||
          <span className='media-spoiler__trigger'><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) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='media-gallery' style={{ height: `${this.props.height}px` }}>
 | 
			
		||||
        <div className={`spoiler-button ${this.state.visible ? 'spoiler-button--visible' : ''}`}>
 | 
			
		||||
          <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {children}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										261
									
								
								app/javascript/mastodon/components/status.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								app/javascript/mastodon/components/status.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,261 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import Avatar from './avatar';
 | 
			
		||||
import AvatarOverlay from './avatar_overlay';
 | 
			
		||||
import RelativeTimestamp from './relative_timestamp';
 | 
			
		||||
import DisplayName from './display_name';
 | 
			
		||||
import StatusContent from './status_content';
 | 
			
		||||
import StatusActionBar from './status_action_bar';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import emojify from '../emoji';
 | 
			
		||||
import escapeTextContentForBrowser from 'escape-html';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
 | 
			
		||||
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
 | 
			
		||||
 | 
			
		||||
// We use the component (and not the container) since we do not want
 | 
			
		||||
// to use the progress bar to show download progress
 | 
			
		||||
import Bundle from '../features/ui/components/bundle';
 | 
			
		||||
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
 | 
			
		||||
 | 
			
		||||
export default class Status extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    status: ImmutablePropTypes.map,
 | 
			
		||||
    account: ImmutablePropTypes.map,
 | 
			
		||||
    wrapped: PropTypes.bool,
 | 
			
		||||
    onReply: PropTypes.func,
 | 
			
		||||
    onFavourite: PropTypes.func,
 | 
			
		||||
    onReblog: PropTypes.func,
 | 
			
		||||
    onDelete: PropTypes.func,
 | 
			
		||||
    onOpenMedia: PropTypes.func,
 | 
			
		||||
    onOpenVideo: PropTypes.func,
 | 
			
		||||
    onBlock: PropTypes.func,
 | 
			
		||||
    me: PropTypes.number,
 | 
			
		||||
    boostModal: PropTypes.bool,
 | 
			
		||||
    autoPlayGif: PropTypes.bool,
 | 
			
		||||
    muted: PropTypes.bool,
 | 
			
		||||
    intersectionObserverWrapper: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    isExpanded: false,
 | 
			
		||||
    isIntersecting: true, // assume intersecting until told otherwise
 | 
			
		||||
    isHidden: false, // set to true in requestIdleCallback to trigger un-render
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Avoid checking props that are functions (and whose equality will always
 | 
			
		||||
  // evaluate to false. See react-immutable-pure-component for usage.
 | 
			
		||||
  updateOnProps = [
 | 
			
		||||
    'status',
 | 
			
		||||
    'account',
 | 
			
		||||
    'wrapped',
 | 
			
		||||
    'me',
 | 
			
		||||
    'boostModal',
 | 
			
		||||
    'autoPlayGif',
 | 
			
		||||
    'muted',
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  updateOnStates = ['isExpanded']
 | 
			
		||||
 | 
			
		||||
  shouldComponentUpdate (nextProps, nextState) {
 | 
			
		||||
    if (!nextState.isIntersecting && nextState.isHidden) {
 | 
			
		||||
      // It's only if we're not intersecting (i.e. offscreen) and isHidden is true
 | 
			
		||||
      // that either "isIntersecting" or "isHidden" matter, and then they're
 | 
			
		||||
      // the only things that matter.
 | 
			
		||||
      return this.state.isIntersecting || !this.state.isHidden;
 | 
			
		||||
    } else if (nextState.isIntersecting && !this.state.isIntersecting) {
 | 
			
		||||
      // If we're going from a non-intersecting state to an intersecting state,
 | 
			
		||||
      // (i.e. offscreen to onscreen), then we definitely need to re-render
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    // Otherwise, diff based on "updateOnProps" and "updateOnStates"
 | 
			
		||||
    return super.shouldComponentUpdate(nextProps, nextState);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    if (!this.props.intersectionObserverWrapper) {
 | 
			
		||||
      // TODO: enable IntersectionObserver optimization for notification statuses.
 | 
			
		||||
      // These are managed in notifications/index.js rather than status_list.js
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.props.intersectionObserverWrapper.observe(
 | 
			
		||||
      this.props.id,
 | 
			
		||||
      this.node,
 | 
			
		||||
      this.handleIntersection
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.componentMounted = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    if (this.props.intersectionObserverWrapper) {
 | 
			
		||||
      this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.componentMounted = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleIntersection = (entry) => {
 | 
			
		||||
    if (this.node && this.node.children.length !== 0) {
 | 
			
		||||
      // save the height of the fully-rendered element
 | 
			
		||||
      this.height = getRectFromEntry(entry).height;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Edge 15 doesn't support isIntersecting, but we can infer it
 | 
			
		||||
    // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
 | 
			
		||||
    // https://github.com/WICG/IntersectionObserver/issues/211
 | 
			
		||||
    const isIntersecting = (typeof entry.isIntersecting === 'boolean') ?
 | 
			
		||||
      entry.isIntersecting : entry.intersectionRect.height > 0;
 | 
			
		||||
    this.setState((prevState) => {
 | 
			
		||||
      if (prevState.isIntersecting && !isIntersecting) {
 | 
			
		||||
        scheduleIdleTask(this.hideIfNotIntersecting);
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        isIntersecting: isIntersecting,
 | 
			
		||||
        isHidden: false,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hideIfNotIntersecting = () => {
 | 
			
		||||
    if (!this.componentMounted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // When the browser gets a chance, test if we're still not intersecting,
 | 
			
		||||
    // and if so, set our isHidden to true to trigger an unrender. The point of
 | 
			
		||||
    // this is to save DOM nodes and avoid using up too much memory.
 | 
			
		||||
    // See: https://github.com/tootsuite/mastodon/issues/2900
 | 
			
		||||
    this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleRef = (node) => {
 | 
			
		||||
    this.node = node;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = () => {
 | 
			
		||||
    if (!this.context.router) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { status } = this.props;
 | 
			
		||||
    this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleAccountClick = (e) => {
 | 
			
		||||
    if (this.context.router && e.button === 0) {
 | 
			
		||||
      const id = Number(e.currentTarget.getAttribute('data-id'));
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.context.router.history.push(`/accounts/${id}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleExpandedToggle = () => {
 | 
			
		||||
    this.setState({ isExpanded: !this.state.isExpanded });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  renderLoadingMediaGallery () {
 | 
			
		||||
    return <div className='media_gallery' style={{ height: '110px' }} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderLoadingVideoPlayer () {
 | 
			
		||||
    return <div className='media-spoiler-video' style={{ height: '110px' }} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    let media = null;
 | 
			
		||||
    let statusAvatar;
 | 
			
		||||
 | 
			
		||||
    // Exclude intersectionObserverWrapper from `other` variable
 | 
			
		||||
    // because intersection is managed in here.
 | 
			
		||||
    const { status, account, intersectionObserverWrapper, ...other } = this.props;
 | 
			
		||||
    const { isExpanded, isIntersecting, isHidden } = this.state;
 | 
			
		||||
 | 
			
		||||
    if (status === null) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!isIntersecting && isHidden) {
 | 
			
		||||
      return (
 | 
			
		||||
        <div ref={this.handleRef} data-id={status.get('id')} style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
 | 
			
		||||
          {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
 | 
			
		||||
          {status.get('content')}
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
 | 
			
		||||
      let displayName = status.getIn(['account', 'display_name']);
 | 
			
		||||
 | 
			
		||||
      if (displayName.length === 0) {
 | 
			
		||||
        displayName = status.getIn(['account', 'username']);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <div className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} >
 | 
			
		||||
          <div className='status__prepend'>
 | 
			
		||||
            <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
 | 
			
		||||
            <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (status.get('media_attachments').size > 0 && !this.props.muted) {
 | 
			
		||||
      if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
 | 
			
		||||
 | 
			
		||||
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
			
		||||
        media = (
 | 
			
		||||
          <Bundle fetchComponent={VideoPlayer} loading={this.renderLoadingVideoPlayer} >
 | 
			
		||||
            {Component => <Component media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        media = (
 | 
			
		||||
          <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
 | 
			
		||||
            {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (account === undefined || account === null) {
 | 
			
		||||
      statusAvatar = <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />;
 | 
			
		||||
    }else{
 | 
			
		||||
      statusAvatar = <AvatarOverlay staticSrc={status.getIn(['account', 'avatar_static'])} overlaySrc={account.get('avatar_static')} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
 | 
			
		||||
        <div className='status__info'>
 | 
			
		||||
          <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
 | 
			
		||||
 | 
			
		||||
          <a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
 | 
			
		||||
            <div className='status__avatar'>
 | 
			
		||||
              {statusAvatar}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <DisplayName account={status.get('account')} />
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <StatusContent status={status} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} />
 | 
			
		||||
 | 
			
		||||
        {media}
 | 
			
		||||
 | 
			
		||||
        <StatusActionBar {...this.props} />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										152
									
								
								app/javascript/mastodon/components/status_action_bar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								app/javascript/mastodon/components/status_action_bar.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,152 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import IconButton from './icon_button';
 | 
			
		||||
import DropdownMenu from './dropdown_menu';
 | 
			
		||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  delete: { id: 'status.delete', defaultMessage: 'Delete' },
 | 
			
		||||
  mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
 | 
			
		||||
  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
 | 
			
		||||
  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
 | 
			
		||||
  reply: { id: 'status.reply', defaultMessage: 'Reply' },
 | 
			
		||||
  replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
 | 
			
		||||
  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
 | 
			
		||||
  cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
 | 
			
		||||
  favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
 | 
			
		||||
  open: { id: 'status.open', defaultMessage: 'Expand this status' },
 | 
			
		||||
  report: { id: 'status.report', defaultMessage: 'Report @{name}' },
 | 
			
		||||
  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
 | 
			
		||||
  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@injectIntl
 | 
			
		||||
export default class StatusActionBar extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    status: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    onReply: PropTypes.func,
 | 
			
		||||
    onFavourite: PropTypes.func,
 | 
			
		||||
    onReblog: PropTypes.func,
 | 
			
		||||
    onDelete: PropTypes.func,
 | 
			
		||||
    onMention: PropTypes.func,
 | 
			
		||||
    onMute: PropTypes.func,
 | 
			
		||||
    onBlock: PropTypes.func,
 | 
			
		||||
    onReport: PropTypes.func,
 | 
			
		||||
    onMuteConversation: PropTypes.func,
 | 
			
		||||
    me: PropTypes.number,
 | 
			
		||||
    withDismiss: PropTypes.bool,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Avoid checking props that are functions (and whose equality will always
 | 
			
		||||
  // evaluate to false. See react-immutable-pure-component for usage.
 | 
			
		||||
  updateOnProps = [
 | 
			
		||||
    'status',
 | 
			
		||||
    'me',
 | 
			
		||||
    'withDismiss',
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  handleReplyClick = () => {
 | 
			
		||||
    this.props.onReply(this.props.status, this.context.router.history);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleFavouriteClick = () => {
 | 
			
		||||
    this.props.onFavourite(this.props.status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleReblogClick = (e) => {
 | 
			
		||||
    this.props.onReblog(this.props.status, e);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleDeleteClick = () => {
 | 
			
		||||
    this.props.onDelete(this.props.status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMentionClick = () => {
 | 
			
		||||
    this.props.onMention(this.props.status.get('account'), this.context.router.history);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMuteClick = () => {
 | 
			
		||||
    this.props.onMute(this.props.status.get('account'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleBlockClick = () => {
 | 
			
		||||
    this.props.onBlock(this.props.status.get('account'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpen = () => {
 | 
			
		||||
    this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleReport = () => {
 | 
			
		||||
    this.props.onReport(this.props.status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleConversationMuteClick = () => {
 | 
			
		||||
    this.props.onMuteConversation(this.props.status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { status, me, intl, withDismiss } = this.props;
 | 
			
		||||
    const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
 | 
			
		||||
    const mutingConversation = status.get('muted');
 | 
			
		||||
    const anonymousAccess = !me;
 | 
			
		||||
 | 
			
		||||
    let menu = [];
 | 
			
		||||
    let reblogIcon = 'retweet';
 | 
			
		||||
    let replyIcon;
 | 
			
		||||
    let replyTitle;
 | 
			
		||||
 | 
			
		||||
    menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
 | 
			
		||||
    menu.push(null);
 | 
			
		||||
 | 
			
		||||
    if (withDismiss) {
 | 
			
		||||
      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
 | 
			
		||||
      menu.push(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (status.getIn(['account', 'id']) === me) {
 | 
			
		||||
      menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
 | 
			
		||||
    } else {
 | 
			
		||||
      menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
 | 
			
		||||
      menu.push(null);
 | 
			
		||||
      menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
 | 
			
		||||
      menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
 | 
			
		||||
      menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (status.get('visibility') === 'direct') {
 | 
			
		||||
      reblogIcon = 'envelope';
 | 
			
		||||
    } else if (status.get('visibility') === 'private') {
 | 
			
		||||
      reblogIcon = 'lock';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (status.get('in_reply_to_id', null) === null) {
 | 
			
		||||
      replyIcon = 'reply';
 | 
			
		||||
      replyTitle = intl.formatMessage(messages.reply);
 | 
			
		||||
    } else {
 | 
			
		||||
      replyIcon = 'reply-all';
 | 
			
		||||
      replyTitle = intl.formatMessage(messages.replyAll);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='status__action-bar'>
 | 
			
		||||
        <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
 | 
			
		||||
        <IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
 | 
			
		||||
        <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
 | 
			
		||||
 | 
			
		||||
        <div className='status__action-bar-dropdown'>
 | 
			
		||||
          <DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										184
									
								
								app/javascript/mastodon/components/status_content.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								app/javascript/mastodon/components/status_content.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import escapeTextContentForBrowser from 'escape-html';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import emojify from '../emoji';
 | 
			
		||||
import { isRtl } from '../rtl';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import Permalink from './permalink';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
 | 
			
		||||
export default class StatusContent extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    status: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    expanded: PropTypes.bool,
 | 
			
		||||
    onExpandedToggle: PropTypes.func,
 | 
			
		||||
    onClick: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    hidden: true,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _updateStatusLinks () {
 | 
			
		||||
    const node  = this.node;
 | 
			
		||||
    const links = node.querySelectorAll('a');
 | 
			
		||||
 | 
			
		||||
    for (var i = 0; i < links.length; ++i) {
 | 
			
		||||
      let link = links[i];
 | 
			
		||||
      if (link.classList.contains('status-link')) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      link.classList.add('status-link');
 | 
			
		||||
 | 
			
		||||
      let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
 | 
			
		||||
 | 
			
		||||
      if (mention) {
 | 
			
		||||
        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
 | 
			
		||||
        link.setAttribute('title', mention.get('acct'));
 | 
			
		||||
      } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
 | 
			
		||||
        link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
 | 
			
		||||
      } else {
 | 
			
		||||
        link.setAttribute('title', link.href);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      link.setAttribute('target', '_blank');
 | 
			
		||||
      link.setAttribute('rel', 'noopener');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    this._updateStatusLinks();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate () {
 | 
			
		||||
    this._updateStatusLinks();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMentionClick = (mention, e) => {
 | 
			
		||||
    if (this.context.router && e.button === 0) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.context.router.history.push(`/accounts/${mention.get('id')}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onHashtagClick = (hashtag, e) => {
 | 
			
		||||
    hashtag = hashtag.replace(/^#/, '').toLowerCase();
 | 
			
		||||
 | 
			
		||||
    if (this.context.router && e.button === 0) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.context.router.history.push(`/timelines/tag/${hashtag}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMouseDown = (e) => {
 | 
			
		||||
    this.startXY = [e.clientX, e.clientY];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMouseUp = (e) => {
 | 
			
		||||
    if (!this.startXY) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [ startX, startY ] = this.startXY;
 | 
			
		||||
    const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
 | 
			
		||||
 | 
			
		||||
    if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
 | 
			
		||||
      this.props.onClick();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.startXY = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleSpoilerClick = (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
    if (this.props.onExpandedToggle) {
 | 
			
		||||
      // The parent manages the state
 | 
			
		||||
      this.props.onExpandedToggle();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.setState({ hidden: !this.state.hidden });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setRef = (c) => {
 | 
			
		||||
    this.node = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { status } = this.props;
 | 
			
		||||
 | 
			
		||||
    const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
 | 
			
		||||
 | 
			
		||||
    const content = { __html: emojify(status.get('content')) };
 | 
			
		||||
    const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
 | 
			
		||||
    const directionStyle = { direction: 'ltr' };
 | 
			
		||||
    const classNames = classnames('status__content', {
 | 
			
		||||
      'status__content--with-action': this.props.onClick && this.context.router,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (isRtl(status.get('search_index'))) {
 | 
			
		||||
      directionStyle.direction = 'rtl';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (status.get('spoiler_text').length > 0) {
 | 
			
		||||
      let mentionsPlaceholder = '';
 | 
			
		||||
 | 
			
		||||
      const mentionLinks = status.get('mentions').map(item => (
 | 
			
		||||
        <Permalink to={`/accounts/${item.get('id')}`} href={item.get('url')} key={item.get('id')} className='mention'>
 | 
			
		||||
          @<span>{item.get('username')}</span>
 | 
			
		||||
        </Permalink>
 | 
			
		||||
      )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
 | 
			
		||||
 | 
			
		||||
      const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
 | 
			
		||||
 | 
			
		||||
      if (hidden) {
 | 
			
		||||
        mentionsPlaceholder = <div>{mentionLinks}</div>;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
 | 
			
		||||
          <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
 | 
			
		||||
            <span dangerouslySetInnerHTML={spoilerContent} />
 | 
			
		||||
            {' '}
 | 
			
		||||
            <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</button>
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          {mentionsPlaceholder}
 | 
			
		||||
 | 
			
		||||
          <div className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (this.props.onClick) {
 | 
			
		||||
      return (
 | 
			
		||||
        <div
 | 
			
		||||
          ref={this.setRef}
 | 
			
		||||
          className={classNames}
 | 
			
		||||
          style={directionStyle}
 | 
			
		||||
          onMouseDown={this.handleMouseDown}
 | 
			
		||||
          onMouseUp={this.handleMouseUp}
 | 
			
		||||
          dangerouslySetInnerHTML={content}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return (
 | 
			
		||||
        <div
 | 
			
		||||
          ref={this.setRef}
 | 
			
		||||
          className='status__content'
 | 
			
		||||
          style={directionStyle}
 | 
			
		||||
          dangerouslySetInnerHTML={content}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										204
									
								
								app/javascript/mastodon/components/video_player.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								app/javascript/mastodon/components/video_player.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,204 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import IconButton from './icon_button';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { isIOS } from '../is_mobile';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
 | 
			
		||||
  toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
 | 
			
		||||
  expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@injectIntl
 | 
			
		||||
export default class VideoPlayer extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    media: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    width: PropTypes.number,
 | 
			
		||||
    height: PropTypes.number,
 | 
			
		||||
    sensitive: PropTypes.bool,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    autoplay: PropTypes.bool,
 | 
			
		||||
    onOpenVideo: PropTypes.func.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
    width: 239,
 | 
			
		||||
    height: 110,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    visible: !this.props.sensitive,
 | 
			
		||||
    preview: true,
 | 
			
		||||
    muted: true,
 | 
			
		||||
    hasAudio: true,
 | 
			
		||||
    videoError: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleClick = () => {
 | 
			
		||||
    this.setState({ muted: !this.state.muted });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVideoClick = (e) => {
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
    const node = this.video;
 | 
			
		||||
 | 
			
		||||
    if (node.paused) {
 | 
			
		||||
      node.play();
 | 
			
		||||
    } else {
 | 
			
		||||
      node.pause();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOpen = () => {
 | 
			
		||||
    this.setState({ preview: !this.state.preview });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVisibility = () => {
 | 
			
		||||
    this.setState({
 | 
			
		||||
      visible: !this.state.visible,
 | 
			
		||||
      preview: true,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleExpand = () => {
 | 
			
		||||
    this.video.pause();
 | 
			
		||||
    this.props.onOpenVideo(this.props.media, this.video.currentTime);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setRef = (c) => {
 | 
			
		||||
    this.video = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadedData = () => {
 | 
			
		||||
    if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
 | 
			
		||||
      this.setState({ hasAudio: false });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVideoError = () => {
 | 
			
		||||
    this.setState({ videoError: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    if (!this.video) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.video.addEventListener('loadeddata', this.handleLoadedData);
 | 
			
		||||
    this.video.addEventListener('error', this.handleVideoError);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate () {
 | 
			
		||||
    if (!this.video) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.video.addEventListener('loadeddata', this.handleLoadedData);
 | 
			
		||||
    this.video.addEventListener('error', this.handleVideoError);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    if (!this.video) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.video.removeEventListener('loadeddata', this.handleLoadedData);
 | 
			
		||||
    this.video.removeEventListener('error', this.handleVideoError);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { media, intl, width, height, sensitive, autoplay } = this.props;
 | 
			
		||||
 | 
			
		||||
    let spoilerButton = (
 | 
			
		||||
      <div className={`status__video-player-spoiler ${this.state.visible ? 'status__video-player-spoiler--visible' : ''}`}>
 | 
			
		||||
        <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let expandButton = '';
 | 
			
		||||
 | 
			
		||||
    if (this.context.router) {
 | 
			
		||||
      expandButton = (
 | 
			
		||||
        <div className='status__video-player-expand'>
 | 
			
		||||
          <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let muteButton = '';
 | 
			
		||||
 | 
			
		||||
    if (this.state.hasAudio) {
 | 
			
		||||
      muteButton = (
 | 
			
		||||
        <div className='status__video-player-mute'>
 | 
			
		||||
          <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.state.visible) {
 | 
			
		||||
      if (sensitive) {
 | 
			
		||||
        return (
 | 
			
		||||
          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
 | 
			
		||||
            {spoilerButton}
 | 
			
		||||
            <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
			
		||||
            <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return (
 | 
			
		||||
          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
 | 
			
		||||
            {spoilerButton}
 | 
			
		||||
            <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
 | 
			
		||||
            <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.state.preview && !autoplay) {
 | 
			
		||||
      return (
 | 
			
		||||
        <div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}>
 | 
			
		||||
          {spoilerButton}
 | 
			
		||||
          <div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.state.videoError) {
 | 
			
		||||
      return (
 | 
			
		||||
        <div style={{ width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
 | 
			
		||||
          <span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='status__video-player' style={{ width: `${width}px`, height: `${height}px` }}>
 | 
			
		||||
        {spoilerButton}
 | 
			
		||||
        {muteButton}
 | 
			
		||||
        {expandButton}
 | 
			
		||||
 | 
			
		||||
        <video
 | 
			
		||||
          className='status__video-player-video'
 | 
			
		||||
          role='button'
 | 
			
		||||
          tabIndex='0'
 | 
			
		||||
          ref={this.setRef}
 | 
			
		||||
          src={media.get('url')}
 | 
			
		||||
          autoPlay={!isIOS()}
 | 
			
		||||
          loop
 | 
			
		||||
          muted={this.state.muted}
 | 
			
		||||
          onClick={this.handleVideoClick}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user