Webcam draggable refactoring

This commit is contained in:
Vitor Mateus 2019-07-09 20:11:48 -03:00
parent f19ba4dda0
commit 68ec6411aa
10 changed files with 466 additions and 542 deletions

View File

@ -266,6 +266,9 @@ class App extends Component {
className={styles.media} className={styles.media}
aria-label={intl.formatMessage(intlMessages.mediaLabel)} aria-label={intl.formatMessage(intlMessages.mediaLabel)}
aria-hidden={this.shouldAriaHide()} aria-hidden={this.shouldAriaHide()}
style={{
overflow: 'hidden',
}}
> >
{media} {media}
{this.renderCaptions()} {this.renderCaptions()}

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import cx from 'classnames'; import cx from 'classnames';
import WebcamDraggableOverlay from './webcam-draggable-overlay/component'; import WebcamDraggable from './webcam-draggable-overlay/component';
import { styles } from './styles'; import { styles } from './styles';
@ -70,6 +70,9 @@ export default class Media extends Component {
id="container" id="container"
className={cx(styles.container)} className={cx(styles.container)}
ref={this.refContainer} ref={this.refContainer}
style={{
overflow: 'hidden',
}}
> >
<div <div
className={!swapLayout ? contentClassName : overlayClassName} className={!swapLayout ? contentClassName : overlayClassName}
@ -79,10 +82,11 @@ export default class Media extends Component {
> >
{children} {children}
</div> </div>
<WebcamDraggableOverlay <WebcamDraggable
refMediaContainer={this.refContainer} refMediaContainer={this.refContainer}
swapLayout={swapLayout} swapLayout={swapLayout}
floatingOverlay={floatingOverlay} singleWebcam={floatingOverlay}
usersVideoLenght={usersVideo.length}
hideOverlay={hideOverlay} hideOverlay={hideOverlay}
disableVideo={disableVideo} disableVideo={disableVideo}
audioModalIsOpen={audioModalIsOpen} audioModalIsOpen={audioModalIsOpen}

View File

@ -1,6 +1,14 @@
@import "../../stylesheets/variables/_all"; @import "../../stylesheets/variables/_all";
@import "../../stylesheets/variables/video"; @import "../../stylesheets/variables/video";
.cursorGrab{
cursor: grab;
}
.cursorGrabbing{
cursor: grabbing;
}
.container { .container {
order: 1; order: 1;
flex: 2; flex: 2;
@ -22,30 +30,21 @@
order: 2; order: 2;
} }
.overlay { %overlay {
display: flex; display: flex;
border: 0; border: 0;
margin-top: 10px;
margin-bottom: 10px;
z-index: 2; z-index: 2;
align-items: center; align-items: center;
max-height: var(--video-height); max-height: var(--video-height);
min-height: var(--video-height); min-height: var(--video-height);
overflow: hidden; overflow: hidden;
}
.overlayRelative{
position: relative; position: relative;
width: 100%; margin-top: 10px;
margin-bottom: 10px;
} }
.overlayAbsoluteSingle{ .overlay {
width: fit-content; @extend %overlay;
}
.overlayAbsoluteMult{
width: 100%;
max-width: 100%;
} }
.overlayToTop { .overlayToTop {
@ -61,27 +60,38 @@
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
height: 1px; width: 1px; width: 1px;
margin: -1px; padding: 0; border: 0; height: 1px;
margin: -1px;
padding: 0;
border: 0;
} }
.floatingOverlay { .floatingOverlay {
margin-top: 10px; @extend %overlay;
margin-bottom: 10px; top: 0;
left: 0;
@include mq($medium-up) { @include mq($medium-up) {
z-index: 999; z-index: 999;
position: absolute; position: absolute;
bottom: 0;
right: 0;
width: fit-content;
min-width: calc(var(--video-height) * var(--video-ratio)); min-width: calc(var(--video-height) * var(--video-ratio));
max-width: fit-content;
height: fit-content;
min-height: var(--video-height); min-height: var(--video-height);
} }
} }
.fit {
width: fit-content;
height: fit-content;
max-width: fit-content;
}
.full {
min-width: 100%;
width: 100%;
max-width: 100%;
}
.hide { .hide {
display: none; display: none;
} }
@ -90,14 +100,6 @@
display: block; display: block;
} }
.top {
top: 0;
}
.bottom {
bottom: 0;
}
.dragging { .dragging {
opacity: .5; opacity: .5;
} }
@ -110,17 +112,24 @@
z-index: 9999; z-index: 9999;
} }
.dropZoneTop { .dropZoneBgTop,
.dropZoneBgBottom {
z-index: 99;
width: 100%;
height: 100%;
}
.dropZoneTop,
.dropZoneBgTop {
top: 0; top: 0;
} }
.dropZoneBottom { .dropZoneBottom,
.dropZoneBgBottom {
bottom: 0; bottom: 0;
} }
.dropZoneBg { .dropZoneTop:hover .dropZoneBgTop,
position: absolute; .dropZoneBottom:hover .dropZoneBgBottom {
z-index: 99;
width: 100%;
background-color: rgba(255, 255, 255, .3); background-color: rgba(255, 255, 255, .3);
} }

