Extends the follow-me feature by adding the possibility to follow the pinned participant, the shared video and the shared document. Adds the possibility to enable disable follow-me from the settings panel. Some other small fixes throughout the UI.

This commit is contained in:
yanas 2016-03-23 20:43:29 -05:00
parent b1469186d1
commit cc761700fe
14 changed files with 338 additions and 107 deletions

View File

@ -31,6 +31,8 @@ const Commands = {
SHARED_VIDEO: "shared-video"
};
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
/**
* Open Connection. When authentication failed it shows auth dialog.
* @param roomName the room name to use
@ -1030,8 +1032,20 @@ export default {
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
room.selectParticipant(id);
});
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (id) => {
room.pinParticipant(id);
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
var smallVideoId = smallVideo.getId();
if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
&& !APP.conference.isLocalId(smallVideoId))
if (isPinned)
room.pinParticipant(smallVideoId);
// When the library starts supporting multiple pins we would
// pass the isPinned parameter together with the identifier,
// but currently we send null to indicate that we unpin the
// last pinned.
else
room.pinParticipant(null);
});
APP.UI.addListener(

View File

@ -43,29 +43,25 @@
cursor: pointer;
}
#startMutedOptions {
#startMutedOptions,
#followMeOptions {
padding-left: 10%;
text-indent: -10%;
margin-top: 10px;
display: none; /* hide by default */
/* clearfix */
overflow: auto;
zoom: 1;
}
#startAudioMuted {
#startAudioMuted,
#startVideoMuted,
#followMeCheckBox {
width: 13px !important;
}
#startVideoMuted {
width: 13px !important;
}
.startMutedLabel {
.startMutedLabel,
.followMeLabel {
width: 94%;
float: left;
cursor: pointer;

View File

@ -240,6 +240,12 @@
<select id="selectMic"></select>
</label>
</div>
<div id="followMeOptions">
<label class = "followMeLabel">
<input type="checkbox" id="followMeCheckBox">
<span data-i18n="settings.followMe"></span>
</label>
</div>
<a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="fa fa-cloud-download"></i></a>
</div>
<div class="feedbackButton">

View File

@ -86,7 +86,8 @@
"startAudioMuted": "start without audio",
"startVideoMuted": "start without video",
"selectCamera": "select camera",
"selectMic": "select microphone"
"selectMic": "select microphone",
"followMe": "Enable follow me"
},
"videothumbnail":
{

View File

@ -15,13 +15,15 @@
*/
import UIEvents from '../service/UI/UIEvents';
import VideoLayout from './UI/videolayout/VideoLayout';
import FilmStrip from './UI/videolayout/FilmStrip';
/**
* The (name of the) command which transports the state (represented by
* {State} for the local state at the time of this writing) of a {FollowMe}
* (instance) between participants.
*/
/* private */ const _COMMAND = "follow-me";
const _COMMAND = "follow-me";
/**
* Represents the set of {FollowMe}-related states (properties and their
@ -29,7 +31,7 @@ import UIEvents from '../service/UI/UIEvents';
* will send {_COMMAND} whenever a property of {State} changes (if the local
* participant is in her right to issue such a command, of course).
*/
/* private */ class State {
class State {
/**
* Initializes a new {State} instance.
*
@ -39,13 +41,13 @@ import UIEvents from '../service/UI/UIEvents';
* the property, the old value of the property before the change, and the
* new value of the property after the change.
*/
/* public */ constructor (propertyChangeCallback) {
/* private*/ this._propertyChangeCallback = propertyChangeCallback;
constructor (propertyChangeCallback) {
this._propertyChangeCallback = propertyChangeCallback;
}
/* public */ get filmStripVisible () { return this._filmStripVisible; }
get filmStripVisible () { return this._filmStripVisible; }
/* public */ set filmStripVisible (b) {
set filmStripVisible (b) {
var oldValue = this._filmStripVisible;
if (oldValue !== b) {
this._filmStripVisible = b;
@ -53,6 +55,26 @@ import UIEvents from '../service/UI/UIEvents';
}
}
get nextOnStage() { return this._nextOnStage; }
set nextOnStage(id) {
var oldValue = this._nextOnStage;
if (oldValue !== id) {
this._nextOnStage = id;
this._firePropertyChange('nextOnStage', oldValue, id);
}
}
get sharedDocumentVisible () { return this._sharedDocumentVisible; }
set sharedDocumentVisible (b) {
var oldValue = this._sharedDocumentVisible;
if (oldValue !== b) {
this._sharedDocumentVisible = b;
this._firePropertyChange('sharedDocumentVisible', oldValue, b);
}
}
/**
* Invokes {_propertyChangeCallback} to notify it that {property} had its
* value changed from {oldValue} to {newValue}.
@ -62,7 +84,7 @@ import UIEvents from '../service/UI/UIEvents';
* @param oldValue the value of {property} before the change
* @param newValue the value of {property} after the change
*/
/* private */ _firePropertyChange (property, oldValue, newValue) {
_firePropertyChange (property, oldValue, newValue) {
var propertyChangeCallback = this._propertyChangeCallback;
if (propertyChangeCallback)
propertyChangeCallback(property, oldValue, newValue);
@ -76,7 +98,7 @@ import UIEvents from '../service/UI/UIEvents';
*
* @author Lyubomir Marinov
*/
/* public */ class FollowMe {
class FollowMe {
/**
* Initializes a new {FollowMe} instance.
*
@ -87,15 +109,14 @@ import UIEvents from '../service/UI/UIEvents';
* destination (model/state) to receive from the remote moderator if the
* local participant is not the moderator
*/
/* public */ constructor (conference, UI) {
/* private */ this._conference = conference;
/* private */ this._UI = UI;
constructor (conference, UI) {
this._conference = conference;
this._UI = UI;
// The states of the local participant which are to be followed (by the
// remote participants when the local participant is in her right to
// issue such commands).
/* private */ this._local
= new State(this._localPropertyChange.bind(this));
this._local = new State(this._localPropertyChange.bind(this));
// Listen to "Follow Me" commands. I'm not sure whether a moderator can
// (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
@ -104,18 +125,57 @@ import UIEvents from '../service/UI/UIEvents';
conference.commands.addCommandListener(
_COMMAND,
this._onFollowMeCommand.bind(this));
// Listen to (user interface) states of the local participant which are
// to be followed (by the remote participants). A non-moderator (very
// likely) can become a moderator so it may be easiest to always track
// the states of interest.
UI.addListener(
UIEvents.TOGGLED_FILM_STRIP,
this._filmStripToggled.bind(this));
// TODO Listen to changes in the moderator role of the local
// participant. When the local participant is granted the moderator
// role, it may need to start sending "Follow Me" commands. Obviously,
// this depends on how we decide to enable the feature at runtime as
// well.
}
/**
* Adds listeners for the UI states of the local participant which are
* to be followed (by the remote participants). A non-moderator (very
* likely) can become a moderator so it may be easiest to always track
* the states of interest.
* @private
*/
_addFollowMeListeners () {
this.filmStripEventHandler = this._filmStripToggled.bind(this);
this._UI.addListener(UIEvents.TOGGLED_FILM_STRIP,
this.filmStripEventHandler);
var self = this;
this.pinnedEndpointEventHandler = function (smallVideo, isPinned) {
self._nextOnStage(smallVideo, isPinned);
};
this._UI.addListener(UIEvents.PINNED_ENDPOINT,
this.pinnedEndpointEventHandler);
this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
this._UI.addListener( UIEvents.TOGGLED_SHARED_DOCUMENT,
this.sharedDocEventHandler);
}
/**
* Removes all follow me listeners.
* @private
*/
_removeFollowMeListeners () {
this._UI.removeListener(UIEvents.TOGGLED_FILM_STRIP,
this.filmStripEventHandler);
this._UI.removeListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
this.sharedDocEventHandler);
this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
this.pinnedEndpointEventHandler);
}
/**
* Enables or disabled the follow me functionality
*
* @param enable {true} to enable the follow me functionality, {false} -
* to disable it
*/
enableFollowMe (enable) {
this.isEnabled = enable;
if (this.isEnabled)
this._addFollowMeListeners();
else
this._removeFollowMeListeners();
}
/**
@ -125,11 +185,49 @@ import UIEvents from '../service/UI/UIEvents';
* @param filmStripVisible {Boolean} {true} if the film strip was shown (as
* a result of the toggle) or {false} if the film strip was hidden
*/
/* private */ _filmStripToggled (filmStripVisible) {
_filmStripToggled (filmStripVisible) {
this._local.filmStripVisible = filmStripVisible;
}
/* private */ _localPropertyChange (property, oldValue, newValue) {
/**
* Notifies this instance that the (visibility of the) shared document was
* toggled (in the user interface of the local participant).
*
* @param sharedDocumentVisible {Boolean} {true} if the shared document was
* shown (as a result of the toggle) or {false} if it was hidden
*/
_sharedDocumentToggled (sharedDocumentVisible) {
this._local.sharedDocumentVisible = sharedDocumentVisible;
}
/**
* Changes the nextOnPage property value.
*
* @param smallVideo the {SmallVideo} that was pinned or unpinned
* @param isPinned indicates if the given {SmallVideo} was pinned or
* unpinned
* @private
*/
_nextOnStage (smallVideo, isPinned) {
if (!this._conference.isModerator)
return;
var nextOnStage = null;
if(isPinned)
nextOnStage = smallVideo.getId();
this._local.nextOnStage = nextOnStage;
}
/**
* Sends the follow-me command, when a local property change occurs.
*
* @param property the property name
* @param oldValue the old value
* @param newValue the new value
* @private
*/
_localPropertyChange (property, oldValue, newValue) {
// Only a moderator is allowed to send commands.
var conference = this._conference;
if (!conference.isModerator)
@ -141,12 +239,14 @@ import UIEvents from '../service/UI/UIEvents';
// sendCommand!
commands.removeCommand(_COMMAND);
var self = this;
commands.sendCommand(
commands.sendCommandOnce(
_COMMAND,
{
attributes: {
filmStripVisible: self._local.filmStripVisible,
},
nextOnStage: self._local.nextOnStage,
sharedDocumentVisible: self._local.sharedDocumentVisible
}
});
}
@ -159,7 +259,7 @@ import UIEvents from '../service/UI/UIEvents';
* notable idiosyncrasy of the Command(s) API to be mindful of here is that
* the command may be issued by the local participant.
*/
/* private */ _onFollowMeCommand ({ attributes }, id) {
_onFollowMeCommand ({ attributes }, id) {
// We require to know who issued the command because (1) only a
// moderator is allowed to send commands and (2) a command MUST be
// issued by a defined commander.
@ -169,28 +269,60 @@ import UIEvents from '../service/UI/UIEvents';
// to act upon them.
if (this._conference.isLocalId(id))
return;
// TODO Don't obey commands issued by non-moderators.
// Apply the received/remote command to the user experience/interface
// Applies the received/remote command to the user experience/interface
// of the local participant.
this._onFilmStripVisible(attributes.filmStripVisible);
this._onNextOnStage(attributes.nextOnStage);
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
}
// filmStripVisible
var filmStripVisible = attributes.filmStripVisible;
_onFilmStripVisible(filmStripVisible) {
if (typeof filmStripVisible !== 'undefined') {
// XXX The Command(s) API doesn't preserve the types (of
// attributes, at least) at the time of this writing so take into
// account that what originated as a Boolean may be a String on
// receipt.
filmStripVisible = (filmStripVisible == 'true');
// FIXME The UI (module) very likely doesn't (want to) expose its
// eventEmitter as a public field. I'm not sure at the time of this
// writing whether calling UI.toggleFilmStrip() is acceptable (from
// a design standpoint) either.
this._UI.eventEmitter.emit(
if (filmStripVisible !== FilmStrip.isFilmStripVisible())
this._UI.eventEmitter.emit(
UIEvents.TOGGLE_FILM_STRIP,
filmStripVisible);
}
}
_onNextOnStage(id) {
var clickId = null;
if(typeof id !== 'undefined' && !VideoLayout.isPinned(id))
clickId = id;
else if (typeof id == 'undefined')
clickId = VideoLayout.getPinnedId();
if (clickId !== null)
VideoLayout.handleVideoThumbClicked(clickId);
}
_onSharedDocumentVisible(sharedDocumentVisible) {
if (typeof sharedDocumentVisible !== 'undefined') {
// XXX The Command(s) API doesn't preserve the types (of
// attributes, at least) at the time of this writing so take into
// account that what originated as a Boolean may be a String on
// receipt.
sharedDocumentVisible = (sharedDocumentVisible == 'true');
if (sharedDocumentVisible
!== this._UI.getSharedDocumentManager().isVisible())
this._UI.getSharedDocumentManager().toggleEtherpad();
}
}
}
export default FollowMe;

View File

@ -35,6 +35,8 @@ UI.eventEmitter = eventEmitter;
let etherpadManager;
let sharedVideoManager;
let followMeHandler;
/**
* Prompt user for nickname.
*/
@ -252,7 +254,7 @@ UI.initConference = function () {
// other participants' UI. Consequently, it needs (1) read and write access
// to the UI (depending on the moderator role of the local participant) and
// (2) APP.conference as means of communication between the participants.
new FollowMe(APP.conference, UI);
followMeHandler = new FollowMe(APP.conference, UI);
};
UI.mucJoined = function () {
@ -290,6 +292,11 @@ function registerListeners() {
UI.toggleFilmStrip();
VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, false);
});
UI.addListener(UIEvents.FOLLOW_ME_ENABLED, function (isEnabled) {
if (followMeHandler)
followMeHandler.enableFollowMe(isEnabled);
});
}
/**
@ -478,10 +485,19 @@ UI.initEtherpad = function (name) {
return;
}
console.log('Etherpad is enabled');
etherpadManager = new EtherpadManager(config.etherpad_base, name);
etherpadManager
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
Toolbar.showEtherpadButton();
};
/**
* Returns the shared document manager object.
* @return {EtherpadManager} the shared document manager object
*/
UI.getSharedDocumentManager = function () {
return etherpadManager;
};
/**
* Show user on UI.
* @param {string} id user id
@ -549,6 +565,7 @@ UI.updateLocalRole = function (isModerator) {
Toolbar.showRecordingButton(isModerator);
Toolbar.showSharedVideoButton(isModerator);
SettingsMenu.showStartMutedOptions(isModerator);
SettingsMenu.showFollowMeOptions(isModerator);
if (isModerator) {
messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
@ -686,10 +703,26 @@ UI.setVideoMuted = function (id, muted) {
}
};
/**
* Adds a listener that would be notified on the given type of event.
*
* @param type the type of the event we're listening for
* @param listener a function that would be called when notified
*/
UI.addListener = function (type, listener) {
eventEmitter.on(type, listener);
};
/**
* Removes the given listener for the given type of event.
*
* @param type the type of the event we're listening for
* @param listener the listener we want to remove
*/
UI.removeListener = function (type, listener) {
eventEmitter.removeListener(type, listener);
};
UI.clickOnVideo = function (videoNumber) {
var remoteVideos = $(".videocontainer:not(#mixedstream)");
if (remoteVideos.length > videoNumber) {

View File

@ -3,6 +3,7 @@
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import SidePanelToggler from "../side_pannels/SidePanelToggler";
import FilmStrip from '../videolayout/FilmStrip';
@ -58,6 +59,7 @@ const ETHERPAD_CONTAINER_TYPE = "etherpad";
* Container for Etherpad iframe.
*/
class Etherpad extends LargeContainer {
constructor (domain, name) {
super();
@ -149,13 +151,14 @@ class Etherpad extends LargeContainer {
* Manager of the Etherpad frame.
*/
export default class EtherpadManager {
constructor (domain, name) {
constructor (domain, name, eventEmitter) {
if (!domain || !name) {
throw new Error("missing domain or name");
}
this.domain = domain;
this.name = name;
this.eventEmitter = eventEmitter;
this.etherpad = null;
}
@ -163,6 +166,10 @@ export default class EtherpadManager {
return !!this.etherpad;
}
isVisible() {
return VideoLayout.isLargeContainerTypeVisible(ETHERPAD_CONTAINER_TYPE);
}
/**
* Create new Etherpad frame.
*/
@ -183,11 +190,12 @@ export default class EtherpadManager {
this.openEtherpad();
}
let isVisible = VideoLayout.isLargeContainerTypeVisible(
ETHERPAD_CONTAINER_TYPE
);
let isVisible = this.isVisible();
VideoLayout.showLargeVideoContainer(
ETHERPAD_CONTAINER_TYPE, !isVisible);
this.eventEmitter
.emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
}
}

View File

@ -120,7 +120,7 @@ export default class SharedVideoManager {
VideoLayout.addLargeVideoContainer(
SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
VideoLayout.handleVideoThumbClicked(true, self.url);
VideoLayout.handleVideoThumbClicked(self.url);
self.isSharedVideoShown = true;
@ -372,7 +372,7 @@ SharedVideoThumb.prototype.createContainer = function (spanId) {
* The thumb click handler.
*/
SharedVideoThumb.prototype.videoClick = function () {
VideoLayout.handleVideoThumbClicked(true, this.url);
VideoLayout.handleVideoThumbClicked(this.url);
};
/**

View File

@ -89,6 +89,14 @@ export default {
);
});
// FOLLOW ME
$("#followMeOptions").change(function () {
let isFollowMeEnabled = $("#followMeCheckBox").is(":checked");
emitter.emit(
UIEvents.FOLLOW_ME_ENABLED,
isFollowMeEnabled
);
});
// LANGUAGES BOX
let languagesBox = $("#languages_selectbox");
@ -135,6 +143,19 @@ export default {
$("#startVideoMuted").attr("checked", startVideoMuted);
},
/**
* Shows/hides the follow me options in the settings dialog.
*
* @param {boolean} show {true} to show those options, {false} to hide them
*/
showFollowMeOptions (show) {
if (show) {
$("#followMeOptions").css("display", "block");
} else {
$("#followMeOptions").css("display", "none");
}
},
/**
* Check if settings menu is visible or not.
* @returns {boolean}

View File

@ -158,7 +158,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
if (event.stopPropagation) {
event.stopPropagation();
}
this.VideoLayout.handleVideoThumbClicked(true, this.id);
this.VideoLayout.handleVideoThumbClicked(this.id);
};
let localVideoContainerSelector = $('#localVideoContainer');

View File

@ -220,7 +220,7 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
// ignore click if it was done in popup menu
if ($(source).parents('.popupmenu').length === 0) {
this.VideoLayout.handleVideoThumbClicked(false, this.id);
this.VideoLayout.handleVideoThumbClicked(this.id);
}
// On IE we need to populate this handler on video <object>

View File

@ -20,6 +20,14 @@ function setVisibility(selector, show) {
}
}
/**
* Returns the identifier of this small video.
*
* @returns the identifier of this small video
*/
SmallVideo.prototype.getId = function () {
return this.id;
};
/* Indicates if this small video is currently visible.
*

View File

@ -31,7 +31,7 @@ var eventEmitter = null;
* Currently focused video jid
* @type {String}
*/
var focusedVideoResourceJid = null;
var pinnedId = null;
/**
* On contact list item clicked.
@ -49,7 +49,7 @@ function onContactClicked (id) {
if (remoteVideo.hasVideoStarted()) {
// We have a video src, great! Let's update the large video
// now.
VideoLayout.handleVideoThumbClicked(false, id);
VideoLayout.handleVideoThumbClicked(id);
} else {
// If we don't have a video src for jid, there's absolutely
@ -63,7 +63,7 @@ function onContactClicked (id) {
// picked up later by the lastN changed event handler.
lastNPickupId = id;
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, id);
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
}
}
}
@ -209,7 +209,7 @@ var VideoLayout = {
// We'll show user's avatar if he is the dominant speaker or if
// his video thumbnail is pinned
if (remoteVideos[id] && (id === focusedVideoResourceJid
if (remoteVideos[id] && (id === pinnedId
|| id === currentDominantSpeaker)) {
newId = id;
} else {
@ -289,20 +289,33 @@ var VideoLayout = {
return smallVideo ? smallVideo.getVideoType() : null;
},
handleVideoThumbClicked (noPinnedEndpointChangedEvent,
resourceJid) {
if(focusedVideoResourceJid) {
isPinned (id) {
return (pinnedId) ? (id === pinnedId) : false;
},
getPinnedId () {
return pinnedId;
},
/**
* Handles the click on a video thumbnail.
*
* @param id the identifier of the video thumbnail
*/
handleVideoThumbClicked (id) {
if(pinnedId) {
var oldSmallVideo
= VideoLayout.getSmallVideo(focusedVideoResourceJid);
= VideoLayout.getSmallVideo(pinnedId);
if (oldSmallVideo && !interfaceConfig.filmStripOnly)
oldSmallVideo.focus(false);
}
var smallVideo = VideoLayout.getSmallVideo(resourceJid);
// Unlock current focused.
if (focusedVideoResourceJid === resourceJid)
var smallVideo = VideoLayout.getSmallVideo(id);
// Unpin if currently pinned.
if (pinnedId === id)
{
focusedVideoResourceJid = null;
pinnedId = null;
// Enable the currently set dominant speaker.
if (currentDominantSpeaker) {
if(smallVideo && smallVideo.hasVideo()) {
@ -310,26 +323,23 @@ var VideoLayout = {
}
}
if (!noPinnedEndpointChangedEvent) {
eventEmitter.emit(UIEvents.PINNED_ENDPOINT);
}
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false);
return;
}
// Lock new video
focusedVideoResourceJid = resourceJid;
pinnedId = id;
// Update focused/pinned interface.
if (resourceJid) {
if (id) {
if (smallVideo && !interfaceConfig.filmStripOnly)
smallVideo.focus(true);
if (!noPinnedEndpointChangedEvent) {
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid);
}
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true);
}
this.updateLargeVideo(resourceJid);
this.updateLargeVideo(id);
},
/**
@ -372,10 +382,10 @@ var VideoLayout = {
// Update the large video to the last added video only if there's no
// current dominant, focused speaker or update it to
// the current dominant speaker.
if ((!focusedVideoResourceJid &&
if ((!pinnedId &&
!currentDominantSpeaker &&
this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
focusedVideoResourceJid === resourceJid ||
pinnedId === resourceJid ||
(resourceJid &&
currentDominantSpeaker === resourceJid)) {
this.updateLargeVideo(resourceJid, true);
@ -531,7 +541,7 @@ var VideoLayout = {
// since we don't want to switch to local video.
// Update the large video if the video source is already available,
// otherwise wait for the "videoactive.jingle" event.
if (!focusedVideoResourceJid
if (!pinnedId
&& remoteVideo.hasVideoStarted()
&& !this.getCurrentlyOnLargeContainer().stayOnStage()) {
this.updateLargeVideo(id);
@ -650,11 +660,7 @@ var VideoLayout = {
// Clean up the lastN pickup id.
lastNPickupId = null;
// Don't fire the events again, they've already
// been fired in the contact list click handler.
VideoLayout.handleVideoThumbClicked(
false,
resourceJid);
VideoLayout.handleVideoThumbClicked(resourceJid);
updateLargeVideo = false;
}
@ -741,9 +747,9 @@ var VideoLayout = {
removeParticipantContainer (id) {
// Unlock large video
if (focusedVideoResourceJid === id) {
if (pinnedId === id) {
console.info("Focused video owner has left the conference");
focusedVideoResourceJid = null;
pinnedId = null;
}
if (currentDominantSpeaker === id) {

View File

@ -56,14 +56,20 @@ export default {
*
* @see {TOGGLE_FILM_STRIP}
*/
TOGGLED_FILM_STRIP: "UI.toggled_fim_strip",
TOGGLED_FILM_STRIP: "UI.toggled_film_strip",
TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document",
CONTACT_CLICKED: "UI.contact_clicked",
HANGUP: "UI.hangup",
LOGOUT: "UI.logout",
RECORDING_TOGGLE: "UI.recording_toggle",
SIP_DIAL: "UI.sip_dial",
SUBEJCT_CHANGED: "UI.subject_changed",
SUBJECT_CHANGED: "UI.subject_changed",
VIDEO_DEVICE_CHANGED: "UI.video_device_changed",
AUDIO_DEVICE_CHANGED: "UI.audio_device_changed"
AUDIO_DEVICE_CHANGED: "UI.audio_device_changed",
/**
* Notifies interested listeners that the follow-me feature is enabled or
* disabled.
*/
FOLLOW_ME_ENABLED: "UI.follow_me_enabled"
};