[Glitch] Add pop-out player for audio/video in web UI
port d88a79b456 to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
			
			
This commit is contained in:
		
				
					committed by
					
						
						Thibaut Girka
					
				
			
			
				
	
			
			
			
						parent
						
							9c88792f0a
						
					
				
				
					commit
					8f950e540b
				
			@@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
import { reduceMotion } from 'flavours/glitch/util/initial_state';
 | 
			
		||||
 | 
			
		||||
const obfuscatedCount = count => {
 | 
			
		||||
  if (count < 0) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  } else if (count <= 1) {
 | 
			
		||||
    return count;
 | 
			
		||||
  } else {
 | 
			
		||||
    return '1+';
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default class AnimatedNumber extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    value: PropTypes.number.isRequired,
 | 
			
		||||
    obfuscate: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
@@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { value } = this.props;
 | 
			
		||||
    const { value, obfuscate } = this.props;
 | 
			
		||||
    const { direction } = this.state;
 | 
			
		||||
 | 
			
		||||
    if (reduceMotion) {
 | 
			
		||||
      return <FormattedNumber value={value} />;
 | 
			
		||||
      return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const styles = [{
 | 
			
		||||
@@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
 | 
			
		||||
        {items => (
 | 
			
		||||
          <span className='animated-number'>
 | 
			
		||||
            {items.map(({ key, data, style }) => (
 | 
			
		||||
              <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span>
 | 
			
		||||
              <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
 | 
			
		||||
            ))}
 | 
			
		||||
          </span>
 | 
			
		||||
        )}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import spring from 'react-motion/lib/spring';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import Icon from 'flavours/glitch/components/icon';
 | 
			
		||||
import AnimatedNumber from 'flavours/glitch/components/animated_number';
 | 
			
		||||
 | 
			
		||||
export default class IconButton extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
@@ -27,6 +28,8 @@ export default class IconButton extends React.PureComponent {
 | 
			
		||||
    overlay: PropTypes.bool,
 | 
			
		||||
    tabIndex: PropTypes.string,
 | 
			
		||||
    label: PropTypes.string,
 | 
			
		||||
    counter: PropTypes.number,
 | 
			
		||||
    obfuscateCount: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
@@ -104,6 +107,8 @@ export default class IconButton extends React.PureComponent {
 | 
			
		||||
      pressed,
 | 
			
		||||
      tabIndex,
 | 
			
		||||
      title,
 | 
			
		||||
      counter,
 | 
			
		||||
      obfuscateCount,
 | 
			
		||||
    } = this.props;
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
@@ -120,6 +125,10 @@ export default class IconButton extends React.PureComponent {
 | 
			
		||||
      overlayed: overlay,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (typeof counter !== 'undefined') {
 | 
			
		||||
      style.width = 'auto';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button
 | 
			
		||||
        aria-label={title}
 | 
			
		||||
@@ -135,7 +144,7 @@ export default class IconButton extends React.PureComponent {
 | 
			
		||||
        tabIndex={tabIndex}
 | 
			
		||||
        disabled={disabled}
 | 
			
		||||
      >
 | 
			
		||||
        <Icon id={icon} fixedWidth aria-hidden='true' />
 | 
			
		||||
        <Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
 | 
			
		||||
        {this.props.label}
 | 
			
		||||
      </button>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import Icon from 'flavours/glitch/components/icon';
 | 
			
		||||
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
export default @connect()
 | 
			
		||||
class PictureInPicturePlaceholder extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    width: PropTypes.number,
 | 
			
		||||
    dispatch: PropTypes.func.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    width: this.props.width,
 | 
			
		||||
    height: this.props.width && (this.props.width / (16/9)),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleClick = () => {
 | 
			
		||||
    const { dispatch } = this.props;
 | 
			
		||||
    dispatch(removePictureInPicture());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setRef = c => {
 | 
			
		||||
    this.node = c;
 | 
			
		||||
 | 
			
		||||
    if (this.node) {
 | 
			
		||||
      this._setDimensions();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _setDimensions () {
 | 
			
		||||
    const width  = this.node.offsetWidth;
 | 
			
		||||
    const height = width / (16/9);
 | 
			
		||||
 | 
			
		||||
    this.setState({ width, height });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    window.removeEventListener('resize', this.handleResize);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleResize = debounce(() => {
 | 
			
		||||
    if (this.node) {
 | 
			
		||||
      this._setDimensions();
 | 
			
		||||
    }
 | 
			
		||||
  }, 250, {
 | 
			
		||||
    trailing: true,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { height } = this.state;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex='0' onClick={this.handleClick}>
 | 
			
		||||
        <Icon id='window-restore' />
 | 
			
		||||
        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,7 @@ import classNames from 'classnames';
 | 
			
		||||
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
 | 
			
		||||
import PollContainer from 'flavours/glitch/containers/poll_container';
 | 
			
		||||
import { displayMedia } from 'flavours/glitch/util/initial_state';
 | 
			
		||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
 | 
			
		||||
 | 
			
		||||
// We use the component (and not the container) since we do not want
 | 
			
		||||
// to use the progress bar to show download progress
 | 
			
		||||
@@ -97,6 +98,8 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    cachedMediaWidth: PropTypes.number,
 | 
			
		||||
    onClick: PropTypes.func,
 | 
			
		||||
    scrollKey: PropTypes.string,
 | 
			
		||||
    deployPictureInPicture: PropTypes.func,
 | 
			
		||||
    usingPiP: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
@@ -123,6 +126,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    'hidden',
 | 
			
		||||
    'expanded',
 | 
			
		||||
    'unread',
 | 
			
		||||
    'usingPiP',
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  updateOnStates = [
 | 
			
		||||
@@ -394,6 +398,12 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleDeployPictureInPicture = (type, mediaProps) => {
 | 
			
		||||
    const { deployPictureInPicture, status } = this.props;
 | 
			
		||||
 | 
			
		||||
    deployPictureInPicture(status, type, mediaProps);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleHotkeyReply = e => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    this.props.onReply(this.props.status, this.context.router.history);
 | 
			
		||||
@@ -496,6 +506,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
      hidden,
 | 
			
		||||
      unread,
 | 
			
		||||
      featured,
 | 
			
		||||
      usingPiP,
 | 
			
		||||
      ...other
 | 
			
		||||
    } = this.props;
 | 
			
		||||
    const { isExpanded, isCollapsed, forceFilter } = this.state;
 | 
			
		||||
@@ -576,6 +587,9 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    if (status.get('poll')) {
 | 
			
		||||
      media = <PollContainer pollId={status.get('poll')} />;
 | 
			
		||||
      mediaIcon = 'tasks';
 | 
			
		||||
    } else if (usingPiP) {
 | 
			
		||||
      media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
 | 
			
		||||
      mediaIcon = 'video-camera';
 | 
			
		||||
    } else if (attachments.size > 0) {
 | 
			
		||||
      if (muted || attachments.some(item => item.get('type') === 'unknown')) {
 | 
			
		||||
        media = (
 | 
			
		||||
@@ -601,6 +615,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
                width={this.props.cachedMediaWidth}
 | 
			
		||||
                height={110}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
                deployPictureInPicture={this.handleDeployPictureInPicture}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
@@ -624,6 +639,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
              onOpenVideo={this.handleOpenVideo}
 | 
			
		||||
              width={this.props.cachedMediaWidth}
 | 
			
		||||
              cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
              deployPictureInPicture={this.handleDeployPictureInPicture}
 | 
			
		||||
              visible={this.state.showMedia}
 | 
			
		||||
              onToggleVisibility={this.handleToggleMediaVisibility}
 | 
			
		||||
            />)}
 | 
			
		||||
 
 | 
			
		||||
@@ -40,16 +40,6 @@ const messages = defineMessages({
 | 
			
		||||
  hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const obfuscatedCount = count => {
 | 
			
		||||
  if (count < 0) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  } else if (count <= 1) {
 | 
			
		||||
    return count;
 | 
			
		||||
  } else {
 | 
			
		||||
    return '1+';
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default @injectIntl
 | 
			
		||||
class StatusActionBar extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
@@ -284,10 +274,14 @@ class StatusActionBar extends ImmutablePureComponent {
 | 
			
		||||
    );
 | 
			
		||||
    if (showReplyCount) {
 | 
			
		||||
      replyButton = (
 | 
			
		||||
        <div className='status__action-bar__counter'>
 | 
			
		||||
          {replyButton}
 | 
			
		||||
          <span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <IconButton
 | 
			
		||||
          className='status__action-bar-button'
 | 
			
		||||
          title={replyTitle}
 | 
			
		||||
          icon={replyIcon}
 | 
			
		||||
          onClick={this.handleReplyClick}
 | 
			
		||||
          counter={status.get('replies_count')}
 | 
			
		||||
          obfuscateCount
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user