View File

@ -1,488 +1,212 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import VideoProviderContainer from '/imports/ui/components/video-provider/container';
import _ from 'lodash';
import browser from 'browser-detect';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import cx from 'classnames';
import * as _ from 'lodash';
import browser from 'browser-detect';
import { withDraggableContext } from './context';
import VideoProviderContainer from '/imports/ui/components/video-provider/container';
import { styles } from '../styles.scss'; import { styles } from '../styles.scss';
import Storage from '../../../services/storage/session';
const propTypes = { const { webcamsDefaultPlacement } = Meteor.settings.public.layout;
floatingOverlay: PropTypes.bool,
hideOverlay: PropTypes.bool,
};
const defaultProps = {
floatingOverlay: false,
hideOverlay: true,
};
const fullscreenChangedEvents = [
'fullscreenchange',
'webkitfullscreenchange',
'mozfullscreenchange',
'MSFullscreenChange',
];
const BROWSER_ISMOBILE = browser().mobile; const BROWSER_ISMOBILE = browser().mobile;
export default class WebcamDraggableOverlay extends Component { class WebcamDraggable extends Component {
static getWebcamGridBySelector() {
return document.querySelector('div[class*="videoList"]');
}
static getVideoCountBySelector() {
return document.querySelectorAll('video[class*="media"]').length;
}
static getOverlayBySelector() {
return document.querySelector('div[class*="overlay"]');
}
static waitFor(condition, callback) {
const cond = condition();
if (!cond) {
setTimeout(WebcamDraggableOverlay.waitFor.bind(null, condition, callback), 500);
} else {
callback();
}
return false;
}
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
dragging: false,
showDropZones: false,
showBgDropZoneTop: false,
showBgDropZoneBottom: false,
dropOnTop: true,
dropOnBottom: false,
initialPosition: {
x: 0,
y: 0,
},
initialRectPosition: {
x: 0,
y: 0,
},
lastPosition: {
x: 0,
y: 0,
},
resetPosition: false,
isFullScreen: false,
isVideoLoaded: false,
isMinWidth: false,
};
this.shouldUpdatePosition = true;
this.updateWebcamPositionByResize = this.updateWebcamPositionByResize.bind(this);
this.eventVideoFocusChangeListener = this.eventVideoFocusChangeListener.bind(this);
this.eventResizeListener = _.throttle(
this.updateWebcamPositionByResize,
500,
{
leading: true,
trailing: true,
},
);
this.videoMounted = this.videoMounted.bind(this);
this.handleWebcamDragStart = this.handleWebcamDragStart.bind(this); this.handleWebcamDragStart = this.handleWebcamDragStart.bind(this);
this.handleWebcamDragStop = this.handleWebcamDragStop.bind(this); this.handleWebcamDragStop = this.handleWebcamDragStop.bind(this);
this.handleFullscreenChange = this.handleFullscreenChange.bind(this);
this.fullscreenButtonChange = this.fullscreenButtonChange.bind(this);
this.setIsFullScreen = this.setIsFullScreen.bind(this);
this.setResetPosition = this.setResetPosition.bind(this);
this.setInitialReferencePoint = this.setInitialReferencePoint.bind(this);
this.setLastPosition = this.setLastPosition.bind(this);
this.setLastWebcamPosition = this.setLastWebcamPosition.bind(this);
this.setisMinWidth = this.setisMinWidth.bind(this);
this.setDropOnTop = this.setDropOnTop.bind(this);
this.dropZoneTopEnterHandler = this.dropZoneTopEnterHandler.bind(this);
this.dropZoneTopLeaveHandler = this.dropZoneTopLeaveHandler.bind(this);
this.dropZoneBottomEnterHandler = this.dropZoneBottomEnterHandler.bind(this);
this.dropZoneBottomLeaveHandler = this.dropZoneBottomLeaveHandler.bind(this);
this.dropZoneTopMouseUpHandler = this.dropZoneTopMouseUpHandler.bind(this);
this.dropZoneBottomMouseUpHandler = this.dropZoneBottomMouseUpHandler.bind(this);
} }
componentDidMount() { componentDidMount() {
const { floatingOverlay } = this.props; window.addEventListener('resize', _.debounce(this.onResize.bind(this), 500));
const { resetPosition } = this.state;
if (!floatingOverlay
&& !resetPosition) {
this.setResetPosition(true);
}
window.addEventListener('resize', this.eventResizeListener);
window.addEventListener('videoFocusChange', this.eventVideoFocusChangeListener);
fullscreenChangedEvents.forEach((event) => {
document.addEventListener(event, this.handleFullscreenChange);
});
// Ensures that the event will be called before the resize
document.addEventListener('webcamFullscreenButtonChange', this.fullscreenButtonChange);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { swapLayout, usersVideo, mediaContainer } = this.props; const { swapLayout } = this.props;
const { lastPosition } = this.state; if (prevProps.swapLayout === true && swapLayout === false) {
const { y } = lastPosition; setTimeout(() => this.forceUpdate(), 500);
const userLength = usersVideo.length; }
const prevUserLength = prevProps.usersVideo.length; }
if (prevProps.mediaContainer && mediaContainer) { onResize() {
const mediaContainerRect = mediaContainer.getBoundingClientRect(); const { webcamDraggableState, webcamDraggableDispatch } = this.props;
const { const { mediaSize } = webcamDraggableState;
left: mediaLeft, const { width: stateWidth, height: stateHeight } = mediaSize;
top: mediaTop, const { width, height } = this.getMediaBounds();
} = mediaContainerRect;
const prevMediaContainerRect = prevProps.mediaContainer.getBoundingClientRect();
const {
left: prevMediaLeft,
top: prevMediaTop,
} = prevMediaContainerRect;
if (mediaLeft !== prevMediaLeft || mediaTop !== prevMediaTop) { if (stateWidth !== width || stateHeight !== height) {
this.shouldUpdatePosition = false; webcamDraggableDispatch(
} else if (this.shouldUpdatePosition === false) { {
this.shouldUpdatePosition = true; type: 'setMediaSize',
} value: {
} width,
height,
if (prevProps.swapLayout && !swapLayout && userLength === 1) { },
this.shouldUpdatePosition = false; },
}
if (prevProps.swapLayout && !swapLayout && userLength > 1) {
this.setLastPosition(0, y);
}
if (prevUserLength === 1 && userLength > 1) {
this.setResetPosition(true);
this.setDropOnTop(true);
}
if (prevUserLength !== userLength) {
WebcamDraggableOverlay.waitFor(
() => WebcamDraggableOverlay.getVideoCountBySelector() === userLength,
this.updateWebcamPositionByResize,
); );
} }
} }
componentWillUnmount() { getMediaBounds() {
fullscreenChangedEvents.forEach((event) => { const { refMediaContainer, webcamDraggableState, webcamDraggableDispatch } = this.props;
document.removeEventListener(event, this.handleFullscreenChange); const { mediaSize: mediaState } = webcamDraggableState;
});
document.removeEventListener('webcamFullscreenButtonChange', this.fullscreenButtonChange);
document.removeEventListener('videoFocusChange', this.eventVideoFocusChangeListener);
}
setIsFullScreen(isFullScreen) {
this.setState({ isFullScreen });
}
setResetPosition(resetPosition) {
this.setState({ resetPosition });
}
setLastPosition(x, y) {
this.setState({
lastPosition: {
x,
y,
},
});
}
setDropOnTop(dropOnTop) {
this.setState({ dropOnTop });
}
setInitialReferencePoint() {
const { refMediaContainer, usersVideo } = this.props;
const { current: mediaContainer } = refMediaContainer; const { current: mediaContainer } = refMediaContainer;
const userLength = usersVideo.length; if (mediaContainer) {
const webcamBySelector = WebcamDraggableOverlay.getWebcamGridBySelector();
if (webcamBySelector && mediaContainer && this.shouldUpdatePosition) {
const webcamBySelectorRect = webcamBySelector.getBoundingClientRect();
const {
width: webcamWidth,
height: webcamHeight,
} = webcamBySelectorRect;
const mediaContainerRect = mediaContainer.getBoundingClientRect(); const mediaContainerRect = mediaContainer.getBoundingClientRect();
const { const {
width: mediaWidth, top, left, width, height,
height: mediaHeight,
} = mediaContainerRect; } = mediaContainerRect;
const x = mediaWidth - ((webcamWidth + 10) * userLength); // 10 is margin if (mediaState.width === 0 || mediaState.height === 0) {
const y = mediaHeight - ((webcamHeight + 10)); // 10 is margin webcamDraggableDispatch(
{
type: 'setMediaSize',
value: {
width,
height,
},
},
);
}
if (x === 0 && y === 0) return false; return {
top,
this.setState({ left,
initialRectPosition: { width,
x, height,
y, };
},
});
return true;
} }
return false; return false;
} }
setLastWebcamPosition() { getWebcamsListBounds() {
const { refMediaContainer, usersVideo, floatingOverlay } = this.props; const { webcamDraggableState, singleWebcam } = this.props;
const { current: mediaContainer } = refMediaContainer; const { videoListRef } = webcamDraggableState;
const { if (videoListRef) {
initialRectPosition, const videoListRefRect = videoListRef.getBoundingClientRect();
dragging,
dropOnTop,
dropOnBottom,
} = this.state;
const userLength = usersVideo.length;
const { x: initX, y: initY } = initialRectPosition;
const webcamBySelector = WebcamDraggableOverlay.getWebcamGridBySelector();
if (webcamBySelector && mediaContainer && this.shouldUpdatePosition) {
const webcamBySelectorRect = webcamBySelector.getBoundingClientRect();
const { const {
left: webcamLeft, top, left, width, height,
top: webcamTop, } = videoListRefRect;
} = webcamBySelectorRect; return {
top: top - 10, // 10 = margin
const mediaContainerRect = mediaContainer.getBoundingClientRect(); left: left - (singleWebcam ? 10 : 0), // 10 = margin
const { width: width + (singleWebcam ? 20 : 0), // 20 = margin
left: mediaLeft, height: height + 20, // 20 = margin
top: mediaTop, };
} = mediaContainerRect;
const webcamXByMedia = webcamLeft - mediaLeft;
const webcamYByMedia = webcamTop - mediaTop;
let x = -(initX - webcamXByMedia);
x = floatingOverlay ? -((initX - webcamXByMedia) + 10) : x;
x = userLength > 1 ? 0 : x;
x = !dragging && webcamXByMedia < 0 ? -initX : x;
let y = -(initY - webcamYByMedia);
y = webcamYByMedia < 0 ? -initY : y;
y = userLength > 1 && dropOnTop ? -initY : y;
y = userLength > 1 && dropOnBottom ? 0 : y;
y = y < -initY ? -initY : y;
y = y > 0 ? 0 : y;
this.setLastPosition(x, y);
} }
return false;
} }
setisMinWidth(isMinWidth) { calculatePosition() {
this.setState({ isMinWidth }); const { top: mediaTop, left: mediaLeft } = this.getMediaBounds();
const { top: webcamsListTop, left: webcamsListLeft } = this.getWebcamsListBounds();
const x = webcamsListLeft - mediaLeft;
const y = webcamsListTop - mediaTop;
return {
x,
y,
};
} }
videoMounted() { async handleWebcamDragStart() {
this.setResetPosition(true); const { webcamDraggableDispatch, singleWebcam } = this.props;
WebcamDraggableOverlay.waitFor(this.setInitialReferencePoint, this.setLastWebcamPosition); const { x, y } = await this.calculatePosition();
this.setState({ isVideoLoaded: true });
}
fullscreenButtonChange() { webcamDraggableDispatch({ type: 'dragStart' });
this.setIsFullScreen(true);
}
updateWebcamPositionByResize() { webcamDraggableDispatch(
const { {
isVideoLoaded, type: 'setTempPosition',
isMinWidth, value: {
} = this.state; x: singleWebcam ? x : 0,
y,
if (isVideoLoaded) { },
this.setInitialReferencePoint(); },
this.setLastWebcamPosition(); );
}
if (window.innerWidth < 641) {
this.setisMinWidth(true);
this.setState({ dropOnTop: true });
this.setResetPosition(true);
} else if (isMinWidth) {
this.setisMinWidth(false);
}
}
eventVideoFocusChangeListener() {
setTimeout(() => {
this.setInitialReferencePoint();
this.setLastWebcamPosition();
}, 500);
}
handleFullscreenChange() {
if (document.fullscreenElement
|| document.webkitFullscreenElement
|| document.mozFullScreenElement
|| document.msFullscreenElement) {
window.removeEventListener('resize', this.eventResizeListener);
this.setIsFullScreen(true);
} else {
this.setIsFullScreen(false);
window.addEventListener('resize', this.eventResizeListener);
}
}
handleWebcamDragStart() {
const { floatingOverlay } = this.props;
const {
dragging,
showDropZones,
dropOnTop,
dropOnBottom,
resetPosition,
} = this.state;
if (!floatingOverlay && dropOnTop) WebcamDraggableOverlay.getOverlayBySelector().style.top = 0;
if (!dragging) this.setState({ dragging: true });
if (dropOnTop) this.setState({ dropOnTop: false });
if (dropOnBottom) this.setState({ dropOnBottom: false });
if (!showDropZones) this.setState({ showDropZones: true });
if (resetPosition) this.setState({ resetPosition: false });
} }
handleWebcamDragStop(e, position) { handleWebcamDragStop(e, position) {
const { const { webcamDraggableDispatch, singleWebcam } = this.props;
dragging, const targetClassname = e.target.className;
showDropZones,
} = this.state;
const { x, y } = position; const { x, y } = position;
if (dragging) this.setState({ dragging: false }); if (targetClassname.includes('Top')) {
if (showDropZones) this.setState({ showDropZones: false }); webcamDraggableDispatch({ type: 'setplacementToTop' });
} else if (targetClassname.includes('Bottom')) {
this.setLastPosition(x, y); webcamDraggableDispatch({ type: 'setplacementToBottom' });
window.dispatchEvent(new Event('resize')); } else if (singleWebcam) {
} webcamDraggableDispatch(
{
dropZoneTopEnterHandler() { type: 'setLastPosition',
const { value: {
showBgDropZoneTop, x,
} = this.state; y,
},
if (!showBgDropZoneTop) this.setState({ showBgDropZoneTop: true }); },
} );
webcamDraggableDispatch({ type: 'setplacementToFloating' });
dropZoneBottomEnterHandler() {
const {
showBgDropZoneBottom,
} = this.state;
if (!showBgDropZoneBottom) this.setState({ showBgDropZoneBottom: true });
}
dropZoneTopLeaveHandler() {
const {
showBgDropZoneTop,
} = this.state;
if (showBgDropZoneTop) this.setState({ showBgDropZoneTop: false });
}
dropZoneBottomLeaveHandler() {
const {
showBgDropZoneBottom,
} = this.state;
if (showBgDropZoneBottom) this.setState({ showBgDropZoneBottom: false });
}
dropZoneTopMouseUpHandler() {
const { dropOnTop } = this.state;
if (!dropOnTop) {
this.setState({
dropOnTop: true,
dropOnBottom: false,
resetPosition: true,
});
} }
webcamDraggableDispatch({ type: 'dragEnd' });
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
setTimeout(() => this.setLastWebcamPosition(), 500);
}
dropZoneBottomMouseUpHandler() {
const { dropOnBottom } = this.state;
if (!dropOnBottom) {
this.setState({
dropOnTop: false,
dropOnBottom: true,
resetPosition: true,
});
}
window.dispatchEvent(new Event('resize'));
setTimeout(() => this.setLastWebcamPosition(), 500);
} }
render() { render() {
const { const {
webcamDraggableState,
singleWebcam,
swapLayout, swapLayout,
floatingOverlay,
hideOverlay, hideOverlay,
disableVideo, disableVideo,
audioModalIsOpen, audioModalIsOpen,
refMediaContainer,
usersVideo,
} = this.props; } = this.props;
const userLength = usersVideo.length; const { dragging, isFullscreen } = webcamDraggableState;
let placement = Storage.getItem('webcamPlacement');
const { current: mediaContainer } = refMediaContainer; const lastPosition = Storage.getItem('webcamLastPosition') || { x: 0, y: 0 };
let position = lastPosition;
let mediaContainerRect; if (!placement) {
let mediaHeight; placement = webcamsDefaultPlacement;
if (mediaContainer) {
mediaContainerRect = mediaContainer.getBoundingClientRect();
const {
height,
} = mediaContainerRect;
mediaHeight = height;
} }
if (dragging) {
position = webcamDraggableState.tempPosition;
} else if (!dragging && placement === 'floating' && singleWebcam) {
position = webcamDraggableState.lastPosition;
} else {
position = {
x: 0,
y: 0,
};
}
if (swapLayout || isFullscreen || BROWSER_ISMOBILE) {
position = {
x: 0,
y: 0,
};
}
const { const {
dragging, width: mediaWidth,
showDropZones, height: mediaHeight,
showBgDropZoneTop, } = this.getMediaBounds();
showBgDropZoneBottom,
dropOnTop, const {
dropOnBottom, width: webcamsWidth,
initialPosition, height: webcamsHeight,
lastPosition, } = this.getWebcamsListBounds();
resetPosition,
isFullScreen, const isOverflowWidth = (lastPosition.x + webcamsWidth) > mediaWidth;
isMinWidth, const isOverflowHeight = (lastPosition.y + webcamsHeight) > mediaHeight;
} = this.state;
position = {
x: isOverflowWidth
&& !dragging && !swapLayout && singleWebcam && placement === 'floating' ? mediaWidth - webcamsWidth : position.x,
y: isOverflowHeight
&& !dragging && !swapLayout && singleWebcam && placement === 'floating' ? mediaHeight - (webcamsHeight + 1) : position.y,
};
const contentClassName = cx({ const contentClassName = cx({
[styles.content]: true, [styles.content]: true,
@ -490,62 +214,50 @@ export default class WebcamDraggableOverlay extends Component {
const overlayClassName = cx({ const overlayClassName = cx({
[styles.overlay]: true, [styles.overlay]: true,
[styles.overlayRelative]: (dropOnTop || dropOnBottom),
[styles.overlayAbsoluteMult]: (!dropOnTop && !dropOnBottom) && userLength > 1,
[styles.hideOverlay]: hideOverlay, [styles.hideOverlay]: hideOverlay,
[styles.floatingOverlay]: floatingOverlay && (!dropOnTop && !dropOnBottom), [styles.floatingOverlay]: (singleWebcam && placement === 'floating') || dragging,
[styles.overlayToTop]: dropOnTop, [styles.fit]: singleWebcam && (placement === 'floating' || dragging),
[styles.overlayToBottom]: dropOnBottom, [styles.full]: (singleWebcam && (placement === 'top' || placement === 'bottom')
&& !dragging)
|| !singleWebcam,
[styles.overlayToTop]: (placement === 'floating' && !singleWebcam)
|| (placement === 'top' && !dragging),
[styles.overlayToBottom]: placement === 'bottom' && !dragging,
[styles.dragging]: dragging, [styles.dragging]: dragging,
}); });
const dropZoneTopClassName = cx({ const dropZoneTopClassName = cx({
[styles.dropZoneTop]: true, [styles.dropZoneTop]: true,
[styles.show]: showDropZones, [styles.show]: dragging,
[styles.hide]: !showDropZones, [styles.hide]: !dragging,
[styles.cursorGrabbing]: dragging,
}); });
const dropZoneBottomClassName = cx({ const dropZoneBottomClassName = cx({
[styles.dropZoneBottom]: true, [styles.dropZoneBottom]: true,
[styles.show]: showDropZones, [styles.show]: dragging,
[styles.hide]: !showDropZones, [styles.hide]: !dragging,
[styles.cursorGrabbing]: dragging,
}); });
const dropZoneBgTopClassName = cx({ const dropZoneBgTopClassName = cx({
[styles.dropZoneBg]: true, [styles.dropZoneBgTop]: true,
[styles.top]: true,
[styles.show]: showBgDropZoneTop,
[styles.hide]: !showBgDropZoneTop,
}); });
const dropZoneBgBottomClassName = cx({ const dropZoneBgBottomClassName = cx({
[styles.dropZoneBg]: true, [styles.dropZoneBgBottom]: true,
[styles.bottom]: true,
[styles.show]: showBgDropZoneBottom,
[styles.hide]: !showBgDropZoneBottom,
}); });
const cursor = () => {
if ((!swapLayout || !isFullScreen || !BROWSER_ISMOBILE || !isMinWidth) && !dragging) return 'grab';
if (dragging) return 'grabbing';
return 'default';
};
return ( return (
<Fragment> <Fragment>
<div <div
className={dropZoneTopClassName} className={dropZoneTopClassName}
onMouseEnter={this.dropZoneTopEnterHandler} style={{ height: !singleWebcam ? '50%' : '20%' }}
onMouseLeave={this.dropZoneTopLeaveHandler} >
onMouseUp={this.dropZoneTopMouseUpHandler} <div
data-dropzone="dropZoneTop" className={dropZoneBgTopClassName}
role="presentation" />
style={{ height: userLength > 1 ? '50%' : '20%' }} </div>
/>
<div
className={dropZoneBgTopClassName}
style={{ height: userLength > 1 ? '50%' : '20%' }}
/>
<Draggable <Draggable
handle="video" handle="video"
@ -553,44 +265,41 @@ export default class WebcamDraggableOverlay extends Component {
onStart={this.handleWebcamDragStart} onStart={this.handleWebcamDragStart}
onStop={this.handleWebcamDragStop} onStop={this.handleWebcamDragStop}
onMouseDown={e => e.preventDefault()} onMouseDown={e => e.preventDefault()}
disabled={swapLayout || isFullScreen || BROWSER_ISMOBILE || isMinWidth} disabled={swapLayout || isFullscreen || BROWSER_ISMOBILE}
position={resetPosition || swapLayout ? initialPosition : lastPosition} position={position}
> >
<div <div
className={!swapLayout ? overlayClassName : contentClassName} className={!swapLayout ? overlayClassName : contentClassName}
style={{ style={{
maxHeight: mediaHeight, marginLeft: singleWebcam
&& !(placement === 'bottom' || placement === 'top')
? 10
: 0,
marginRight: singleWebcam
&& !(placement === 'bottom' || placement === 'top')
? 10
: 0,
}} }}
> >
{ {!disableVideo && !audioModalIsOpen ? (
!disableVideo && !audioModalIsOpen <VideoProviderContainer
? ( swapLayout={swapLayout}
<VideoProviderContainer />
cursor={cursor()} ) : null}
swapLayout={swapLayout}
onMount={this.videoMounted}
/>
) : null}
</div> </div>
</Draggable> </Draggable>
<div <div
className={dropZoneBottomClassName} className={dropZoneBottomClassName}
onMouseEnter={this.dropZoneBottomEnterHandler} style={{ height: !singleWebcam ? '50%' : '20%' }}
onMouseLeave={this.dropZoneBottomLeaveHandler} >
onMouseUp={this.dropZoneBottomMouseUpHandler} <div
data-dropzone="dropZoneBottom" className={dropZoneBgBottomClassName}
role="presentation" />
style={{ height: userLength > 1 ? '50%' : '20%' }} </div>
/>
<div
className={dropZoneBgBottomClassName}
style={{ height: userLength > 1 ? '50%' : '20%' }}
/>
</Fragment> </Fragment>
); );
} }
} }
WebcamDraggableOverlay.propTypes = propTypes; export default withDraggableContext(WebcamDraggable);
WebcamDraggableOverlay.defaultProps = defaultProps;

View File

@ -0,0 +1,178 @@
import React, { createContext, useReducer, useEffect } from 'react';
import Storage from '../../../services/storage/session';
export const WebcamDraggableContext = createContext();
const initialState = {
placement: 'top',
mediaSize: {
width: 0,
height: 0,
},
initialRef: {
x: 0,
y: 0,
},
tempPosition: {
x: 0,
y: 0,
},
lastPosition: {
x: 0,
y: 0,
},
dragging: false,
videoRef: null,
videoListRef: null,
isFullscreen: false,
};
const reducer = (state, action) => {
switch (action.type) {
case 'setplacementToTop': {
return {
...state,
placement: 'top',
};
}
case 'setplacementToBottom': {
return {
...state,
placement: 'bottom',
};
}
case 'setplacementToFloating': {
return {
...state,
placement: 'floating',
};
}
case 'setMediaSize': {
return {
...state,
mediaSize: {
width: action.value.width,
height: action.value.height,
},
};
}
case 'setWebcamRef': {
return {
...state,
webcamRef: action.value,
};
}
case 'setInitialRef': {
return {
...state,
initialRef: {
x: action.value.x,
y: action.value.y,
},
};
}
case 'setTempPosition': {
return {
...state,
tempPosition: {
x: action.value.x,
y: action.value.y,
},
};
}
case 'setLastPosition': {
return {
...state,
lastPosition: {
x: action.value.x,
y: action.value.y,
},
};
}
case 'setVideoRef': {
return {
...state,
videoRef: action.value,
};
}
case 'setVideoListRef': {
return {
...state,
videoListRef: action.value,
};
}
case 'dragStart': {
return {
...state,
dragging: true,
};
}
case 'dragEnd': {
return {
...state,
dragging: false,
};
}
case 'onFullscreen': {
return {
...state,
isFullscreen: true,
};
}
case 'offFullscreen': {
return {
...state,
isFullscreen: false,
};
}
default: {
throw new Error('Unexpected action');
}
}
};
const ContextConsumer = Component => props => (
<WebcamDraggableContext.Consumer>
{contexts => <Component {...props} {...contexts} />}
</WebcamDraggableContext.Consumer>
);
const ContextProvider = (props) => {
const [webcamDraggableState, webcamDraggableDispatch] = useReducer(reducer, initialState);
const { placement, lastPosition } = webcamDraggableState;
const { children } = props;
useEffect(() => {
Storage.setItem('webcamPlacement', placement);
Storage.setItem('webcamLastPosition', lastPosition);
}, [
placement,
lastPosition,
]);
return (
<WebcamDraggableContext.Provider value={{
webcamDraggableState,
webcamDraggableDispatch,
...props,
}}
>
{children}
</WebcamDraggableContext.Provider>
);
};
const withProvider = Component => props => (
<ContextProvider {...props}>
<Component />
</ContextProvider>
);
const withConsumer = Component => ContextConsumer(Component);
const withDraggableContext = Component => withProvider(withConsumer(Component));
export {
withProvider,
withConsumer,
withDraggableContext,
};

View File

@ -161,9 +161,6 @@ class VideoProvider extends Component {
} }
componentDidMount() { componentDidMount() {
const { onMount } = this.props;
onMount();
this.checkIceConnectivity(); this.checkIceConnectivity();
document.addEventListener('joinVideo', this.shareWebcam); // TODO find a better way to do this document.addEventListener('joinVideo', this.shareWebcam); // TODO find a better way to do this
document.addEventListener('exitVideo', this.unshareWebcam); document.addEventListener('exitVideo', this.unshareWebcam);
@ -1017,14 +1014,10 @@ class VideoProvider extends Component {
const { const {
users, users,
enableVideoStats, enableVideoStats,
cursor,
swapLayout,
mediaHeight, mediaHeight,
} = this.props; } = this.props;
return ( return (
<VideoList <VideoList
cursor={cursor}
swapLayout={swapLayout}
mediaHeight={mediaHeight} mediaHeight={mediaHeight}
users={users} users={users}
onMount={this.createVideoTag} onMount={this.createVideoTag}

View File

@ -12,6 +12,7 @@ const VideoProviderContainer = ({ children, ...props }) => {
export default withTracker(props => ({ export default withTracker(props => ({
cursor: props.cursor, cursor: props.cursor,
swapLayout: props.swapLayout, swapLayout: props.swapLayout,
mediaHeight: props.mediaHeight,
meetingId: VideoService.meetingId(), meetingId: VideoService.meetingId(),
users: VideoService.getAllUsersVideo(), users: VideoService.getAllUsersVideo(),
userId: VideoService.userId(), userId: VideoService.userId(),
@ -19,5 +20,4 @@ export default withTracker(props => ({
userName: VideoService.userName(), userName: VideoService.userName(),
enableVideoStats: getFromUserSettings('enableVideoStats', Meteor.settings.public.kurento.enableVideoStats), enableVideoStats: getFromUserSettings('enableVideoStats', Meteor.settings.public.kurento.enableVideoStats),
voiceBridge: VideoService.voiceBridge(), voiceBridge: VideoService.voiceBridge(),
onMount: props.onMount,
}))(VideoProviderContainer); }))(VideoProviderContainer);

View File

@ -5,6 +5,7 @@ import cx from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import { styles } from './styles'; import { styles } from './styles';
import VideoListItem from './video-list-item/component'; import VideoListItem from './video-list-item/component';
import { withConsumer } from '../../media/webcam-draggable-overlay/context';
const propTypes = { const propTypes = {
users: PropTypes.arrayOf(PropTypes.object).isRequired, users: PropTypes.arrayOf(PropTypes.object).isRequired,
@ -77,6 +78,14 @@ class VideoList extends Component {
} }
componentDidMount() { componentDidMount() {
const { webcamDraggableDispatch } = this.props;
webcamDraggableDispatch(
{
type: 'setVideoListRef',
value: this.grid,
},
);
this.handleCanvasResize(); this.handleCanvasResize();
window.addEventListener('resize', this.handleCanvasResize, false); window.addEventListener('resize', this.handleCanvasResize, false);
} }
@ -93,6 +102,7 @@ class VideoList extends Component {
} }
const { focusedId } = this.state; const { focusedId } = this.state;
const { width: canvasWidth, height: canvasHeight } = this.canvas.getBoundingClientRect(); const { width: canvasWidth, height: canvasHeight } = this.canvas.getBoundingClientRect();
const gridGutter = parseInt(window.getComputedStyle(this.grid) const gridGutter = parseInt(window.getComputedStyle(this.grid)
.getPropertyValue('grid-row-gap'), 10); .getPropertyValue('grid-row-gap'), 10);
const hasFocusedItem = numItems > 2 && focusedId; const hasFocusedItem = numItems > 2 && focusedId;
@ -142,8 +152,6 @@ class VideoList extends Component {
getStats, getStats,
stopGettingStats, stopGettingStats,
enableVideoStats, enableVideoStats,
cursor,
swapLayout,
} = this.props; } = this.props;
const { focusedId } = this.state; const { focusedId } = this.state;
@ -167,9 +175,6 @@ class VideoList extends Component {
[styles.videoListItem]: true, [styles.videoListItem]: true,
[styles.focused]: focusedId === user.id && users.length > 2, [styles.focused]: focusedId === user.id && users.length > 2,
})} })}
style={{
cursor,
}}
> >
<VideoListItem <VideoListItem
numOfUsers={users.length} numOfUsers={users.length}
@ -182,7 +187,6 @@ class VideoList extends Component {
getStats={(videoRef, callback) => getStats(user.id, videoRef, callback)} getStats={(videoRef, callback) => getStats(user.id, videoRef, callback)}
stopGettingStats={() => stopGettingStats(user.id)} stopGettingStats={() => stopGettingStats(user.id)}
enableVideoStats={enableVideoStats} enableVideoStats={enableVideoStats}
swapLayout={swapLayout}
/> />
</div> </div>
); );
@ -231,4 +235,4 @@ class VideoList extends Component {
VideoList.propTypes = propTypes; VideoList.propTypes = propTypes;
export default injectIntl(VideoList); export default injectIntl(withConsumer(VideoList));

View File

@ -39,6 +39,9 @@
.videoListItem { .videoListItem {
display: flex; display: flex;
overflow: hidden;
width: 100%;
height: 100%;
&.focused { &.focused {
grid-column: 1 / span 2; grid-column: 1 / span 2;
@ -50,7 +53,6 @@
position: relative; position: relative;
display: flex; display: flex;
min-width: 100%; min-width: 100%;
height: 100%;
border-radius: 5px; border-radius: 5px;
&::after { &::after {
@ -83,6 +85,14 @@
border-radius: 5px; border-radius: 5px;
} }
.cursorGrab{
cursor: grab;
}
.cursorGrabbing{
cursor: grabbing;
}
@keyframes spin { @keyframes spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
@ -129,6 +139,7 @@
.media { .media {
@extend %media-area; @extend %media-area;
background-color: var(--color-gray);
} }
.info { .info {

View File

@ -16,6 +16,7 @@ import logger from '/imports/startup/client/logger';
import VideoListItemStats from './video-list-item-stats/component'; import VideoListItemStats from './video-list-item-stats/component';
import FullscreenButtonContainer from '../../fullscreen-button/container'; import FullscreenButtonContainer from '../../fullscreen-button/container';
import { styles } from '../styles'; import { styles } from '../styles';
import { withConsumer } from '../../../media/webcam-draggable-overlay/context';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
connectionStatsLabel: { connectionStatsLabel: {
@ -40,7 +41,15 @@ class VideoListItem extends Component {
} }
componentDidMount() { componentDidMount() {
const { onMount } = this.props; const { onMount, webcamDraggableDispatch } = this.props;
webcamDraggableDispatch(
{
type: 'setVideoRef',
value: this.videoTag,
},
);
onMount(this.videoTag); onMount(this.videoTag);
this.videoTag.addEventListener('loadeddata', () => this.setVideoIsReady()); this.videoTag.addEventListener('loadeddata', () => this.setVideoIsReady());
@ -130,7 +139,9 @@ class VideoListItem extends Component {
render() { render() {
const { showStats, stats, videoIsReady } = this.state; const { showStats, stats, videoIsReady } = this.state;
const { const {
user, numOfUsers, user,
numOfUsers,
webcamDraggableState,
} = this.props; } = this.props;
const availableActions = this.getAvailableActions(); const availableActions = this.getAvailableActions();
const enableVideoMenu = Meteor.settings.public.kurento.enableVideoMenu || false; const enableVideoMenu = Meteor.settings.public.kurento.enableVideoMenu || false;
@ -149,6 +160,8 @@ class VideoListItem extends Component {
muted muted
className={cx({ className={cx({
[styles.media]: true, [styles.media]: true,
[styles.cursorGrab]: !webcamDraggableState.dragging,
[styles.cursorGrabbing]: webcamDraggableState.dragging,
})} })}
ref={(ref) => { this.videoTag = ref; }} ref={(ref) => { this.videoTag = ref; }}
autoPlay autoPlay
@ -198,7 +211,7 @@ class VideoListItem extends Component {
} }
} }
export default injectIntl(VideoListItem); export default injectIntl(withConsumer(VideoListItem));
VideoListItem.defaultProps = { VideoListItem.defaultProps = {
numOfUsers: 0, numOfUsers: 0,