diff --git a/conference.js b/conference.js
index b5f6100d6..a59cfd429 100644
--- a/conference.js
+++ b/conference.js
@@ -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(
diff --git a/css/settingsmenu.css b/css/settingsmenu.css
index c990dfb4a..1cd63203e 100644
--- a/css/settingsmenu.css
+++ b/css/settingsmenu.css
@@ -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;
diff --git a/index.html b/index.html
index 6c8a831db..1d7fcb29f 100644
--- a/index.html
+++ b/index.html
@@ -240,6 +240,12 @@
+
+
+
diff --git a/lang/main.json b/lang/main.json
index 5125714df..f03c1bbc3 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -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":
{
diff --git a/modules/FollowMe.js b/modules/FollowMe.js
index 03c2a2192..dd9bd3926 100644
--- a/modules/FollowMe.js
+++ b/modules/FollowMe.js
@@ -1,27 +1,29 @@
-/*
- * Copyright @ 2015 Atlassian Pty Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+/*
+ * Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
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);
@@ -74,9 +96,9 @@ import UIEvents from '../service/UI/UIEvents';
* (partially) control the user experience/interface (e.g. film strip
* visibility) of (other) non-moderator particiapnts.
*
- * @author Lyubomir Marinov
+ * @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;
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index b462f6905..a914e1d24 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -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) {
diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js
index 7c65016a5..58315c9a9 100644
--- a/modules/UI/etherpad/Etherpad.js
+++ b/modules/UI/etherpad/Etherpad.js
@@ -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);
}
}
diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js
index e65f7d12a..ce3663175 100644
--- a/modules/UI/shared_video/SharedVideo.js
+++ b/modules/UI/shared_video/SharedVideo.js
@@ -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);
};
/**
diff --git a/modules/UI/side_pannels/settings/SettingsMenu.js b/modules/UI/side_pannels/settings/SettingsMenu.js
index b2baecd0a..266f10e78 100644
--- a/modules/UI/side_pannels/settings/SettingsMenu.js
+++ b/modules/UI/side_pannels/settings/SettingsMenu.js
@@ -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}
diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js
index 380e9d7cc..59ad9123c 100644
--- a/modules/UI/videolayout/LocalVideo.js
+++ b/modules/UI/videolayout/LocalVideo.js
@@ -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');
diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js
index 60c9fbcf9..37e752946 100644
--- a/modules/UI/videolayout/RemoteVideo.js
+++ b/modules/UI/videolayout/RemoteVideo.js
@@ -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