import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
+import { MediaGallery, Video, Audio } from 'flavours/glitch/util/async-components';
import { HotKeys } from 'react-hotkeys';
import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
import classNames from 'classnames';
}
renderLoadingMediaGallery () {
- return <div className='media_gallery' style={{ height: '110px' }} />;
+ return <div className='media-gallery' style={{ height: '110px' }} />;
}
renderLoadingVideoPlayer () {
- return <div className='media-spoiler-video' style={{ height: '110px' }} />;
+ return <div className='video-player' style={{ height: '110px' }} />;
+ }
+
+ renderLoadingAudioPlayer () {
+ return <div className='audio-player' style={{ height: '110px' }} />;
}
render () {
media={status.get('media_attachments')}
/>
);
- } else if (['video', 'audio'].includes(attachments.getIn([0, 'type']))) {
+ } else if (attachments.getIn([0, 'type']) === 'audio') {
+ const attachment = status.getIn(['media_attachments', 0]);
+
+ media = (
+ <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
+ {Component => (
+ <Component
+ src={attachment.get('url')}
+ alt={attachment.get('description')}
+ duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
+ height={110}
+ />
+ )}
+ </Bundle>
+ );
+ mediaIcon = 'music';
+ } else if (attachments.getIn([0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
/>)}
</Bundle>
);
- mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
+ mediaIcon = 'video-camera';
} else { // Media type is 'image' or 'gifv'
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import WaveSurfer from 'wavesurfer.js';
+import { defineMessages, injectIntl } from 'react-intl';
+import { formatTime } from 'flavours/glitch/features/video';
+import Icon from 'flavours/glitch/components/icon';
+import classNames from 'classnames';
+import { throttle } from 'lodash';
+
+const messages = defineMessages({
+ play: { id: 'video.play', defaultMessage: 'Play' },
+ pause: { id: 'video.pause', defaultMessage: 'Pause' },
+ mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
+ unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
+});
+
+const arrayOf = (length, fill) => (new Array(length)).fill(fill);
+
+export default @injectIntl
+class Audio extends React.PureComponent {
+
+ static propTypes = {
+ src: PropTypes.string.isRequired,
+ alt: PropTypes.string,
+ duration: PropTypes.number,
+ height: PropTypes.number,
+ preload: PropTypes.bool,
+ editable: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ };
+
+ state = {
+ currentTime: 0,
+ duration: null,
+ paused: true,
+ muted: false,
+ volume: 0.5,
+ };
+
+ // hard coded in components.scss
+ // any way to get ::before values programatically?
+
+ volWidth = 50;
+
+ volOffset = 70;
+
+ volHandleOffset = v => {
+ const offset = v * this.volWidth + this.volOffset;
+ return (offset > 110) ? 110 : offset;
+ }
+
+ setVolumeRef = c => {
+ this.volume = c;
+ }
+
+ setWaveformRef = c => {
+ this.waveform = c;
+ }
+
+ componentDidMount () {
+ if (this.waveform) {
+ this._updateWaveform();
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ if (this.waveform && prevProps.src !== this.props.src) {
+ this._updateWaveform();
+ }
+ }
+
+ componentWillUnmount () {
+ if (this.wavesurfer) {
+ this.wavesurfer.destroy();
+ this.wavesurfer = null;
+ }
+ }
+
+ _updateWaveform () {
+ const { src, height, duration, preload } = this.props;
+
+ const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color');
+ const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color');
+
+ if (this.wavesurfer) {
+ this.wavesurfer.destroy();
+ }
+
+ const wavesurfer = WaveSurfer.create({
+ container: this.waveform,
+ height,
+ barWidth: 3,
+ cursorWidth: 0,
+ progressColor,
+ waveColor,
+ forceDecode: true,
+ });
+
+ wavesurfer.setVolume(this.state.volume);
+
+ if (preload) {
+ wavesurfer.load(src);
+ } else {
+ wavesurfer.load(src, arrayOf(1, 0.5), null, duration);
+ }
+
+ wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) }));
+ wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) }));
+ wavesurfer.on('pause', () => this.setState({ paused: true }));
+ wavesurfer.on('play', () => this.setState({ paused: false }));
+ wavesurfer.on('volume', volume => this.setState({ volume }));
+ wavesurfer.on('mute', muted => this.setState({ muted }));
+
+ this.wavesurfer = wavesurfer;
+ }
+
+ togglePlay = () => {
+ if (this.state.paused) {
+ if (!this.props.preload) {
+ this.wavesurfer.createBackend();
+ this.wavesurfer.createPeakCache();
+ this.wavesurfer.load(this.props.src);
+ }
+
+ this.wavesurfer.play();
+ } else {
+ this.wavesurfer.pause();
+ }
+ }
+
+ toggleMute = () => {
+ this.wavesurfer.setMute(!this.state.muted);
+ }
+
+ handleVolumeMouseDown = e => {
+ document.addEventListener('mousemove', this.handleMouseVolSlide, true);
+ document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
+ document.addEventListener('touchmove', this.handleMouseVolSlide, true);
+ document.addEventListener('touchend', this.handleVolumeMouseUp, true);
+
+ this.handleMouseVolSlide(e);
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ handleVolumeMouseUp = () => {
+ document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
+ document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
+ document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
+ document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
+ }
+
+ handleMouseVolSlide = throttle(e => {
+ const rect = this.volume.getBoundingClientRect();
+ const x = (e.clientX - rect.left) / this.volWidth; // x position within the element.
+
+ if(!isNaN(x)) {
+ let slideamt = x;
+
+ if (x > 1) {
+ slideamt = 1;
+ } else if(x < 0) {
+ slideamt = 0;
+ }
+
+ this.wavesurfer.setVolume(slideamt);
+ }
+ }, 60);
+
+ render () {
+ const { height, intl, alt, editable } = this.props;
+ const { paused, muted, volume, currentTime } = this.state;
+
+ const volumeWidth = muted ? 0 : volume * this.volWidth;
+ const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
+
+ return (
+ <div className={classNames('audio-player', { editable })}>
+ <div className='audio-player__progress-placeholder' style={{ display: 'none' }} />
+ <div className='audio-player__wave-placeholder' style={{ display: 'none' }} />
+
+ <div
+ className='audio-player__waveform'
+ aria-label={alt}
+ title={alt}
+ style={{ height }}
+ ref={this.setWaveformRef}
+ />
+
+ <div className='video-player__controls active'>
+ <div className='video-player__buttons-bar'>
+ <div className='video-player__buttons left'>
+ <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon icon={paused ? 'play' : 'pause'} fixedWidth /></button>
+ <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon icon={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+
+ <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
+ <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
+
+ <span
+ className={classNames('video-player__volume__handle')}
+ tabIndex='0'
+ style={{ left: `${volumeHandleLoc}px` }}
+ />
+ </div>
+
+ <span>
+ <span className='video-player__time-current'>{formatTime(currentTime)}</span>
+ <span className='video-player__time-sep'>/</span>
+ <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from 'flavours/glitch/features/video';
+import Audio from 'flavours/glitch/features/audio';
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
import classNames from 'classnames';
} else if (status.get('media_attachments').size > 0) {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
media = <AttachmentList media={status.get('media_attachments')} />;
- } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
+ } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
+ const attachment = status.getIn(['media_attachments', 0]);
+
+ media = (
+ <Audio
+ src={attachment.get('url')}
+ alt={attachment.get('description')}
+ duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
+ height={150}
+ preload
+ />
+ );
+ mediaIcon = 'music';
+ } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Video
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
- mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music';
+ mediaIcon = 'video-camera';
} else {
media = (
<MediaGallery