diff --git a/Makefile b/Makefile index 617a110ca..766325053 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ BROWSERIFY = ./node_modules/.bin/browserify UGLIFYJS = ./node_modules/.bin/uglifyjs EXORCIST = ./node_modules/.bin/exorcist CLEANCSS = ./node_modules/.bin/cleancss -CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css +CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css jquery.contextMenu.css DEPLOY_DIR = libs BROWSERIFY_FLAGS = -d OUTPUT_DIR = . diff --git a/app.js b/app.js index e8a02a2ee..523e55781 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ import "babel-polyfill"; import "jquery"; +import "jquery-contextmenu"; import "jquery-ui"; import "strophe"; import "strophe-disco"; diff --git a/conference.js b/conference.js index e48f28814..f5895f6c3 100644 --- a/conference.js +++ b/conference.js @@ -7,6 +7,8 @@ import AuthHandler from './modules/UI/authentication/AuthHandler'; import ConnectionQuality from './modules/connectionquality/connectionquality'; +import Recorder from './modules/recorder/Recorder'; + import CQEvents from './service/connectionquality/CQEvents'; import UIEvents from './service/UI/UIEvents'; @@ -121,30 +123,49 @@ function muteLocalVideo (muted) { } } +/** + * Check if the welcome page is enabled and redirects to it. + */ +function maybeRedirectToWelcomePage() { + if (!config.enableWelcomePage) { + return; + } + // redirect to welcome page + setTimeout(() => { + APP.settings.setWelcomePageEnabled(true); + window.location.pathname = "/"; + }, 3000); +} + +/** + * Executes connection.disconnect and shows the feedback dialog + * @param {boolean} [requestFeedback=false] if user feedback should be requested + * @returns Promise. + */ +function disconnectAndShowFeedback(requestFeedback) { + connection.disconnect(); + if (requestFeedback) { + return APP.UI.requestFeedback(); + } else { + return Promise.resolve(); + } +} + /** * Disconnect from the conference and optionally request user feedback. * @param {boolean} [requestFeedback=false] if user feedback should be requested */ function hangup (requestFeedback = false) { - APP.conference._room.leave().then(() => { - connection.disconnect(); - if (requestFeedback) { - return APP.UI.requestFeedback(); - } else { - return Promise.resolve(); - } - }).then(function () { - if (!config.enableWelcomePage) { - return; - } - // redirect to welcome page - setTimeout(() => { - APP.settings.setWelcomePageEnabled(true); - window.location.pathname = "/"; - }, 3000); - }, function (err) { - console.error('Failed to hangup the call:', err); - }); + const errCallback = (f, err) => { + console.error('Error occurred during hanging up: ', err); + return f(); + }; + const disconnect = disconnectAndShowFeedback.bind(null, requestFeedback); + APP.conference._room.leave() + .then(disconnect) + .catch(errCallback.bind(null, disconnect)) + .then(maybeRedirectToWelcomePage) + .catch(errCallback.bind(null, maybeRedirectToWelcomePage)); } /** @@ -346,6 +367,9 @@ export default { devices => APP.UI.onAvailableDevicesChanged(devices) ); } + if (config.iAmRecorder) + this.recorder = new Recorder(); + // XXX The API will take care of disconnecting from the XMPP server // (and, thus, leaving the room) on unload. return new Promise((resolve, reject) => { @@ -532,14 +556,14 @@ export default { * @param command {String} the name of the command * @param handler {Function} handler for the command */ - addCommandListener () { + addCommandListener () { room.addCommandListener.apply(room, arguments); }, /** * Removes command. * @param name {String} the name of the command. */ - removeCommand () { + removeCommand () { room.removeCommand.apply(room, arguments); }, /** @@ -547,7 +571,7 @@ export default { * @param name {String} the name of the command. * @param values {Object} with keys and values that will be sent. */ - sendCommand () { + sendCommand () { room.sendCommand.apply(room, arguments); }, /** @@ -555,7 +579,7 @@ export default { * @param name {String} the name of the command. * @param values {Object} with keys and values that will be sent. */ - sendCommandOnce () { + sendCommandOnce () { room.sendCommandOnce.apply(room, arguments); } }, @@ -878,10 +902,6 @@ export default { room.on(ConferenceEvents.RECORDER_STATE_CHANGED, (status, error) => { console.log("Received recorder status change: ", status, error); - if(status == "error") { - console.error(error); - return; - } APP.UI.updateRecordingState(status); }); diff --git a/css/jquery.contextMenu.css b/css/jquery.contextMenu.css new file mode 100644 index 000000000..45fd987c6 --- /dev/null +++ b/css/jquery.contextMenu.css @@ -0,0 +1,206 @@ +@charset "UTF-8"; +/*! + * jQuery contextMenu - Plugin for simple contextMenu handling + * + * Version: v2.1.1 + * + * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://swisnl.github.io/jQuery-contextMenu/ + * + * Copyright (c) 2011-2016 SWIS BV and contributors + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * + * Date: 2016-02-28T09:53:18.890Z + */ +@font-face { + font-family: "context-menu-icons"; + font-style: normal; + font-weight: normal; + + src: url("font/context-menu-icons.eot?2qmzf"); + src: url("font/context-menu-icons.eot?2qmzf#iefix") format("embedded-opentype"), url("font/context-menu-icons.woff2?2qmzf") format("woff2"), url("font/context-menu-icons.woff?2qmzf") format("woff"), url("font/context-menu-icons.ttf?2qmzf") format("truetype"); +} + +.context-menu-icon:before { + position: absolute; + top: 50%; + left: 0; + width: 28px; + font-family: "context-menu-icons"; + font-size: 16px; + font-style: normal; + font-weight: normal; + line-height: 1; + color: #2980b9; + text-align: center; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -o-transform: translateY(-50%); + transform: translateY(-50%); + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.context-menu-icon-add:before { + content: ""; +} + +.context-menu-icon-copy:before { + content: ""; +} + +.context-menu-icon-cut:before { + content: ""; +} + +.context-menu-icon-delete:before { + content: ""; +} + +.context-menu-icon-edit:before { + content: ""; +} + +.context-menu-icon-paste:before { + content: ""; +} + +.context-menu-icon-quit:before { + content: ""; +} + +.context-menu-icon.context-menu-hover:before { + color: #fff; +} + +.context-menu-list { + position: absolute; + display: inline-block; + min-width: 180px; + max-width: 360px; + padding: 4px 0; + margin: 5px; + font-family: inherit; + font-size: inherit; + list-style-type: none; + background: #fff; + border: 1px solid #bebebe; + border-radius: 3px; + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5); + box-shadow: 0 2px 5px rgba(0, 0, 0, .5); +} + +.context-menu-item { + position: relative; + padding: 3px 28px; + color: #2f2f2f; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: #fff; +} + +.context-menu-separator { + padding: 0; + margin: 5px 0; + border-bottom: 1px solid #e6e6e6; +} + +.context-menu-item > label > input, +.context-menu-item > label > textarea { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.context-menu-item.context-menu-hover { + color: #fff; + cursor: pointer; + background-color: #2980b9; +} + +.context-menu-item.context-menu-disabled { + color: #626262; + background-color: #fff; +} + +.context-menu-item.context-menu-disabled { + color: #626262; +} + +.context-menu-input.context-menu-hover, +.context-menu-item.context-menu-disabled.context-menu-hover { + cursor: default; + background-color: #eee; +} + +.context-menu-submenu:after { + position: absolute; + top: 50%; + right: 8px; + z-index: 1; + width: 0; + height: 0; + content: ''; + border-color: transparent transparent transparent #2f2f2f; + border-style: solid; + border-width: 4px 0 4px 4px; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -o-transform: translateY(-50%); + transform: translateY(-50%); +} + +/** + * Inputs + */ +.context-menu-item.context-menu-input { + padding: 5px 10px; +} + +/* vertically align inside labels */ +.context-menu-input > label > * { + vertical-align: top; +} + +/* position checkboxes and radios as icons */ +.context-menu-input > label > input[type="checkbox"], +.context-menu-input > label > input[type="radio"] { + position: relative; + top: 3px; +} + +.context-menu-input > label, +.context-menu-input > label > input[type="text"], +.context-menu-input > label > textarea, +.context-menu-input > label > select { + display: block; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.context-menu-input > label > textarea { + height: 100px; +} + +.context-menu-item > .context-menu-list { + top: 5px; + /* re-positioned by js */ + right: -5px; + display: none; +} + +.context-menu-item.context-menu-visible > .context-menu-list { + display: block; +} + +.context-menu-accesskey { + text-decoration: underline; +} diff --git a/lang/languages-hy.json b/lang/languages-hy.json new file mode 100644 index 000000000..62927dad1 --- /dev/null +++ b/lang/languages-hy.json @@ -0,0 +1,11 @@ +{ + "en": "", + "bg": "", + "de": "", + "tr": "", + "it": "", + "fr": "", + "sl": "", + "sk": "", + "sv": "" +} \ No newline at end of file diff --git a/lang/main-hy.json b/lang/main-hy.json new file mode 100644 index 000000000..a588f44b1 --- /dev/null +++ b/lang/main-hy.json @@ -0,0 +1,250 @@ +{ + "contactlist": "", + "connectionsettings": "", + "poweredby": "", + "downloadlogs": "", + "feedback": "", + "roomUrlDefaultMsg": "", + "participant": "", + "me": "", + "speaker": "", + "defaultNickname": "", + "defaultLink": "", + "welcomepage": { + "go": "", + "roomname": "", + "disable": "", + "feature1": { + "title": "", + "content": "" + }, + "feature2": { + "title": "", + "content": "" + }, + "feature3": { + "title": "", + "content": "" + }, + "feature4": { + "title": "", + "content": "" + }, + "feature5": { + "title": "", + "content": "" + }, + "feature6": { + "title": "", + "content": "" + }, + "feature7": { + "title": "", + "content": "" + }, + "feature8": { + "title": "", + "content": "" + } + }, + "toolbar": { + "mute": "", + "videomute": "", + "authenticate": "", + "lock": "", + "invite": "", + "chat": "", + "etherpad": "", + "sharedvideo": "", + "sharescreen": "", + "fullscreen": "", + "sip": "", + "Settings": "", + "hangup": "", + "login": "", + "logout": "", + "dialpad": "", + "sharedVideoMutedPopup": "", + "micMutedPopup": "", + "unableToUnmutePopup": "" + }, + "bottomtoolbar": { + "chat": "", + "filmstrip": "", + "contactlist": "" + }, + "chat": { + "nickname": { + "title": "", + "popover": "" + }, + "messagebox": "" + }, + "settings": { + "title": "", + "update": "", + "name": "", + "startAudioMuted": "", + "startVideoMuted": "", + "selectCamera": "", + "selectMic": "", + "followMe": "" + }, + "videothumbnail": { + "editnickname": "", + "moderator": "", + "videomute": "", + "mute": "", + "kick": "", + "muted": "", + "domute": "" + }, + "connectionindicator": { + "bitrate": "", + "packetloss": "", + "resolution": "", + "less": "", + "more": "", + "address": "", + "remoteport": "", + "remoteport_plural": "", + "localport": "", + "localport_plural": "", + "localaddress": "", + "localaddress_plural": "", + "remoteaddress": "", + "remoteaddress_plural": "", + "transport": "", + "bandwidth": "", + "na": "" + }, + "notify": { + "disconnected": "", + "moderator": "", + "connected": "", + "somebody": "", + "me": "", + "focus": "", + "focusFail": "", + "grantedTo": "", + "grantedToUnknown": "", + "muted": "", + "mutedTitle": "" + }, + "dialog": { + "kickMessage": "", + "popupError": "", + "passwordError": "", + "passwordError2": "", + "connectError": "", + "connectErrorWithMsg": "", + "connecting": "", + "error": "", + "detectext": "", + "failtoinstall": "", + "failedpermissions": "", + "bridgeUnavailable": "", + "jicofoUnavailable": "", + "maxUsersLimitReached": "", + "lockTitle": "", + "lockMessage": "", + "warning": "", + "passwordNotSupported": "", + "sorry": "", + "internalError": "", + "unableToSwitch": "", + "SLDFailure": "", + "SRDFailure": "", + "oops": "", + "defaultError": "", + "passwordRequired": "", + "Ok": "", + "Remove": "", + "shareVideoTitle": "", + "shareVideoLinkError": "", + "removeSharedVideoTitle": "", + "removeSharedVideoMsg": "", + "alreadySharedVideoMsg": "", + "WaitingForHost": "", + "WaitForHostMsg": "", + "IamHost": "", + "Cancel": "", + "retry": "", + "logoutTitle": "", + "logoutQuestion": "", + "sessTerminated": "", + "hungUp": "", + "joinAgain": "", + "Share": "", + "Save": "", + "recording": "", + "recordingToken": "", + "Dial": "", + "sipMsg": "", + "passwordCheck": "", + "passwordMsg": "", + "Invite": "", + "shareLink": "", + "settings1": "", + "settings2": "", + "settings3": "", + "yourPassword": "", + "Back": "", + "serviceUnavailable": "", + "gracefulShutdown": "", + "Yes": "", + "reservationError": "", + "reservationErrorMsg": "", + "password": "", + "userPassword": "", + "token": "", + "tokenAuthFailed": "", + "displayNameRequired": "", + "extensionRequired": "", + "firefoxExtensionPrompt": "", + "feedbackQuestion": "", + "thankYou": "", + "sorryFeedback": "", + "liveStreaming": "", + "streamKey": "", + "startLiveStreaming": "", + "stopStreamingWarning": "", + "stopRecordingWarning": "", + "stopLiveStreaming": "", + "stopRecording": "" + }, + "email": { + "sharedKey": "", + "subject": "", + "body": "", + "and": "" + }, + "connection": { + "ERROR": "", + "CONNECTING": "", + "RECONNECTING": "", + "CONNFAIL": "", + "AUTHENTICATING": "", + "AUTHFAIL": "", + "CONNECTED": "", + "DISCONNECTED": "", + "DISCONNECTING": "", + "ATTACHED": "" + }, + "recording": { + "pending": "", + "on": "", + "off": "", + "failedToStart": "", + "buttonTooltip": "" + }, + "liveStreaming": { + "pending": "", + "on": "", + "off": "", + "unavailable": "", + "failedToStart": "", + "buttonTooltip": "", + "streamIdRequired": "" + } +} \ No newline at end of file diff --git a/lang/main.json b/lang/main.json index 0e5233b05..68137827f 100644 --- a/lang/main.json +++ b/lang/main.json @@ -99,7 +99,8 @@ "mute": "Participant is muted", "kick": "Kick out", "muted": "Muted", - "domute": "Mute" + "domute": "Mute", + "flip": "Flip" }, "connectionindicator": @@ -268,7 +269,8 @@ "on": "Recording", "off": "Recording stopped", "failedToStart": "Recording failed to start", - "buttonTooltip": "Start / stop recording" + "buttonTooltip": "Start / stop recording", + "error": "Recording failed. Please try again." }, "liveStreaming": { @@ -278,6 +280,7 @@ "unavailable": "The live streaming service is currently unavailable. Please try again later.", "failedToStart": "Live streaming failed to start", "buttonTooltip": "Start / stop live stream", - "streamIdRequired": "Please fill in the stream id in order to launch the live streaming." + "streamIdRequired": "Please fill in the stream id in order to launch the live streaming.", + "error": "Live streaming failed. Please try again" } } diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 9d1511ab2..1d5f75f3f 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -205,7 +205,8 @@ var Status = { OFF: "off", AVAILABLE: "available", UNAVAILABLE: "unavailable", - PENDING: "pending" + PENDING: "pending", + ERROR: "error" }; /** @@ -248,6 +249,7 @@ var Recording = { this.recordingOffKey = "liveStreaming.off"; this.recordingPendingKey = "liveStreaming.pending"; this.failedToStartKey = "liveStreaming.failedToStart"; + this.recordingErrorKey = "liveStreaming.error"; this.recordingButtonTooltip = "liveStreaming.buttonTooltip"; } else { @@ -256,6 +258,7 @@ var Recording = { this.recordingOffKey = "recording.off"; this.recordingPendingKey = "recording.pending"; this.failedToStartKey = "recording.failedToStart"; + this.recordingErrorKey = "recording.error"; this.recordingButtonTooltip = "recording.buttonTooltip"; } @@ -338,7 +341,6 @@ var Recording = { */ updateRecordingUI (recordingState) { let buttonSelector = $('#toolbar_button_record'); - let labelSelector = $('#recordingLabel'); // TODO: handle recording state=available if (recordingState === Status.ON) { @@ -346,12 +348,10 @@ var Recording = { buttonSelector.removeClass(this.baseClass); buttonSelector.addClass(this.baseClass + " active"); - labelSelector.attr("data-i18n", this.recordingOnKey); - moveToCorner(labelSelector, true, 3000); - labelSelector - .text(APP.translation.translateString(this.recordingOnKey)); - } else if (recordingState === Status.OFF - || recordingState === Status.UNAVAILABLE) { + this._updateStatusLabel(this.recordingOnKey, false); + } + else if (recordingState === Status.OFF + || recordingState === Status.UNAVAILABLE) { // We don't want to do any changes if this is // an availability change. @@ -362,15 +362,13 @@ var Recording = { buttonSelector.removeClass(this.baseClass + " active"); buttonSelector.addClass(this.baseClass); - moveToCorner(labelSelector, false); let messageKey; if (this.currentState === Status.PENDING) messageKey = this.failedToStartKey; else messageKey = this.recordingOffKey; - labelSelector.attr("data-i18n", messageKey); - labelSelector.text(APP.translation.translateString(messageKey)); + this._updateStatusLabel(messageKey, true); setTimeout(function(){ $('#recordingLabel').css({display: "none"}); @@ -381,16 +379,19 @@ var Recording = { buttonSelector.removeClass(this.baseClass + " active"); buttonSelector.addClass(this.baseClass); - moveToCorner(labelSelector, false); - labelSelector - .attr("data-i18n", this.recordingPendingKey); - labelSelector - .text(APP.translation.translateString( - this.recordingPendingKey)); + this._updateStatusLabel(this.recordingPendingKey, true); + } + else if (recordingState === Status.ERROR) { + buttonSelector.removeClass(this.baseClass + " active"); + buttonSelector.addClass(this.baseClass); + + this._updateStatusLabel(this.recordingErrorKey, true); } this.currentState = recordingState; + let labelSelector = $('#recordingLabel'); + // We don't show the label for available state. if (recordingState !== Status.AVAILABLE && !labelSelector.is(":visible")) @@ -404,6 +405,20 @@ var Recording = { this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED, this.predefinedToken); } + }, + /** + * Updates the status label. + * @param textKey the text to show + * @param isCentered indicates if the label should be centered on the window + * or moved to the top right corner. + */ + _updateStatusLabel(textKey, isCentered) { + let labelSelector = $('#recordingLabel'); + + moveToCorner(labelSelector, !isCentered); + + labelSelector.attr("data-i18n", textKey); + labelSelector.text(APP.translation.translateString(textKey)); } }; diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 351a2b0f3..69a613867 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -168,6 +168,7 @@ class VideoContainer extends LargeContainer { super(); this.stream = null; this.videoType = null; + this.localFlipX = true; this.isVisible = false; @@ -284,13 +285,25 @@ class VideoContainer extends LargeContainer { } stream.attach(this.$video[0]); - - let flipX = stream.isLocal() && !this.isScreenSharing(); + let flipX = stream.isLocal() && this.localFlipX; this.$video.css({ transform: flipX ? 'scaleX(-1)' : 'none' }); } + /** + * Changes the flipX state of the local video. + * @param val {boolean} true if flipped. + */ + setLocalFlipX(val) { + this.localFlipX = val; + if(!this.$video || !this.stream || !this.stream.isLocal()) + return; + this.$video.css({ + transform: this.localFlipX ? 'scaleX(-1)' : 'none' + }); + } + /** * Check if current video stream is screen sharing. * @returns {boolean} @@ -453,7 +466,7 @@ export default class LargeVideoManager { } else { preUpdate = Promise.resolve(); } - + preUpdate.then(() => { let {id, stream, videoType, resolve} = this.newStreamData; this.newStreamData = null; @@ -651,4 +664,12 @@ export default class LargeVideoManager { } }); } + + /** + * Changes the flipX state of the local video. + * @param val {boolean} true if flipped. + */ + onLocalFlipXChange(val) { + this.videoContainer.setLocalFlipX(val); + } } diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index 09b10184e..76964f44d 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -4,16 +4,15 @@ import UIUtil from "../util/UIUtil"; import UIEvents from "../../../service/UI/UIEvents"; import SmallVideo from "./SmallVideo"; -var LargeVideo = require("./LargeVideo"); - const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper; const TrackEvents = JitsiMeetJS.events.track; function LocalVideo(VideoLayout, emitter) { this.videoSpanId = "localVideoContainer"; this.container = $("#localVideoContainer").get(0); + this.localVideoId = null; this.bindHoverHandler(); - this.flipX = true; + this._buildContextMenu(); this.isLocal = true; this.emitter = emitter; Object.defineProperty(this, 'id', { @@ -165,9 +164,8 @@ LocalVideo.prototype.changeVideo = function (stream) { localVideoContainerSelector.off('click'); localVideoContainerSelector.on('click', localVideoClick); - this.flipX = stream.videoType != "desktop"; let localVideo = document.createElement('video'); - localVideo.id = 'localVideo_' + stream.getId(); + localVideo.id = this.localVideoId = 'localVideo_' + stream.getId(); RTCUIUtils.setAutoPlay(localVideo, true); RTCUIUtils.setVolume(localVideo, 0); @@ -182,9 +180,9 @@ LocalVideo.prototype.changeVideo = function (stream) { // onclick has to be used with Temasys plugin localVideo.onclick = localVideoClick; - if (this.flipX) { - $(localVideo).addClass("flipVideoX"); - } + let isVideo = stream.videoType != "desktop"; + this._enableDisableContextMenu(isVideo); + this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false); // Attach WebRTC stream localVideo = stream.attach(localVideo); @@ -222,4 +220,54 @@ LocalVideo.prototype.setVisible = function(visible) { } }; +/** + * Sets the flipX state of the video. + * @param val {boolean} true for flipped otherwise false; + */ +LocalVideo.prototype.setFlipX = function (val) { + this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val); + if(!this.localVideoId) + return; + if(val) { + this.selectVideoElement().addClass("flipVideoX"); + } else { + this.selectVideoElement().removeClass("flipVideoX"); + } +}; + +/** + * Builds the context menu for the local video. + */ +LocalVideo.prototype._buildContextMenu = function () { + $.contextMenu({ + selector: '#' + this.videoSpanId, + zIndex: 10000, + items: { + flip: { + name: "Flip", + callback: () => { + let val = !APP.settings.getLocalFlipX(); + this.setFlipX(val); + APP.settings.setLocalFlipX(val); + } + } + }, + events: { + show : function(options){ + options.items.flip.name = + APP.translation.translateString("videothumbnail.flip"); + } + } + }); +}; + +/** + * Enables or disables the context menu for the local video. + * @param enable {boolean} true for enable, false for disable + */ +LocalVideo.prototype._enableDisableContextMenu = function (enable) { + if($('#' + this.videoSpanId).contextMenu) + $('#' + this.videoSpanId).contextMenu(enable); +}; + export default LocalVideo; diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index d3acdaa35..a750e1156 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -165,9 +165,6 @@ SmallVideo.createStreamElement = function (stream) { console.log("(TIME) Render " + type + ":\t", now); }; - - element.oncontextmenu = function () { return false; }; - return element; }; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 3bb02ca9d..5f2a785b4 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -33,6 +33,11 @@ var eventEmitter = null; */ var pinnedId = null; +/** + * flipX state of the localVideo + */ +let localFlipX = null; + /** * On contact list item clicked. */ @@ -92,6 +97,11 @@ let largeVideo; var VideoLayout = { init (emitter) { eventEmitter = emitter; + eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED, function (val) { + localFlipX = val; + if(largeVideo) + largeVideo.onLocalFlipXChange(val); + }); localVideoThumbnail = new LocalVideo(VideoLayout, emitter); // sets default video type of local video localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE); @@ -105,6 +115,9 @@ var VideoLayout = { initLargeVideo (isSideBarVisible) { largeVideo = new LargeVideoManager(); + if(localFlipX) { + largeVideo.onLocalFlipXChange(localFlipX); + } largeVideo.updateContainerSize(isSideBarVisible); AudioLevels.init(); }, @@ -1084,6 +1097,15 @@ var VideoLayout = { videoResolutionLabel.css({display: "block"}); else if (!isResolutionHD && videoResolutionLabel.is(":visible")) videoResolutionLabel.css({display: "none"}); + }, + + /** + * Sets the flipX state of the local video. + * @param {boolean} true for flipped otherwise false; + */ + setLocalFlipX: function (val) { + this.localFlipX = val; + } }; diff --git a/modules/recorder/Recorder.js b/modules/recorder/Recorder.js new file mode 100644 index 000000000..2bc527c15 --- /dev/null +++ b/modules/recorder/Recorder.js @@ -0,0 +1,41 @@ +/* global APP, $, config */ + +/** + * The (name of the) command which transports the recorder info. + */ +const _USER_INFO_COMMAND = "userinfo"; + +/** + * The Recorder class is meant to take care of recorder related presence + * commands. + */ +class Recorder { + constructor() { + if (config.iAmRecorder) + this._sendRecorderInfo(); + } + + /** + * Sends the information that this is a recorder through the presence. + * @private + */ + _sendRecorderInfo() { + var commands = APP.conference.commands; + + // XXX The "Follow Me" command represents a snapshot of all states + // which are to be followed so don't forget to removeCommand before + // sendCommand! + commands.removeCommand(_USER_INFO_COMMAND); + var self = this; + commands.sendCommand( + _USER_INFO_COMMAND, + { + attributes: { + xmlns: 'http://jitsi.org/jitmeet/userinfo', + robot: true + } + }); + } +} + +export default Recorder; \ No newline at end of file diff --git a/modules/settings/Settings.js b/modules/settings/Settings.js index 8ba6a7d30..ba5b01ff1 100644 --- a/modules/settings/Settings.js +++ b/modules/settings/Settings.js @@ -6,6 +6,7 @@ let language = null; let cameraDeviceId = ''; let micDeviceId = ''; let welcomePageDisabled = false; +let localFlipX = null; function supportsLocalStorage() { try { @@ -31,6 +32,7 @@ if (supportsLocalStorage()) { } email = UIUtil.unescapeHtml(window.localStorage.email || ''); + localFlipX = JSON.parse(window.localStorage.localFlipX || true); displayName = UIUtil.unescapeHtml(window.localStorage.displayname || ''); language = window.localStorage.language; cameraDeviceId = window.localStorage.cameraDeviceId || ''; @@ -87,6 +89,23 @@ export default { window.localStorage.language = lang; }, + /** + * Sets new flipX state of local video and saves it to the local storage. + * @param {string} val flipX state of local video + */ + setLocalFlipX: function (val) { + localFlipX = val; + window.localStorage.localFlipX = val; + }, + + /** + * Returns flipX state of local video. + * @returns {string} flipX + */ + getLocalFlipX: function () { + return localFlipX; + }, + /** * Get device id of the camera which is currently in use. * Empty string stands for default device. diff --git a/package.json b/package.json index 0fd97c61a..e71995d25 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "jquery": "~2.1.1", "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0", "lib-jitsi-meet": "jitsi/lib-jitsi-meet", + "jquery-contextmenu": "*", "jquery-ui": "^1.10.5", "jssha": "1.5.0", "retry": "0.6.1", @@ -98,6 +99,9 @@ "jQuery-Impromptu": { "depends": "jquery:jQuery" }, + "jquery-contextmenu": { + "depends": "jquery:jQuery" + }, "autosize": { "depends": "jquery:jQuery" } diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 5da292339..2040f2bf0 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -71,5 +71,9 @@ export default { * Notifies interested listeners that the follow-me feature is enabled or * disabled. */ - FOLLOW_ME_ENABLED: "UI.follow_me_enabled" + FOLLOW_ME_ENABLED: "UI.follow_me_enabled", + /** + * Notifies that flipX property of the local video is changed. + */ + LOCAL_FLIPX_CHANGED: "UI.local_flipx_changed" };