diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 71a8ad471..ab533503f 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -595,17 +595,7 @@ UI.removeListener = function(type, listener) { */ UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options); -UI.clickOnVideo = function(videoNumber) { - const videos = $('#remoteVideos .videocontainer:not(#mixedstream)'); - const videosLength = videos.length; - - if (videosLength <= videoNumber) { - return; - } - const videoIndex = videoNumber === 0 ? 0 : videosLength - videoNumber; - - videos[videoIndex].click(); -}; +UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber); // Used by torture. UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout)); diff --git a/modules/UI/shared_video/SharedVideoThumb.js b/modules/UI/shared_video/SharedVideoThumb.js index 4f8f4405a..015603867 100644 --- a/modules/UI/shared_video/SharedVideoThumb.js +++ b/modules/UI/shared_video/SharedVideoThumb.js @@ -15,11 +15,14 @@ export default function SharedVideoThumb(participant, videoType, VideoLayout) { this.videoSpanId = 'sharedVideoContainer'; this.container = this.createContainer(this.videoSpanId); this.$container = $(this.container); - this.container.onclick = this.videoClick.bind(this); + this.bindHoverHandler(); SmallVideo.call(this, VideoLayout); this.isVideoMuted = true; this.setDisplayName(participant.name); + + this.container.onclick = this._onContainerClick; + this.container.ondblclick = this._onContainerDoubleClick; } SharedVideoThumb.prototype = Object.create(SmallVideo.prototype); SharedVideoThumb.prototype.constructor = SharedVideoThumb; @@ -61,13 +64,6 @@ SharedVideoThumb.prototype.createContainer = function(spanId) { return container; }; -/** - * The thumb click handler. - */ -SharedVideoThumb.prototype.videoClick = function() { - this._togglePin(); -}; - /** * Removes RemoteVideo from the page. */ diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index ad9ae43d2..764b3430b 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -61,7 +61,8 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) { this.addAudioLevelIndicator(); this.updateIndicators(); - this.container.onclick = this._onContainerClick.bind(this); + this.container.onclick = this._onContainerClick; + this.container.ondblclick = this._onContainerDoubleClick; } LocalVideo.prototype = Object.create(SmallVideo.prototype); @@ -253,40 +254,6 @@ LocalVideo.prototype.updateDOMLocation = function() { this._updateVideoElement(); }; -/** - * Callback invoked when the thumbnail is clicked. Will directly call - * VideoLayout to handle thumbnail click if certain elements have not been - * clicked. - * - * @param {MouseEvent} event - The click event to intercept. - * @private - * @returns {void} - */ -LocalVideo.prototype._onContainerClick = function(event) { - // TODO Checking the classes is a workround to allow events to bubble into - // the DisplayName component if it was clicked. React's synthetic events - // will fire after jQuery handlers execute, so stop propogation at this - // point will prevent DisplayName from getting click events. This workaround - // should be removeable once LocalVideo is a React Component because then - // the components share the same eventing system. - const $source = $(event.target || event.srcElement); - const { classList } = event.target; - - const clickedOnDisplayName - = $source.parents('.displayNameContainer').length > 0; - const clickedOnPopover = $source.parents('.popover').length > 0 - || classList.contains('popover'); - const ignoreClick = clickedOnDisplayName || clickedOnPopover; - - if (event.stopPropagation && !ignoreClick) { - event.stopPropagation(); - } - - if (!ignoreClick) { - this._togglePin(); - } -}; - /** * Renders the React Element for displaying video in {@code LocalVideo}. * diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 1ec950273..169542d51 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -22,8 +22,7 @@ import { } from '../../../react/features/remote-video-menu'; import { LAYOUTS, - getCurrentLayout, - shouldDisplayTileView + getCurrentLayout } from '../../../react/features/video-layout'; /* eslint-enable no-unused-vars */ @@ -89,7 +88,8 @@ function RemoteVideo(user, VideoLayout, emitter) { this._setAudioVolume = this._setAudioVolume.bind(this); this._stopRemoteControl = this._stopRemoteControl.bind(this); - this.container.onclick = this._onContainerClick.bind(this); + this.container.onclick = this._onContainerClick; + this.container.ondblclick = this._onContainerDoubleClick; } RemoteVideo.prototype = Object.create(SmallVideo.prototype); @@ -613,36 +613,6 @@ RemoteVideo.prototype.removePresenceLabel = function() { } }; -/** - * Callback invoked when the thumbnail is clicked. Will directly call - * VideoLayout to handle thumbnail click if certain elements have not been - * clicked. - * - * @param {MouseEvent} event - The click event to intercept. - * @private - * @returns {void} - */ -RemoteVideo.prototype._onContainerClick = function(event) { - const $source = $(event.target || event.srcElement); - const { classList } = event.target; - - const ignoreClick = $source.parents('.popover').length > 0 - || classList.contains('popover'); - - if (!ignoreClick) { - this._togglePin(); - } - - // On IE we need to populate this handler on video and it does not - // give event instance as an argument, so we check here for methods. - if (event.stopPropagation && event.preventDefault && !ignoreClick) { - event.stopPropagation(); - event.preventDefault(); - } - - return false; -}; - RemoteVideo.createContainer = function(spanId) { const container = document.createElement('span'); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index fab159a7a..72a29dd35 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -142,6 +142,9 @@ function SmallVideo(VideoLayout) { // Bind event handlers so they are only bound once for every instance. this._onPopoverHover = this._onPopoverHover.bind(this); this.updateView = this.updateView.bind(this); + + this._onContainerClick = this._onContainerClick.bind(this); + this._onContainerDoubleClick = this._onContainerDoubleClick.bind(this); } /** @@ -821,12 +824,71 @@ SmallVideo.prototype.updateIndicators = function() { }; /** - * Pins the participant displayed by this thumbnail or unpins if already pinned. + * Callback invoked when the thumbnail is double clicked. Will pin the + * participant if in tile view. * + * @param {MouseEvent} event - The click event to intercept. * @private * @returns {void} */ -SmallVideo.prototype._togglePin = function() { +SmallVideo.prototype._onContainerDoubleClick = function(event) { + if (this._pinningRequiresDoubleClick() && this._shouldTriggerPin(event)) { + APP.store.dispatch(pinParticipant(this.id)); + } +}; + +/** + * Callback invoked when the thumbnail is clicked and potentially trigger + * pinning of the participant. + * + * @param {MouseEvent} event - The click event to intercept. + * @private + * @returns {void} + */ +SmallVideo.prototype._onContainerClick = function(event) { + const triggerPin = this._shouldTriggerPin(event) + && !this._pinningRequiresDoubleClick(); + + if (event.stopPropagation && triggerPin) { + event.stopPropagation(); + event.preventDefault(); + } + + if (triggerPin) { + this.togglePin(); + } + + return false; +}; + +/** + * Returns whether or not a click event is targeted at certain elements which + * should not trigger a pin. + * + * @param {MouseEvent} event - The click event to intercept. + * @private + * @returns {boolean} + */ +SmallVideo.prototype._shouldTriggerPin = function(event) { + // TODO Checking the classes is a workround to allow events to bubble into + // the DisplayName component if it was clicked. React's synthetic events + // will fire after jQuery handlers execute, so stop propogation at this + // point will prevent DisplayName from getting click events. This workaround + // should be removeable once LocalVideo is a React Component because then + // the components share the same eventing system. + const $source = $(event.target || event.srcElement); + + return $source.parents('.displayNameContainer').length === 0 + && $source.parents('.popover').length === 0 + && !event.target.classList.contains('popover'); +}; + +/** + * Pins the participant displayed by this thumbnail or unpins if already pinned. + * + * @returns {void} + */ +SmallVideo.prototype.togglePin = function() { const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {}; const participantIdToPin @@ -836,6 +898,17 @@ SmallVideo.prototype._togglePin = function() { APP.store.dispatch(pinParticipant(participantIdToPin)); }; +/** + * Returns whether or not clicking to pin the participant needs to be a double + * click instead of a single click. + * + * @private + * @returns {boolean} + */ +SmallVideo.prototype._pinningRequiresDoubleClick = function() { + return shouldDisplayTileView(APP.store.getState()); +}; + /** * Removes the React element responsible for showing connection status, dominant * speaker, and raised hand icons. diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index b64dd9105..d82274d4e 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -391,6 +391,19 @@ const VideoLayout = { return id || null; }, + /** + * Triggers a thumbnail to pin or unpin itself. + * + * @param {number} videoNumber - The index of the video to toggle pin on. + * @private + */ + togglePin(videoNumber) { + const videos = getAllThumbnails(); + const videoView = videos[videoNumber]; + + videoView && videoView.togglePin(); + }, + /** * Callback invoked to update display when the pin participant has changed. *