diff --git a/app/soapbox/features/ui/util/fullscreen.js b/app/soapbox/features/ui/util/fullscreen.ts
similarity index 57%
rename from app/soapbox/features/ui/util/fullscreen.js
rename to app/soapbox/features/ui/util/fullscreen.ts
index cf5d0cf98..5e13d68cc 100644
--- a/app/soapbox/features/ui/util/fullscreen.js
+++ b/app/soapbox/features/ui/util/fullscreen.ts
@@ -1,31 +1,43 @@
// APIs for normalizing fullscreen operations. Note that Edge uses
// the WebKit-prefixed APIs currently (as of Edge 16).
-export const isFullscreen = () => document.fullscreenElement ||
- document.webkitFullscreenElement ||
- document.mozFullScreenElement;
+export const isFullscreen = (): boolean => {
+ return Boolean(
+ document.fullscreenElement ||
+ // @ts-ignore
+ document.webkitFullscreenElement ||
+ // @ts-ignore
+ document.mozFullScreenElement,
+ );
+};
-export const exitFullscreen = () => {
+export const exitFullscreen = (): void => {
if (document.exitFullscreen) {
document.exitFullscreen();
- } else if (document.webkitExitFullscreen) {
+ } else if ('webkitExitFullscreen' in document) {
+ // @ts-ignore
document.webkitExitFullscreen();
- } else if (document.mozCancelFullScreen) {
+ } else if ('mozCancelFullScreen' in document) {
+ // @ts-ignore
document.mozCancelFullScreen();
}
};
-export const requestFullscreen = el => {
+export const requestFullscreen = (el: Element): void => {
if (el.requestFullscreen) {
el.requestFullscreen();
- } else if (el.webkitRequestFullscreen) {
+ } else if ('webkitRequestFullscreen' in el) {
+ // @ts-ignore
el.webkitRequestFullscreen();
- } else if (el.mozRequestFullScreen) {
+ } else if ('mozRequestFullScreen' in el) {
+ // @ts-ignore
el.mozRequestFullScreen();
}
};
-export const attachFullscreenListener = (listener) => {
+type FullscreenListener = (this: Document, ev: Event) => void;
+
+export const attachFullscreenListener = (listener: FullscreenListener): void => {
if ('onfullscreenchange' in document) {
document.addEventListener('fullscreenchange', listener);
} else if ('onwebkitfullscreenchange' in document) {
@@ -35,7 +47,7 @@ export const attachFullscreenListener = (listener) => {
}
};
-export const detachFullscreenListener = (listener) => {
+export const detachFullscreenListener = (listener: FullscreenListener): void => {
if ('onfullscreenchange' in document) {
document.removeEventListener('fullscreenchange', listener);
} else if ('onwebkitfullscreenchange' in document) {
diff --git a/app/soapbox/features/video/index.js b/app/soapbox/features/video/index.js
deleted file mode 100644
index 1d6b55ff6..000000000
--- a/app/soapbox/features/video/index.js
+++ /dev/null
@@ -1,625 +0,0 @@
-import classNames from 'clsx';
-import { fromJS, is } from 'immutable';
-import debounce from 'lodash/debounce';
-import throttle from 'lodash/throttle';
-import PropTypes from 'prop-types';
-import React from 'react';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-
-import { getSettings } from 'soapbox/actions/settings';
-import Blurhash from 'soapbox/components/blurhash';
-import Icon from 'soapbox/components/icon';
-import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from 'soapbox/utils/media_aspect_ratio';
-
-import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
-
-const DEFAULT_HEIGHT = 300;
-
-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' },
- hide: { id: 'video.hide', defaultMessage: 'Hide video' },
- expand: { id: 'video.expand', defaultMessage: 'Expand video' },
- close: { id: 'video.close', defaultMessage: 'Close video' },
- fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
- exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
-});
-
-export const formatTime = secondsNum => {
- let hours = Math.floor(secondsNum / 3600);
- let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
- let seconds = secondsNum - (hours * 3600) - (minutes * 60);
-
- if (hours < 10) hours = '0' + hours;
- if (minutes < 10) minutes = '0' + minutes;
- if (seconds < 10) seconds = '0' + seconds;
-
- return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
-};
-
-export const findElementPosition = el => {
- let box;
-
- if (el.getBoundingClientRect && el.parentNode) {
- box = el.getBoundingClientRect();
- }
-
- if (!box) {
- return {
- left: 0,
- top: 0,
- };
- }
-
- const docEl = document.documentElement;
- const body = document.body;
-
- const clientLeft = docEl.clientLeft || body.clientLeft || 0;
- const scrollLeft = window.pageXOffset || body.scrollLeft;
- const left = (box.left + scrollLeft) - clientLeft;
-
- const clientTop = docEl.clientTop || body.clientTop || 0;
- const scrollTop = window.pageYOffset || body.scrollTop;
- const top = (box.top + scrollTop) - clientTop;
-
- return {
- left: Math.round(left),
- top: Math.round(top),
- };
-};
-
-export const getPointerPosition = (el, event) => {
- const position = {};
- const box = findElementPosition(el);
- const boxW = el.offsetWidth;
- const boxH = el.offsetHeight;
- const boxY = box.top;
- const boxX = box.left;
-
- let pageY = event.pageY;
- let pageX = event.pageX;
-
- if (event.changedTouches) {
- pageX = event.changedTouches[0].pageX;
- pageY = event.changedTouches[0].pageY;
- }
-
- position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH));
- position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
-
- return position;
-};
-
-export const fileNameFromURL = str => {
- const url = new URL(str);
- const pathname = url.pathname;
- const index = pathname.lastIndexOf('/');
-
- return pathname.substring(index + 1);
-};
-
-const mapStateToProps = state => ({
- displayMedia: getSettings(state).get('displayMedia'),
-});
-
-export default @connect(mapStateToProps)
-@injectIntl
-class Video extends React.PureComponent {
-
- static propTypes = {
- preview: PropTypes.string,
- src: PropTypes.string.isRequired,
- alt: PropTypes.string,
- width: PropTypes.number,
- height: PropTypes.number,
- sensitive: PropTypes.bool,
- startTime: PropTypes.number,
- onOpenVideo: PropTypes.func,
- onCloseVideo: PropTypes.func,
- detailed: PropTypes.bool,
- inline: PropTypes.bool,
- cacheWidth: PropTypes.func,
- visible: PropTypes.bool,
- onToggleVisibility: PropTypes.func,
- intl: PropTypes.object.isRequired,
- blurhash: PropTypes.string,
- link: PropTypes.node,
- aspectRatio: PropTypes.number,
- displayMedia: PropTypes.string,
- };
-
- state = {
- currentTime: 0,
- duration: 0,
- volume: 0.5,
- paused: true,
- dragging: false,
- containerWidth: this.props.width,
- fullscreen: false,
- hovered: false,
- muted: false,
- revealed: this.props.visible !== undefined ? this.props.visible : (this.props.displayMedia !== 'hide_all' && !this.props.sensitive || this.props.displayMedia === 'show_all'),
- };
-
- setPlayerRef = c => {
- this.player = c;
-
- if (this.player) {
- this._setDimensions();
- }
- }
-
- _setDimensions() {
- const width = this.player.offsetWidth;
-
- if (this.props.cacheWidth) {
- this.props.cacheWidth(width);
- }
-
- this.setState({
- containerWidth: width,
- });
- }
-
- setVideoRef = c => {
- this.video = c;
-
- if (this.video) {
- this.setState({ volume: this.video.volume, muted: this.video.muted });
- }
- }
-
- setSeekRef = c => {
- this.seek = c;
- }
-
- setVolumeRef = c => {
- this.volume = c;
- }
-
- handleClickRoot = e => e.stopPropagation();
-
- handlePlay = () => {
- this.setState({ paused: false });
- }
-
- handlePause = () => {
- this.setState({ paused: true });
- }
-
- handleTimeUpdate = () => {
- this.setState({
- currentTime: Math.floor(this.video.currentTime),
- duration: Math.floor(this.video.duration),
- });
- }
-
- 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 { x } = getPointerPosition(this.volume, e);
-
- if (!isNaN(x)) {
- let slideamt = x;
-
- if (x > 1) {
- slideamt = 1;
- } else if (x < 0) {
- slideamt = 0;
- }
-
- this.video.volume = slideamt;
- this.setState({ volume: slideamt });
- }
- }, 60);
-
- handleMouseDown = e => {
- document.addEventListener('mousemove', this.handleMouseMove, true);
- document.addEventListener('mouseup', this.handleMouseUp, true);
- document.addEventListener('touchmove', this.handleMouseMove, true);
- document.addEventListener('touchend', this.handleMouseUp, true);
-
- this.setState({ dragging: true });
- this.video.pause();
- this.handleMouseMove(e);
-
- e.preventDefault();
- e.stopPropagation();
- }
-
- handleMouseUp = () => {
- document.removeEventListener('mousemove', this.handleMouseMove, true);
- document.removeEventListener('mouseup', this.handleMouseUp, true);
- document.removeEventListener('touchmove', this.handleMouseMove, true);
- document.removeEventListener('touchend', this.handleMouseUp, true);
-
- this.setState({ dragging: false });
- this.video.play();
- }
-
- handleMouseMove = throttle(e => {
- const { x } = getPointerPosition(this.seek, e);
- const currentTime = Math.floor(this.video.duration * x);
-
- if (!isNaN(currentTime)) {
- this.video.currentTime = currentTime;
- this.setState({ currentTime });
- }
- }, 60);
-
- seekBy(time) {
- const currentTime = this.video.currentTime + time;
-
- if (!isNaN(currentTime)) {
- this.setState({ currentTime }, () => {
- this.video.currentTime = currentTime;
- });
- }
- }
-
- handleVideoKeyDown = e => {
- // On the video element or the seek bar, we can safely use the space bar
- // for playback control because there are no buttons to press
-
- if (e.key === ' ') {
- e.preventDefault();
- e.stopPropagation();
- this.togglePlay();
- }
- }
-
- handleKeyDown = e => {
- const frameTime = 1 / 25;
-
- switch (e.key) {
- case 'k':
- e.preventDefault();
- e.stopPropagation();
- this.togglePlay();
- break;
- case 'm':
- e.preventDefault();
- e.stopPropagation();
- this.toggleMute();
- break;
- case 'f':
- e.preventDefault();
- e.stopPropagation();
- this.toggleFullscreen();
- break;
- case 'j':
- e.preventDefault();
- e.stopPropagation();
- this.seekBy(-10);
- break;
- case 'l':
- e.preventDefault();
- e.stopPropagation();
- this.seekBy(10);
- break;
- case ',':
- e.preventDefault();
- e.stopPropagation();
- this.seekBy(-frameTime);
- break;
- case '.':
- e.preventDefault();
- e.stopPropagation();
- this.seekBy(frameTime);
- break;
- }
-
- // If we are in fullscreen mode, we don't want any hotkeys
- // interacting with the UI that's not visible
-
- if (this.state.fullscreen) {
- e.preventDefault();
- e.stopPropagation();
-
- if (e.key === 'Escape') {
- exitFullscreen();
- }
- }
- }
-
- togglePlay = (e) => {
- if (e) {
- e.stopPropagation();
- }
-
- if (this.state.paused) {
- this.setState({ paused: false }, () => this.video.play());
- } else {
- this.setState({ paused: true }, () => this.video.pause());
- }
- }
-
- toggleFullscreen = () => {
- if (isFullscreen()) {
- exitFullscreen();
- } else {
- requestFullscreen(this.player);
- }
- }
-
- componentDidMount() {
- document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
- document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
- document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
- document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
-
- window.addEventListener('scroll', this.handleScroll);
- window.addEventListener('resize', this.handleResize, { passive: true });
- }
-
- componentWillUnmount() {
- window.removeEventListener('scroll', this.handleScroll);
- window.removeEventListener('resize', this.handleResize);
-
- document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
- document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
- document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
- document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
- }
-
- componentDidUpdate(prevProps, prevState) {
- const { visible } = this.props;
-
- if (!is(visible, prevProps.visible) && visible !== undefined) {
- this.setState({ revealed: visible });
- }
-
- if (prevState.revealed && !this.state.revealed && this.video) {
- this.video.pause();
- }
- }
-
- handleResize = debounce(() => {
- if (this.player) {
- this._setDimensions();
- }
- }, 250, {
- trailing: true,
- });
-
- handleScroll = throttle(() => {
- if (!this.video) {
- return;
- }
-
- const { top, height } = this.video.getBoundingClientRect();
- const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
-
- if (!this.state.paused && !inView) {
- this.setState({ paused: true }, () => this.video.pause());
- }
- }, 150, { trailing: true })
-
- handleFullscreenChange = () => {
- this.setState({ fullscreen: isFullscreen() });
- }
-
- handleMouseEnter = () => {
- this.setState({ hovered: true });
- }
-
- handleMouseLeave = () => {
- this.setState({ hovered: false });
- }
-
- toggleMute = () => {
- const muted = !this.video.muted;
-
- this.setState({ muted }, () => {
- this.video.muted = muted;
- });
- }
-
- toggleReveal = (e) => {
- e.stopPropagation();
-
- if (this.props.onToggleVisibility) {
- this.props.onToggleVisibility();
- } else {
- this.setState({ revealed: !this.state.revealed });
- }
- }
-
- handleLoadedData = () => {
- if (this.props.startTime) {
- this.video.currentTime = this.props.startTime;
- this.video.play();
- }
- }
-
- handleProgress = () => {
- if (this.video.buffered.length > 0) {
- this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
- }
- }
-
- handleVolumeChange = () => {
- this.setState({ volume: this.video.volume, muted: this.video.muted });
- }
-
- handleOpenVideo = () => {
- const { src, preview, width, height, alt } = this.props;
-
- const media = fromJS({
- type: 'video',
- url: src,
- preview_url: preview,
- description: alt,
- width,
- height,
- });
-
- this.video.pause();
- this.props.onOpenVideo(media, this.video.currentTime);
- }
-
- handleCloseVideo = () => {
- this.video.pause();
- this.props.onCloseVideo();
- }
-
- getPreload = () => {
- const { startTime, detailed } = this.props;
- const { dragging, fullscreen } = this.state;
-
- if (startTime || fullscreen || dragging) {
- return 'auto';
- } else if (detailed) {
- return 'metadata';
- } else {
- return 'none';
- }
- }
-
- render() {
- const { src, inline, onCloseVideo, intl, alt, detailed, sensitive, link, aspectRatio, blurhash } = this.props;
- const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
- const progress = (currentTime / duration) * 100;
- const playerStyle = {};
-
- let { width, height } = this.props;
-
- if (inline && containerWidth) {
- width = containerWidth;
- const minSize = containerWidth / (16 / 9);
-
- if (isPanoramic(aspectRatio)) {
- height = Math.max(Math.floor(containerWidth / maximumAspectRatio), minSize);
- } else if (isPortrait(aspectRatio)) {
- height = Math.max(Math.floor(containerWidth / minimumAspectRatio), minSize);
- } else {
- height = Math.floor(containerWidth / aspectRatio);
- }
-
- playerStyle.height = height || DEFAULT_HEIGHT;
- }
-
- let warning;
-
- if (sensitive) {
- warning =