Compare commits

..

16 Commits

Author SHA1 Message Date
Hristo Terezov
9235000a7f ref(remote-control): Use React/Redux. 2020-12-11 14:24:33 -06:00
damencho
d97f46c163 feat: Skips the default tile view when jibri is loading.
Follows me and switching to tile view, still works.
2020-11-13 14:48:09 -06:00
Jaya Allamsetty
5510138944 fix(screenshare): do not reconfigure encodings for simulcast SS
chore(deps) lib-jitsi-meet@latest
2020-11-13 13:45:36 -05:00
Saúl Ibarra Corretgé
29fa4c935e fix(chat) stop using nicknames
We stopped providing a way to set them, so don't render them either.

Also cleanup some leftover config options.
2020-11-13 17:40:57 +01:00
damencho
7de1e6d89e Updates kick, fixes it after 4b8aae90. 2020-11-11 13:18:13 -06:00
Andrei Bora
c4ef7d8601 Fix get subdomain function 2020-11-11 08:37:35 -06:00
Mihai-Andrei Uscat
9379bb3c5b fix(Toolbox) Maintain overflow button visible at all times
* fix(Toolbox) Maintain overflow button visible at all times

* Make changes only on desktop browser
2020-11-11 16:11:51 +02:00
Vlad Piersec
101a40a8da fix(welcome_page): Fix background image url path 2020-11-11 15:37:16 +02:00
Jaya Allamsetty
36871fa37e fix(safari): Ensure simulcast stream resolutions don't change.
Safari 14.1 has a bug where it returns 720p for every simulcast stream when RTCRtpSender.getParameters is called even though the stream resolutions are different.
By using the encodings config used when source was added, on every RTCRtpSender.setParameters call, we ensure that simulcast stream resolutions don't change.
chore(deps) lib-jitsi-meet@latest
2020-11-11 07:55:58 -05:00
Vlad Piersec
c09ed4c8ef fix(welcome_page): Add max width to welcome card 2020-11-11 14:11:08 +02:00
Vlad Piersec
08dce76763 fix(vpaas): Make user media permission message more generic 2020-11-11 13:24:02 +02:00
Prime9999
d03173e827 fix(lang) update Japanese translations 2020-11-11 09:42:22 +01:00
Дамян Минков
12c835dd91 feat: Drops filmStripOnly mode. (#8074)
* feat: Drops filmStripOnly mode.

* squash: Let's make lint happy again.

* squash: Drop some css.
2020-11-10 16:21:07 -06:00
damencho
f6127d45e9 fix: Fix module allowners and moderated rooms. 2020-11-10 10:43:29 -06:00
Saúl Ibarra Corretgé
9219e80a2a fix(password) set input type to "password"
This will make browsers not cache results in cleartext.

Co-authored-by: Tim Dittler <t.dittler@heinlein-support.de>
2020-11-10 17:05:20 +01:00
tmoldovan8x8
71fb5aef6c feat(rn) add mute everyone / (else) capabilities 2020-11-10 15:49:38 +01:00
88 changed files with 2428 additions and 2836 deletions

View File

@@ -1,4 +0,0 @@
/**
* Notifies interested parties that hangup procedure will start.
*/
export const BEFORE_HANGUP = 'conference.before_hangup';

2
app.js
View File

@@ -17,7 +17,6 @@ import conference from './conference';
import API from './modules/API';
import UI from './modules/UI/UI';
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
import remoteControl from './modules/remotecontrol/RemoteControl';
import translation from './modules/translation/translation';
// Initialize Olm as early as possible.
@@ -49,7 +48,6 @@ window.APP = {
},
keyboardshortcut,
remoteControl,
translation,
UI
};

View File

@@ -3,7 +3,6 @@
import EventEmitter from 'events';
import Logger from 'jitsi-meet-logger';
import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import { openConnection } from './connection';
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
import AuthHandler from './modules/UI/authentication/AuthHandler';
@@ -86,7 +85,8 @@ import {
participantMutedUs,
participantPresenceChanged,
participantRoleChanged,
participantUpdated
participantUpdated,
updateRemoteParticipantFeatures
} from './react/features/base/participants';
import {
getUserSelectedCameraDeviceId,
@@ -122,14 +122,13 @@ import {
isPrejoinPageVisible,
makePrecallTest
} from './react/features/prejoin';
import { disableReceiver, stopReceiver } from './react/features/remote-control';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
import { setSharedVideoStatus } from './react/features/shared-video';
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
import { createPresenterEffect } from './react/features/stream-effects/presenter';
import { endpointMessageReceived } from './react/features/subtitles';
import UIEvents from './service/UI/UIEvents';
import * as RemoteControlEvents
from './service/remotecontrol/RemoteControlEvents';
const logger = Logger.getLogger(__filename);
@@ -671,7 +670,6 @@ export default {
APP.connection = connection = con;
this._createRoom(tracks);
APP.remoteControl.init();
// if user didn't give access to mic or camera or doesn't have
// them at all, we mark corresponding toolbar buttons as muted,
@@ -1428,11 +1426,8 @@ export default {
async _turnScreenSharingOff(didHaveVideo) {
this._untoggleScreenSharing = null;
this.videoSwitchInProgress = true;
const { receiver } = APP.remoteControl;
if (receiver) {
receiver.stop();
}
APP.store.dispatch(stopReceiver());
this._stopProxyConnection();
if (config.enableScreenshotCapture) {
@@ -1855,8 +1850,9 @@ export default {
(authEnabled, authLogin) =>
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
user => APP.UI.onUserFeaturesChanged(user));
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => {
APP.store.dispatch(updateRemoteParticipantFeatures(user));
});
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
// The logic shared between RN and web.
commonUserJoinedHandling(APP.store, room, user);
@@ -1865,6 +1861,7 @@ export default {
return;
}
APP.store.dispatch(updateRemoteParticipantFeatures(user));
logger.log(`USER ${id} connnected:`, user);
APP.UI.addUser(user);
});
@@ -2035,30 +2032,6 @@ export default {
JitsiConferenceEvents.LOCK_STATE_CHANGED,
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
APP.remoteControl.on(RemoteControlEvents.ACTIVE_CHANGED, isActive => {
room.setLocalParticipantProperty(
'remoteControlSessionStatus',
isActive
);
APP.UI.setLocalRemoteControlActiveChanged();
});
/* eslint-disable max-params */
room.on(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant, name, oldValue, newValue) => {
switch (name) {
case 'remoteControlSessionStatus':
APP.UI.setRemoteControlActiveStatus(
participant.getId(),
newValue);
break;
default:
// ignore
}
});
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(kickedOut(room, participant));
@@ -2403,25 +2376,6 @@ export default {
APP.UI.changeDisplayName('localVideoContainer', displayName);
},
/**
* Adds any room listener.
* @param {string} eventName one of the JitsiConferenceEvents
* @param {Function} listener the function to be called when the event
* occurs
*/
addConferenceListener(eventName, listener) {
room.on(eventName, listener);
},
/**
* Removes any room listener.
* @param {string} eventName one of the JitsiConferenceEvents
* @param {Function} listener the listener to be removed.
*/
removeConferenceListener(eventName, listener) {
room.off(eventName, listener);
},
/**
* Updates the list of current devices.
* @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
@@ -2704,7 +2658,7 @@ export default {
* requested
*/
hangup(requestFeedback = false) {
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
APP.store.dispatch(disableReceiver());
this._stopProxyConnection();
@@ -2721,7 +2675,6 @@ export default {
}
APP.UI.removeAllListeners();
APP.remoteControl.removeAllListeners();
let requestFeedbackPromise;
@@ -2904,29 +2857,6 @@ export default {
}
},
/**
* Returns the desktop sharing source id or undefined if the desktop sharing
* is not active at the moment.
*
* @returns {string|undefined} - The source id. If the track is not desktop
* track or the source id is not available, undefined will be returned.
*/
getDesktopSharingSourceId() {
return this.localVideo.sourceId;
},
/**
* Returns the desktop sharing source type or undefined if the desktop
* sharing is not active at the moment.
*
* @returns {'screen'|'window'|undefined} - The source type. If the track is
* not desktop track or the source type is not available, undefined will be
* returned.
*/
getDesktopSharingSourceType() {
return this.localVideo.sourceType;
},
/**
* Callback invoked by the external api create or update a direct connection
* from the local client to an external client.

View File

@@ -678,7 +678,6 @@ var config = {
forceJVB121Ratio
hiddenDomain
ignoreStartMuted
nick
startBitrate
*/

View File

@@ -28,9 +28,6 @@ body {
overflow: hidden;
color: $defaultColor;
background: $defaultBackground;
&.filmstrip-only {
background: transparent;
}
}
/**
@@ -70,16 +67,6 @@ body {
cursor: pointer;
}
/**
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
* preventing transparency in filmstrip-only mode. The selector chosen to
* override this behavior is specific to where the AtlasKitThemeProvider might
* be placed within the app hierarchy.
*/
.filmstrip-only #react > .ckAJgx {
background: transparent;
}
p {
margin: 0;
}
@@ -214,3 +201,74 @@ form {
background: rgba(0, 0, 0, .5);
border-radius: 4px;
}
.desktop-browser {
@media only screen and (max-width: $smallScreen) {
.watermark {
width: 20%;
height: 20%;
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.toolbox-button {
.toolbox-icon {
width: 28px;
height: 28px;
svg {
width: 18px;
height: 18px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 30px;
height: 30px;
}
}
}
}
}
}
}
@media only screen and (max-width: $verySmallScreen) {
#videoResolutionLabel {
display: none;
}
.vertical-filmstrip .filmstrip {
display: none;
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.settings-button-small-icon {
display: none;
}
.toolbox-button {
.toolbox-icon {
width: 18px;
height: 18px;
svg {
width: 12px;
height: 12px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 20px;
height: 20px;
}
}
}
}
}
}
.chrome-extension-banner {
display: none;
}
}
}

View File

@@ -27,84 +27,4 @@
font-size: 50px;
}
&-filmstrip-only {
background-color: $inlayFilmstripOnlyBg;
color: $inlayFilmstripOnlyColor;
margin-left: 20px;
margin-right: 20px;
margin-top: 20px;
bottom: 30px;
position: absolute;
display: flex;
max-height: 120px;
height: 80%;
right: 0px;
border-radius: 4px;
overflow: hidden;
&__content {
padding: 20px;
display: flex;
justify-content: center;
position: relative;
> .button-control {
align-self: center;
}
> #reloadProgressBar {
position: absolute;
left: 0px;
bottom: 0px;
margin-bottom: 0px;
width: 100%;
border-radius: 0px;
}
}
&__title {
font-size: 18px;
font-weight: 600;
}
&__container {
align-self: center;
}
&__text {
margin-top: 10px;
font-size: 14px;
font-weight: 600;
}
&__icon {
font-size: 50px;
align-self: center;
color: $inlayIconColor;
opacity: 0.6;
}
&__icon-container {
text-align: center;
display: flex;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
}
&__avatar-container {
height: 100%;
position: relative;
> img {
height: 100%;
}
}
&__icon-background {
background: $inlayIconBg;
opacity: 0.6;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
}
}
}

View File

@@ -1,136 +1,67 @@
@media only screen and (max-width: $smallScreen) {
.watermark {
width: 20%;
height: 20%;
}
@media only screen and (max-width: $verySmallScreen) {
.welcome {
display: block;
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.toolbox-button {
.toolbox-icon {
width: 28px;
height: 28px;
svg {
width: 18px;
height: 18px;
}
}
#enter_room {
position: relative;
height: 42px;
&:nth-child(2) {
.toolbox-icon {
width: 30px;
height: 30px;
}
}
.welcome-page-button {
font-size: 16px;
left: 0;
position: absolute;
top: 68px;
text-align: center;
width: 100%;
}
}
.header {
background-color: #002637;
#enter_room {
.enter-room-input-container {
padding-right: 0;
}
.warning-without-link,
.warning-with-link {
top: 120px;
}
}
}
}
}
@media only screen and (max-width: $verySmallScreen) {
.welcome {
display: block;
#enter_room {
position: relative;
height: 42px;
.welcome-page-button {
font-size: 16px;
left: 0;
position: absolute;
top: 68px;
text-align: center;
width: 100%;
.welcome-tabs {
display: none;
}
}
.header {
background-color: #002637;
#enter_room {
.enter-room-input-container {
padding-right: 0;
}
.warning-without-link,
.warning-with-link {
top: 120px;
}
}
}
.welcome-tabs {
display: none;
}
.header-text-title {
text-align: center;
}
.welcome-cards-container {
padding: 0;
}
&.without-content {
.header {
height: 100%;
.header-text-title {
text-align: center;
}
}
#moderated-meetings {
display: none;
}
.welcome-cards-container {
padding: 0;
}
.welcome-footer-row-block {
display: block;
}
.welcome-badge {
margin-right: 16px;
}
&.without-content {
.header {
height: 100%;
}
}
.welcome-footer {
display: none;
}
}
#moderated-meetings {
display: none;
}
#videoResolutionLabel {
display: none;
}
.desktop-browser {
.vertical-filmstrip .filmstrip {
.welcome-footer-row-block {
display: block;
}
.welcome-badge {
margin-right: 16px;
}
.welcome-footer {
display: none;
}
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.settings-button-small-icon {
display: none;
}
.toolbox-button {
.toolbox-icon {
width: 18px;
height: 18px;
svg {
width: 12px;
height: 12px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 20px;
height: 20px;
}
}
}
}
}
}
.chrome-extension-banner {
display: none;
}
}

View File

@@ -175,7 +175,7 @@ $welcomePageFontFamily: inherit;
$welcomePageBackground: none;
$welcomePageTitleColor: #fff;
$welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('/images/welcome-background.png');
$welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('../images/welcome-background.png');
$welcomePageHeaderBackgroundPosition: center;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: cover;

View File

@@ -224,6 +224,7 @@ body.welcome-page {
&.without-content {
.welcome-card {
min-width: 500px;
max-width: 580px;
}
}

View File

@@ -67,20 +67,6 @@
}
}
/**
* Style the filmstrip videos in filmstrip-only mode.
*/
&__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
.remote-videos-container {
transition: opacity 1s;
}

View File

@@ -145,26 +145,6 @@
}
}
/**
* Override other styles to support vertical filmstrip mode.
*/
.filmstrip-only .vertical-filmstrip {
.filmstrip {
flex-direction: row-reverse;
}
.filmstrip__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
height: 100%;
}
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: column-reverse. The remove videos in filmstrip should

View File

@@ -8,16 +8,10 @@
position: fixed;
z-index: $overlayZ;
background: $defaultBackground;
&.filmstrip-only {
@include transparentBg($filmstripOnlyOverlayBg, 0.8);
}
}
&__container-light {
@include transparentBg($defaultBackground, 0.7);
&.filmstrip-only {
@include transparentBg($filmstripOnlyOverlayBg, 0.2);
}
}
&__content {
@@ -27,11 +21,6 @@
width: 56%;
left: 50%;
@include transform(translateX(-50%));
&.filmstrip-only {
left: 0px;
width: 100%;
@include transform(none);
}
&_bottom {
position: absolute;

View File

@@ -41,7 +41,6 @@ $overlayButtonBg: #0074E0;
* Color variables
**/
$defaultBackground: #474747;
$filmstripOnlyOverlayBg: #000;
$reloadProgressBarBg: #0074E0;
/**
@@ -59,10 +58,6 @@ $dialogErrorText: #344563;
**/
$inlayColorBg: lighten($defaultBackground, 20%);
$inlayBorderColor: lighten($baseLight, 10%);
$inlayIconBg: #000;
$inlayIconColor: #fff;
$inlayFilmstripOnlyColor: #474747;
$inlayFilmstripOnlyBg: #fff;
// Main controls
$placeHolderColor: #a7a7a7;

View File

@@ -4,6 +4,5 @@ var config = {
muc: 'conference.jitsi.example.com', // FIXME: use XEP-0030
bridge: 'jitsi-videobridge.jitsi.example.com' // FIXME: use XEP-0030
},
useNicks: false,
bosh: '//jitsi.example.com/http-bind' // FIXME: use xep-0156 for that
};

View File

@@ -9,7 +9,6 @@ var config = {
muc: 'conference.'+subdomain+'jitsi.example.com', // FIXME: use XEP-0030
focus: 'focus.jitsi.example.com',
},
useNicks: false,
bosh: '//jitsi.example.com/http-bind', // FIXME: use xep-0156 for that
websocket: 'wss://jitsi.example.com/xmpp-websocket'
};

View File

@@ -13,9 +13,7 @@
height: 180,
parentNode: undefined,
configOverwrite: {},
interfaceConfigOverwrite: {
filmStripOnly: true
}
interfaceConfigOverwrite: {}
}
var api = new JitsiMeetExternalAPI(domain, options);
</script>

View File

@@ -97,11 +97,6 @@ var interfaceConfig = {
FILM_STRIP_MAX_HEIGHT: 120,
/**
* Whether to only show the filmstrip (and hide the toolbar).
*/
filmStripOnly: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
/**

View File

@@ -1,27 +1,48 @@
{
"en": "英語",
"af": "アフリカーンス語",
"az": "アゼルバイジャン語",
"ar": "アラビア語",
"bg": "ブルガリア語",
"ca": "カタルーニャ語",
"cs": "チェコ語",
"da": "デンマーク語",
"de": "ドイツ語",
"el": "ギリシア語",
"enGB": "英語 (英国)",
"eo": "エスペラント語",
"es": "スペイン語",
"esUS": "スペイン語 (ラテンアメリカ)",
"et": "エストニア語",
"eu": "バスク語",
"fi": "フィンランド語",
"fr": "フランス語",
"frCA": "フランス語 (カナダ)",
"he": "ヘブライ語",
"mr": "マラーティー語",
"hr": "クロアチア語",
"hu": "ハンガリー語",
"hy": "アルメニア語",
"id": "インドネシア語",
"it": "イタリア語",
"ja": "日本語",
"kab": "カビル語",
"ko": "韓国語",
"nb": "ノルウェー語 (ブークモール)",
"lt": "リトアニア語",
"nl": "オランダ語",
"oc": "オック語",
"pl": "ポーランド語",
"ptBR": "ポルトガル語 (ブラジル)",
"ru": "ロシア語",
"ro": "ルーマニア語",
"sc": "サルデーニャ語",
"sk": "スロバキア語",
"sl": "スロベニア語",
"sr": "セルビア語",
"sv": "スウェーデン語",
"th": "タイ語",
"tr": "トルコ語",
"uk": "ウクライナ語",
"vi": "ベトナム語",
"zhCN": "中国語 (中国)"
}
"zhCN": "中国語 (中国)",
"zhTW": "中国語 (台湾)"
}

File diff suppressed because it is too large Load Diff

View File

@@ -678,6 +678,7 @@
},
"startupoverlay": {
"policyText": " ",
"genericTitle": "The meeting needs to use your microphone and camera.",
"title": "{{app}} needs to use your microphone and camera."
},
"suspendedoverlay": {

View File

@@ -1,4 +1,4 @@
/* global APP, $, config, interfaceConfig */
/* global APP, $, config */
const UI = {};
@@ -59,14 +59,6 @@ UI.isFullScreen = function() {
return UIUtil.isFullScreen();
};
/**
* Returns true if the etherpad window is currently visible.
* @returns {Boolean} - true if the etherpad window is currently visible.
*/
UI.isEtherpadVisible = function() {
return Boolean(etherpadManager && etherpadManager.isVisible());
};
/**
* Returns true if there is a shared video which is being shown (?).
* @returns {boolean} - true if there is a shared video which is being shown.
@@ -143,9 +135,7 @@ UI.start = function() {
$.prompt.setDefaults({ persistent: false });
VideoLayout.init(eventEmitter);
if (!interfaceConfig.filmStripOnly) {
VideoLayout.initLargeVideo();
}
VideoLayout.initLargeVideo();
// Do not animate the video area on UI start (second argument passed into
// resizeVideoArea) because the animation is not visible anyway. Plus with
@@ -161,10 +151,7 @@ UI.start = function() {
$('body').addClass('desktop-browser');
}
if (interfaceConfig.filmStripOnly) {
$('body').addClass('filmstrip-only');
APP.store.dispatch(setNotificationsEnabled(false));
} else if (config.iAmRecorder) {
if (config.iAmRecorder) {
// in case of iAmSipGateway keep local video visible
if (!config.iAmSipGateway) {
VideoLayout.setLocalVideoVisible(false);
@@ -310,49 +297,6 @@ UI.toggleFilmstrip = function() {
*/
UI.toggleChat = () => APP.store.dispatch(toggleChat());
/**
* Handle new user display name.
*/
UI.inputDisplayNameHandler = function(newDisplayName) {
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
};
// FIXME check if someone user this
UI.showLoginPopup = function(callback) {
logger.log('password is required');
const message
= `<input name="username" type="text"
placeholder="user@domain.net"
class="input-control" autofocus>
<input name="password" type="password"
data-i18n="[placeholder]dialog.userPassword"
class="input-control"
placeholder="user password">`
;
// eslint-disable-next-line max-params
const submitFunction = (e, v, m, f) => {
if (v && f.username && f.password) {
callback(f.username, f.password);
}
};
messageHandler.openTwoButtonDialog({
titleKey: 'dialog.passwordRequired',
msgString: message,
leftButtonKey: 'dialog.Ok',
submitFunction,
focus: ':input:first'
});
};
UI.askForNickname = function() {
// eslint-disable-next-line no-alert
return window.prompt('Your nickname (optional)');
};
/**
* Sets muted audio state for participant
*/
@@ -509,14 +453,6 @@ UI.notifyTokenAuthFailed = function() {
});
};
UI.notifyInternalError = function(error) {
messageHandler.showError({
descriptionArguments: { error },
descriptionKey: 'dialog.internalError',
titleKey: 'dialog.internalErrorTitle'
});
};
UI.notifyFocusDisconnected = function(focus, retrySec) {
messageHandler.participantNotification(
null, 'notify.focus',
@@ -526,16 +462,6 @@ UI.notifyFocusDisconnected = function(focus, retrySec) {
);
};
/**
* Notifies interested listeners that the raise hand property has changed.
*
* @param {boolean} isRaisedHand indicates the current state of the
* "raised hand"
*/
UI.onLocalRaiseHandChanged = function(isRaisedHand) {
eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
};
/**
* Update list of available physical devices.
*/
@@ -595,38 +521,6 @@ UI.onSharedVideoStop = function(id, attributes) {
}
};
/**
* Handles user's features changes.
*/
UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
/**
* Returns the number of known remote videos.
*
* @returns {number} The number of remote videos.
*/
UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
/**
* Sets the remote control active status for a remote participant.
*
* @param {string} participantID - The id of the remote participant.
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
UI.setRemoteControlActiveStatus = function(participantID, isActive) {
VideoLayout.setRemoteControlActiveStatus(participantID, isActive);
};
/**
* Sets the remote control active status for the local participant.
*
* @returns {void}
*/
UI.setLocalRemoteControlActiveChanged = function() {
VideoLayout.setLocalRemoteControlActiveChanged();
};
// TODO: Export every function separately. For now there is no point of doing
// this because we are importing everything.
export default UI;

View File

@@ -6,48 +6,6 @@ import UIUtil from '../util/UIUtil';
* Responsible for drawing audio levels.
*/
const AudioLevels = {
/**
* Fills the dot(s) with the specified "index", with as much opacity as
* indicated by "opacity".
*
* @param {string} elementID the parent audio indicator span element
* @param {number} index the index of the dots to fill, where 0 indicates
* the middle dot and the following increments point toward the
* corresponding pair of dots.
* @param {number} opacity the opacity to set for the specified dot.
*/
_setDotLevel(elementID, index, opacity) {
let audioSpan
= document.getElementById(elementID)
.getElementsByClassName('audioindicator');
// Make sure the audio span is still around.
if (audioSpan && audioSpan.length > 0) {
audioSpan = audioSpan[0];
} else {
return;
}
const audioTopDots
= audioSpan.getElementsByClassName('audiodot-top');
const audioDotMiddle
= audioSpan.getElementsByClassName('audiodot-middle');
const audioBottomDots
= audioSpan.getElementsByClassName('audiodot-bottom');
// First take care of the middle dot case.
if (index === 0) {
audioDotMiddle[0].style.opacity = opacity;
return;
}
// Index > 0 : we are setting non-middle dots.
index--;// eslint-disable-line no-param-reassign
audioBottomDots[index].style.opacity = opacity;
audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
},
/**
* Updates the audio level of the large video.
*

View File

@@ -12,19 +12,10 @@ import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../../react/features/base/media';
import {
getParticipantById,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import { isRemoteTrackMuted } from '../../../react/features/base/tracks';
import { getParticipantById } from '../../../react/features/base/participants';
import { PresenceLabel } from '../../../react/features/presence-status';
import {
REMOTE_CONTROL_MENU_STATES,
RemoteVideoMenuTriggerButton
} from '../../../react/features/remote-video-menu';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
/* eslint-enable no-unused-vars */
import UIUtils from '../util/UIUtil';
@@ -81,7 +72,6 @@ export default class RemoteVideo extends SmallVideo {
this.videoSpanId = `participant_${this.id}`;
this._audioStreamElement = null;
this._supportsRemoteControl = false;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
this.addRemoteVideoContainer();
this.updateIndicators();
@@ -89,7 +79,6 @@ export default class RemoteVideo extends SmallVideo {
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
this._isRemoteControlSessionActive = false;
/**
* The flag is set to <tt>true</tt> after the 'canplay' event has been
@@ -103,10 +92,7 @@ export default class RemoteVideo extends SmallVideo {
// Bind event handlers so they are only bound once for every instance.
// TODO The event handlers should be turned into actions so changes can be
// handled through reducers and middleware.
this._requestRemoteControlPermissions
= this._requestRemoteControlPermissions.bind(this);
this._setAudioVolume = this._setAudioVolume.bind(this);
this._stopRemoteControl = this._stopRemoteControl.bind(this);
this.container.onclick = this._onContainerClick;
}
@@ -135,10 +121,6 @@ export default class RemoteVideo extends SmallVideo {
* @private
*/
_generatePopupContent() {
if (interfaceConfig.filmStripOnly) {
return;
}
const remoteVideoMenuContainer
= this.container.querySelector('.remotevideomenu');
@@ -146,40 +128,11 @@ export default class RemoteVideo extends SmallVideo {
return;
}
const { controller } = APP.remoteControl;
let remoteControlState = null;
let onRemoteControlToggle;
if (this._supportsRemoteControl
&& ((!APP.remoteControl.active && !this._isRemoteControlSessionActive)
|| APP.remoteControl.controller.activeParticipant === this.id)) {
if (controller.getRequestedParticipant() === this.id) {
remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
} else if (controller.isStarted()) {
onRemoteControlToggle = this._stopRemoteControl;
remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
} else {
onRemoteControlToggle = this._requestRemoteControlPermissions;
remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
}
}
const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
// hide volume when in silent mode
const onVolumeChange
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
const participantID = this.id;
const currentLayout = getCurrentLayout(APP.store.getState());
let remoteMenuPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
remoteMenuPosition = 'left top';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
remoteMenuPosition = 'left bottom';
} else {
remoteMenuPosition = 'top center';
}
ReactDOM.render(
<Provider store = { APP.store }>
@@ -187,13 +140,10 @@ export default class RemoteVideo extends SmallVideo {
<AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { initialVolumeValue }
menuPosition = { remoteMenuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
onVolumeChange = { onVolumeChange }
participantID = { participantID }
remoteControlState = { remoteControlState } />
participantID = { this.id } />
</AtlasKitThemeProvider>
</I18nextProvider>
</Provider>,
@@ -207,76 +157,6 @@ export default class RemoteVideo extends SmallVideo {
this.updateRemoteVideoMenu();
}
/**
* Sets the remote control active status for the remote video.
*
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
setRemoteControlActiveStatus(isActive) {
this._isRemoteControlSessionActive = isActive;
this.updateRemoteVideoMenu();
}
/**
* Sets the remote control supported value and initializes or updates the menu
* depending on the remote control is supported or not.
* @param {boolean} isSupported
*/
setRemoteControlSupport(isSupported = false) {
if (this._supportsRemoteControl === isSupported) {
return;
}
this._supportsRemoteControl = isSupported;
this.updateRemoteVideoMenu();
}
/**
* Requests permissions for remote control session.
*/
_requestRemoteControlPermissions() {
APP.remoteControl.controller.requestPermissions(this.id, this.VideoLayout.getLargeVideoWrapper())
.then(result => {
if (result === null) {
return;
}
this.updateRemoteVideoMenu();
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
result === false ? 'dialog.remoteControlDeniedMessage' : 'dialog.remoteControlAllowedMessage',
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
);
if (result === true) {
// the remote control permissions has been granted
// pin the controlled participant
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
const pinnedId = pinnedParticipant.id;
if (pinnedId !== this.id) {
APP.store.dispatch(pinParticipant(this.id));
}
}
}, error => {
logger.error(error);
this.updateRemoteVideoMenu();
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.remoteControlErrorMessage',
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
);
});
this.updateRemoteVideoMenu();
}
/**
* Stops remote control session.
*/
_stopRemoteControl() {
// send message about stopping
APP.remoteControl.controller.stop();
this.updateRemoteVideoMenu();
}
/**
* Change the remote participant's volume level.
*

View File

@@ -699,7 +699,7 @@ export default class SmallVideo {
alwaysVisible = { showConnectionIndicator }
iconSize = { iconSize }
isLocalVideo = { this.isLocal }
enableStatsDisplay = { !interfaceConfig.filmStripOnly }
enableStatsDisplay = { true }
participantId = { this.id }
statsPopoverPosition = { statsPopoverPosition } />
: null }

View File

@@ -1,4 +1,4 @@
/* global APP, $, interfaceConfig */
/* global APP, $ */
import Logger from 'jitsi-meet-logger';
@@ -264,10 +264,6 @@ const VideoLayout = {
* @returns {void}
*/
onPinChange(pinnedParticipantID) {
if (interfaceConfig.filmStripOnly) {
return;
}
getAllThumbnails().forEach(thumbnail =>
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
},
@@ -297,7 +293,6 @@ const VideoLayout = {
const jitsiParticipant = APP.conference.getParticipantById(id);
const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
this._setRemoteControlProperties(jitsiParticipant, remoteVideo);
this.addRemoteVideoContainer(id, remoteVideo);
this.updateMutedForNoTracks(id, 'audio');
@@ -658,33 +653,6 @@ const VideoLayout = {
this.localFlipX = val;
},
/**
* Handles user's features changes.
*/
onUserFeaturesChanged(user) {
const video = this.getSmallVideo(user.getId());
if (!video) {
return;
}
this._setRemoteControlProperties(user, video);
},
/**
* Sets the remote control properties (checks whether remote control
* is supported and executes remoteVideo.setRemoteControlSupport).
* @param {JitsiParticipant} user the user that will be checked for remote
* control support.
* @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
* will be set.
*/
_setRemoteControlProperties(user, remoteVideo) {
APP.remoteControl.checkUserRemoteControlSupport(user)
.then(result => remoteVideo.setRemoteControlSupport(result))
.catch(error =>
logger.warn(`could not get remote control properties for: ${user.getJid()}`, error));
},
/**
* Returns the wrapper jquery selector for the largeVideo
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
@@ -702,28 +670,6 @@ const VideoLayout = {
return Object.keys(remoteVideos).length;
},
/**
* Sets the remote control active status for a remote participant.
*
* @param {string} participantID - The id of the remote participant.
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
setRemoteControlActiveStatus(participantID, isActive) {
remoteVideos[participantID].setRemoteControlActiveStatus(isActive);
},
/**
* Sets the remote control active status for the local participant.
*
* @returns {void}
*/
setLocalRemoteControlActiveChanged() {
Object.values(remoteVideos).forEach(
remoteVideo => remoteVideo.updateRemoteVideoMenu()
);
},
/**
* Helper method to invoke when the video layout has changed and elements
* have to be re-arranged and resized.

View File

@@ -1,4 +1,4 @@
/* global APP, $, interfaceConfig */
/* global APP, $ */
import Logger from 'jitsi-meet-logger';
@@ -203,14 +203,12 @@ const KeyboardShortcut = {
});
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
if (!interfaceConfig.filmStripOnly) {
this.registerShortcut('T', null, () => {
sendAnalytics(createShortcutEvent('speaker.stats'));
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, 'keyboardShortcuts.showSpeakerStats');
}
this.registerShortcut('T', null, () => {
sendAnalytics(createShortcutEvent('speaker.stats'));
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, 'keyboardShortcuts.showSpeakerStats');
/**
* FIXME: Currently focus keys are directly implemented below in

View File

@@ -1,3 +0,0 @@
module.exports = {
'extends': '../../react/.eslintrc.js'
};

View File

@@ -1,474 +0,0 @@
/* @flow */
import { getLogger } from 'jitsi-meet-logger';
import {
JitsiConferenceEvents
} from '../../react/features/base/lib-jitsi-meet';
import UIEvents from '../../service/UI/UIEvents';
import {
EVENTS,
PERMISSIONS_ACTIONS,
REMOTE_CONTROL_MESSAGE_NAME
} from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
from '../../service/remotecontrol/RemoteControlEvents';
import * as KeyCodes from '../keycode/keycode';
import RemoteControlParticipant from './RemoteControlParticipant';
declare var $: Function;
declare var APP: Object;
const logger = getLogger(__filename);
/**
* Extract the keyboard key from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {KEYS} The key that is pressed or undefined.
*/
function getKey(event) {
return KeyCodes.keyboardEventToKey(event);
}
/**
* Extract the modifiers from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {Array} With possible values: "shift", "control", "alt", "command".
*/
function getModifiers(event) {
const modifiers = [];
if (event.shiftKey) {
modifiers.push('shift');
}
if (event.ctrlKey) {
modifiers.push('control');
}
if (event.altKey) {
modifiers.push('alt');
}
if (event.metaKey) {
modifiers.push('command');
}
return modifiers;
}
/**
* This class represents the controller party for a remote controller session.
* It listens for mouse and keyboard events and sends them to the receiver
* party of the remote control session.
*/
export default class Controller extends RemoteControlParticipant {
_area: ?Object;
_controlledParticipant: string | null;
_isCollectingEvents: boolean;
_largeVideoChangedListener: Function;
_requestedParticipant: string | null;
_stopListener: Function;
_userLeftListener: Function;
/**
* Creates new instance.
*/
constructor() {
super();
this._isCollectingEvents = false;
this._controlledParticipant = null;
this._requestedParticipant = null;
this._stopListener = this._handleRemoteControlStoppedEvent.bind(this);
this._userLeftListener = this._onUserLeft.bind(this);
this._largeVideoChangedListener
= this._onLargeVideoIdChanged.bind(this);
}
/**
* Returns the current active participant's id.
*
* @returns {string|null} - The id of the current active participant.
*/
get activeParticipant(): string | null {
return this._requestedParticipant || this._controlledParticipant;
}
/**
* Requests permissions from the remote control receiver side.
*
* @param {string} userId - The user id of the participant that will be
* requested.
* @param {JQuerySelector} eventCaptureArea - The area that is going to be
* used mouse and keyboard event capture.
* @returns {Promise<boolean>} Resolve values - true(accept), false(deny),
* null(the participant has left).
*/
requestPermissions(
userId: string,
eventCaptureArea: Object
): Promise<boolean | null> {
if (!this._enabled) {
return Promise.reject(new Error('Remote control is disabled!'));
}
this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
this._area = eventCaptureArea;// $("#largeVideoWrapper")
logger.log(`Requsting remote control permissions from: ${userId}`);
return new Promise((resolve, reject) => {
// eslint-disable-next-line prefer-const
let onUserLeft, permissionsReplyListener;
const clearRequest = () => {
this._requestedParticipant = null;
APP.conference.removeConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
permissionsReplyListener);
APP.conference.removeConferenceListener(
JitsiConferenceEvents.USER_LEFT,
onUserLeft);
};
permissionsReplyListener = (participant, event) => {
let result = null;
try {
result = this._handleReply(participant, event);
} catch (e) {
clearRequest();
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
reject(e);
}
if (result !== null) {
clearRequest();
if (result === false) {
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
}
resolve(result);
}
};
onUserLeft = id => {
if (id === this._requestedParticipant) {
clearRequest();
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
resolve(null);
}
};
APP.conference.addConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
permissionsReplyListener);
APP.conference.addConferenceListener(
JitsiConferenceEvents.USER_LEFT,
onUserLeft);
this._requestedParticipant = userId;
this.sendRemoteControlEndpointMessage(
userId,
{
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.request
},
e => {
clearRequest();
reject(e);
});
});
}
/**
* Handles the reply of the permissions request.
*
* @param {JitsiParticipant} participant - The participant that has sent the
* reply.
* @param {RemoteControlEvent} event - The remote control event.
* @returns {boolean|null}
*/
_handleReply(participant: Object, event: Object) {
const userId = participant.getId();
if (this._enabled
&& event.name === REMOTE_CONTROL_MESSAGE_NAME
&& event.type === EVENTS.permissions
&& userId === this._requestedParticipant) {
if (event.action !== PERMISSIONS_ACTIONS.grant) {
this._area = undefined;
}
switch (event.action) {
case PERMISSIONS_ACTIONS.grant: {
this._controlledParticipant = userId;
logger.log('Remote control permissions granted to:', userId);
this._start();
return true;
}
case PERMISSIONS_ACTIONS.deny:
return false;
case PERMISSIONS_ACTIONS.error:
throw new Error('Error occurred on receiver side');
default:
throw new Error('Unknown reply received!');
}
} else {
// different message type or another user -> ignoring the message
return null;
}
}
/**
* Handles remote control stopped.
*
* @param {JitsiParticipant} participant - The participant that has sent the
* event.
* @param {Object} event - EndpointMessage event from the data channels.
* @property {string} type - The function process only events with
* name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {void}
*/
_handleRemoteControlStoppedEvent(participant: Object, event: Object) {
if (this._enabled
&& event.name === REMOTE_CONTROL_MESSAGE_NAME
&& event.type === EVENTS.stop
&& participant.getId() === this._controlledParticipant) {
this._stop();
}
}
/**
* Starts processing the mouse and keyboard events. Sets conference
* listeners. Disables keyboard events.
*
* @returns {void}
*/
_start() {
logger.log('Starting remote control controller.');
APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
this._largeVideoChangedListener);
APP.conference.addConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._stopListener);
APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
this.resume();
}
/**
* Disables the keyboatd shortcuts. Starts collecting remote control
* events. It can be used to resume an active remote control session wchich
* was paused with this.pause().
*
* @returns {void}
*/
resume() {
let area;
if (!this._enabled
|| this._isCollectingEvents
|| !(area = this._area)) {
return;
}
logger.log('Resuming remote control controller.');
this._isCollectingEvents = true;
APP.keyboardshortcut.enable(false);
area.mousemove(event => {
const area = this._area; // eslint-disable-line no-shadow
if (!area) {
return;
}
const position = area.position();
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type: EVENTS.mousemove,
x: (event.pageX - position.left) / area.width(),
y: (event.pageY - position.top) / area.height()
});
});
area.mousedown(this._onMouseClickHandler.bind(this, EVENTS.mousedown));
area.mouseup(this._onMouseClickHandler.bind(this, EVENTS.mouseup));
area.dblclick(
this._onMouseClickHandler.bind(this, EVENTS.mousedblclick));
area.contextmenu(() => false);
area[0].onmousewheel = event => {
event.preventDefault();
event.stopPropagation();
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type: EVENTS.mousescroll,
x: event.deltaX,
y: event.deltaY
});
return false;
};
$(window).keydown(this._onKeyPessHandler.bind(this,
EVENTS.keydown));
$(window).keyup(this._onKeyPessHandler.bind(this, EVENTS.keyup));
}
/**
* Stops processing the mouse and keyboard events. Removes added listeners.
* Enables the keyboard shortcuts. Displays dialog to notify the user that
* remote control session has ended.
*
* @returns {void}
*/
_stop() {
if (!this._controlledParticipant) {
return;
}
logger.log('Stopping remote control controller.');
APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
this._largeVideoChangedListener);
APP.conference.removeConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._stopListener);
APP.conference.removeConferenceListener(JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
this.pause();
this._controlledParticipant = null;
this._area = undefined;
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.remoteControlStopMessage'
);
}
/**
* Executes this._stop() mehtod which stops processing the mouse and
* keyboard events, removes added listeners, enables the keyboard shortcuts,
* displays dialog to notify the user that remote control session has ended.
* In addition sends stop message to the controlled participant.
*
* @returns {void}
*/
stop() {
if (!this._controlledParticipant) {
return;
}
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type: EVENTS.stop
});
this._stop();
}
/**
* Pauses the collecting of events and enables the keyboard shortcus. But
* it doesn't removes any other listeners. Basically the remote control
* session will be still active after this.pause(), but no events from the
* controller side will be captured and sent. You can resume the collecting
* of the events with this.resume().
*
* @returns {void}
*/
pause() {
if (!this._controlledParticipant) {
return;
}
logger.log('Pausing remote control controller.');
this._isCollectingEvents = false;
APP.keyboardshortcut.enable(true);
const area = this._area;
if (area) {
area.off('contextmenu');
area.off('dblclick');
area.off('mousedown');
area.off('mousemove');
area.off('mouseup');
area[0].onmousewheel = undefined;
}
$(window).off('keydown');
$(window).off('keyup');
}
/**
* Handler for mouse click events.
*
* @param {string} type - The type of event ("mousedown"/"mouseup").
* @param {Event} event - The mouse event.
* @returns {void}
*/
_onMouseClickHandler(type: string, event: Object) {
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type,
button: event.which
});
}
/**
* Returns true if the remote control session is started.
*
* @returns {boolean}
*/
isStarted() {
return this._controlledParticipant !== null;
}
/**
* Returns the id of the requested participant.
*
* @returns {string} The id of the requested participant.
* NOTE: This id should be the result of JitsiParticipant.getId() call.
*/
getRequestedParticipant() {
return this._requestedParticipant;
}
/**
* Handler for key press events.
*
* @param {string} type - The type of event ("keydown"/"keyup").
* @param {Event} event - The key event.
* @returns {void}
*/
_onKeyPessHandler(type: string, event: Object) {
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
type,
key: getKey(event),
modifiers: getModifiers(event)
});
}
/**
* Calls the stop method if the other side have left.
*
* @param {string} id - The user id for the participant that have left.
* @returns {void}
*/
_onUserLeft(id: string) {
if (this._controlledParticipant === id) {
this._stop();
}
}
/**
* Handles changes of the participant displayed on the large video.
*
* @param {string} id - The user id for the participant that is displayed.
* @returns {void}
*/
_onLargeVideoIdChanged(id: string) {
if (!this._controlledParticipant) {
return;
}
if (this._controlledParticipant === id) {
this.resume();
} else {
this.pause();
}
}
}

View File

@@ -1,331 +0,0 @@
/* @flow */
import { getLogger } from 'jitsi-meet-logger';
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
import {
JitsiConferenceEvents
} from '../../react/features/base/lib-jitsi-meet';
import {
openRemoteControlAuthorizationDialog
} from '../../react/features/remote-control';
import {
DISCO_REMOTE_CONTROL_FEATURE,
EVENTS,
PERMISSIONS_ACTIONS,
REMOTE_CONTROL_MESSAGE_NAME,
REQUESTS
} from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
from '../../service/remotecontrol/RemoteControlEvents';
import { Transport, PostMessageTransportBackend } from '../transport';
import RemoteControlParticipant from './RemoteControlParticipant';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
const logger = getLogger(__filename);
/**
* The transport instance used for communication with external apps.
*
* @type {Transport}
*/
const transport = new Transport({
backend: new PostMessageTransportBackend({
postisOptions: { scope: 'jitsi-remote-control' }
})
});
/**
* This class represents the receiver party for a remote controller session.
* It handles "remote-control-event" events and sends them to the
* API module. From there the events can be received from wrapper application
* and executed.
*/
export default class Receiver extends RemoteControlParticipant {
_controller: ?string;
_enabled: boolean;
_hangupListener: Function;
_remoteControlEventsListener: Function;
_userLeftListener: Function;
/**
* Creates new instance.
*/
constructor() {
super();
this._controller = null;
this._remoteControlEventsListener
= this._onRemoteControlMessage.bind(this);
this._userLeftListener = this._onUserLeft.bind(this);
this._hangupListener = this._onHangup.bind(this);
// We expect here that even if we receive the supported event earlier
// it will be cached and we'll receive it.
transport.on('event', event => {
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
this._onRemoteControlAPIEvent(event);
return true;
}
return false;
});
}
/**
* Enables / Disables the remote control.
*
* @param {boolean} enabled - The new state.
* @returns {void}
*/
_enable(enabled: boolean) {
if (this._enabled === enabled) {
return;
}
this._enabled = enabled;
if (enabled === true) {
logger.log('Remote control receiver enabled.');
// Announce remote control support.
APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
APP.conference.addConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._remoteControlEventsListener);
APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
this._hangupListener);
} else {
logger.log('Remote control receiver disabled.');
this._stop(true);
APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
APP.conference.removeConferenceListener(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._remoteControlEventsListener);
APP.conference.removeListener(
JitsiMeetConferenceEvents.BEFORE_HANGUP,
this._hangupListener);
}
}
/**
* Removes the listener for JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED
* events. Sends stop message to the wrapper application. Optionally
* displays dialog for informing the user that remote control session
* ended.
*
* @param {boolean} [dontNotify] - If true - a notification about stopping
* the remote control won't be displayed.
* @returns {void}
*/
_stop(dontNotify: boolean = false) {
if (!this._controller) {
return;
}
logger.log('Remote control receiver stop.');
this._controller = null;
APP.conference.removeConferenceListener(
JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
transport.sendEvent({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: EVENTS.stop
});
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
if (!dontNotify) {
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.remoteControlStopMessage'
);
}
}
/**
* Calls this._stop() and sends stop message to the controller participant.
*
* @returns {void}
*/
stop() {
if (!this._controller) {
return;
}
this.sendRemoteControlEndpointMessage(this._controller, {
type: EVENTS.stop
});
this._stop();
}
/**
* Listens for data channel EndpointMessage. Handles only remote control
* messages. Sends the remote control messages to the external app that
* will execute them.
*
* @param {JitsiParticipant} participant - The controller participant.
* @param {Object} message - EndpointMessage from the data channels.
* @param {string} message.name - The function processes only messages with
* name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {void}
*/
_onRemoteControlMessage(participant: Object, message: Object) {
if (message.name !== REMOTE_CONTROL_MESSAGE_NAME) {
return;
}
if (this._enabled) {
if (this._controller === null
&& message.type === EVENTS.permissions
&& message.action === PERMISSIONS_ACTIONS.request) {
const userId = participant.getId();
this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
APP.store.dispatch(
openRemoteControlAuthorizationDialog(userId));
} else if (this._controller === participant.getId()) {
if (message.type === EVENTS.stop) {
this._stop();
} else { // forward the message
transport.sendEvent(message);
}
} // else ignore
} else {
logger.log('Remote control message is ignored because remote '
+ 'control is disabled', message);
}
}
/**
* Denies remote control access for user associated with the passed user id.
*
* @param {string} userId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {void}
*/
deny(userId: string) {
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
this.sendRemoteControlEndpointMessage(userId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.deny
});
}
/**
* Grants remote control access to user associated with the passed user id.
*
* @param {string} userId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {void}
*/
grant(userId: string) {
APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
this._userLeftListener);
this._controller = userId;
logger.log(`Remote control permissions granted to: ${userId}`);
let promise;
if (APP.conference.isSharingScreen
&& APP.conference.getDesktopSharingSourceType() === 'screen') {
promise = this._sendStartRequest();
} else {
promise = APP.conference.toggleScreenSharing(
true,
{
desktopSharingSources: [ 'screen' ]
})
.then(() => this._sendStartRequest());
}
promise
.then(() =>
this.sendRemoteControlEndpointMessage(userId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.grant
})
)
.catch(error => {
logger.error(error);
this.sendRemoteControlEndpointMessage(userId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.error
});
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.startRemoteControlErrorMessage'
);
this._stop(true);
});
}
/**
* Sends remote control start request.
*
* @returns {Promise}
*/
_sendStartRequest() {
return transport.sendRequest({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: REQUESTS.start,
sourceId: APP.conference.getDesktopSharingSourceId()
});
}
/**
* Handles remote control events from the external app. Currently only
* events with type EVENTS.supported and EVENTS.stop are
* supported.
*
* @param {RemoteControlEvent} event - The remote control event.
* @returns {void}
*/
_onRemoteControlAPIEvent(event: Object) {
switch (event.type) {
case EVENTS.supported:
this._onRemoteControlSupported();
break;
case EVENTS.stop:
this.stop();
break;
}
}
/**
* Handles events for support for executing remote control events into
* the wrapper application.
*
* @returns {void}
*/
_onRemoteControlSupported() {
logger.log('Remote Control supported.');
if (config.disableRemoteControl) {
logger.log('Remote Control disabled.');
} else {
this._enable(true);
}
}
/**
* Calls the stop method if the other side have left.
*
* @param {string} id - The user id for the participant that have left.
* @returns {void}
*/
_onUserLeft(id: string) {
if (this._controller === id) {
this._stop();
}
}
/**
* Handles hangup events. Disables the receiver.
*
* @returns {void}
*/
_onHangup() {
this._enable(false);
}
}

View File

@@ -1,98 +0,0 @@
/* @flow */
import EventEmitter from 'events';
import { getLogger } from 'jitsi-meet-logger';
import JitsiMeetJS from '../../react/features/base/lib-jitsi-meet';
import { DISCO_REMOTE_CONTROL_FEATURE }
from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
from '../../service/remotecontrol/RemoteControlEvents';
import Controller from './Controller';
import Receiver from './Receiver';
const logger = getLogger(__filename);
declare var APP: Object;
declare var config: Object;
/**
* Implements the remote control functionality.
*/
class RemoteControl extends EventEmitter {
_active: boolean;
_initialized: boolean;
controller: Controller;
receiver: Receiver;
/**
* Constructs new instance. Creates controller and receiver properties.
*/
constructor() {
super();
this.controller = new Controller();
this._active = false;
this._initialized = false;
this.controller.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
this.active = active;
});
}
/**
* Sets the remote control session active status.
*
* @param {boolean} isActive - True - if the controller or the receiver is
* currently in remote control session and false otherwise.
* @returns {void}
*/
set active(isActive: boolean) {
this._active = isActive;
this.emit(RemoteControlEvents.ACTIVE_CHANGED, isActive);
}
/**
* Returns the remote control session active status.
*
* @returns {boolean} - True - if the controller or the receiver is
* currently in remote control session and false otherwise.
*/
get active(): boolean {
return this._active;
}
/**
* Initializes the remote control - checks if the remote control should be
* enabled or not.
*
* @returns {void}
*/
init() {
if (config.disableRemoteControl || this._initialized || !JitsiMeetJS.isDesktopSharingEnabled()) {
return;
}
logger.log('Initializing remote control.');
this._initialized = true;
this.controller.enable(true);
this.receiver = new Receiver();
this.receiver.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
this.active = active;
});
}
/**
* Checks whether the passed user supports remote control or not.
*
* @param {JitsiParticipant} user - The user to be tested.
* @returns {Promise<boolean>} The promise will be resolved with true if
* the user supports remote control and with false if not.
*/
checkUserRemoteControlSupport(user: Object) {
return user.getFeatures()
.then(features => features.has(DISCO_REMOTE_CONTROL_FEATURE));
}
}
export default new RemoteControl();

View File

@@ -1,72 +0,0 @@
/* @flow */
import EventEmitter from 'events';
import { getLogger } from 'jitsi-meet-logger';
import {
REMOTE_CONTROL_MESSAGE_NAME
} from '../../service/remotecontrol/Constants';
const logger = getLogger(__filename);
declare var APP: Object;
/**
* Implements common logic for Receiver class and Controller class.
*/
export default class RemoteControlParticipant extends EventEmitter {
_enabled: boolean;
/**
* Creates new instance.
*/
constructor() {
super();
this._enabled = false;
}
/**
* Enables / Disables the remote control.
*
* @param {boolean} enabled - The new state.
* @returns {void}
*/
enable(enabled: boolean) {
this._enabled = enabled;
}
/**
* Sends remote control message to other participant trough data channel.
*
* @param {string} to - The participant who will receive the event.
* @param {RemoteControlEvent} event - The remote control event.
* @param {Function} onDataChannelFail - Handler for data channel failure.
* @returns {void}
*/
sendRemoteControlEndpointMessage(
to: ?string,
event: Object,
onDataChannelFail: ?Function) {
if (!this._enabled || !to) {
logger.warn(
'Remote control: Skip sending remote control event. Params:',
this.enable,
to);
return;
}
try {
APP.conference.sendEndpointMessage(to, {
name: REMOTE_CONTROL_MESSAGE_NAME,
...event
});
} catch (e) {
logger.error(
'Failed to send EndpointMessage via the datachannels',
e);
if (typeof onDataChannelFail === 'function') {
onDataChannelFail(e);
}
}
}
}

4
package-lock.json generated
View File

@@ -10771,8 +10771,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#301e3a22dd76341b15d3846ab539e713d8e7569b",
"from": "github:jitsi/lib-jitsi-meet#301e3a22dd76341b15d3846ab539e713d8e7569b",
"version": "github:jitsi/lib-jitsi-meet#d2153eb404ddadef6d5b89ae8c499fa144280531",
"from": "github:jitsi/lib-jitsi-meet#d2153eb404ddadef6d5b89ae8c499fa144280531",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#301e3a22dd76341b15d3846ab539e713d8e7569b",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#d2153eb404ddadef6d5b89ae8c499fa144280531",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",

View File

@@ -37,6 +37,7 @@ import '../overlay/middleware';
import '../recent-list/middleware';
import '../recording/middleware';
import '../rejoin/middleware';
import '../remote-control/middleware';
import '../room-lock/middleware';
import '../rtcstats/middleware';
import '../subtitles/middleware';

View File

@@ -43,6 +43,7 @@ import '../notifications/reducer';
import '../overlay/reducer';
import '../recent-list/reducer';
import '../recording/reducer';
import '../remote-control/reducer';
import '../settings/reducer';
import '../subtitles/reducer';
import '../toolbox/reducer';

View File

@@ -128,7 +128,6 @@ export default [
'localRecording',
'maxFullResolutionParticipants',
'minParticipants',
'nick',
'openBridgeChannel',
'opusMaxAverageBitrate',
'p2p',

View File

@@ -54,6 +54,5 @@ export default [
'UNSUPPORTED_BROWSERS',
'VERTICAL_FILMSTRIP',
'VIDEO_LAYOUT_FIT',
'VIDEO_QUALITY_LABEL_DISABLED',
'filmStripOnly'
'VIDEO_QUALITY_LABEL_DISABLED'
];

View File

@@ -16,12 +16,15 @@ import {
PIN_PARTICIPANT,
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import { DISCO_REMOTE_CONTROL_FEATURE } from './constants';
import {
getLocalParticipant,
getNormalizedDisplayName,
getParticipantDisplayName,
figureOutMutedWhileDisconnectedStatus
figureOutMutedWhileDisconnectedStatus,
getParticipantById
} from './functions';
import logger from './logger';
/**
* Create an action for when dominant speaker changes.
@@ -276,6 +279,48 @@ export function participantJoined(participant) {
};
}
/**
* Updates the features of a remote participant.
*
* @param {JitsiParticipant} jitsiParticipant - The ID of the participant.
* @returns {{
* type: PARTICIPANT_UPDATED,
* participant: Participant
* }}
*/
export function updateRemoteParticipantFeatures(jitsiParticipant) {
return (dispatch, getState) => {
if (!jitsiParticipant) {
return;
}
const id = jitsiParticipant.getId();
jitsiParticipant.getFeatures()
.then(features => {
const supportsRemoteControl = features.has(DISCO_REMOTE_CONTROL_FEATURE);
const participant = getParticipantById(getState(), id);
if (!participant || participant.local) {
return;
}
if (participant?.supportsRemoteControl !== supportsRemoteControl) {
return dispatch({
type: PARTICIPANT_UPDATED,
participant: {
id,
supportsRemoteControl
}
});
}
})
.catch(error => {
logger.error(`Failed to get participant features for ${id}!`, error);
});
};
}
/**
* Action to signal that a hidden participant has joined the conference.
*
@@ -499,3 +544,4 @@ export function setLoadableAvatarUrl(participantId, url) {
}
};
}

View File

@@ -17,6 +17,11 @@ import { IconPhone } from '../icons';
*/
export const DEFAULT_AVATAR_RELATIVE_PATH = 'images/avatar.png';
/**
* The value for the "var" attribute of feature tag in disco-info packets.
*/
export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
/**
* Icon URL for jigasi participants.
*

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/participants');

View File

@@ -237,6 +237,13 @@ StateListenerRegistry.register(
_raiseHandUpdated(store, conference, participant.getId(), newValue);
break;
}
case 'remoteControlSessionStatus':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
remoteControlSessionStatus: newValue
}));
break;
default:
// Ignore for now.

View File

@@ -84,13 +84,7 @@ class Watermarks extends Component<Props, State> {
constructor(props: Props) {
super(props);
let showBrandWatermark;
if (interfaceConfig.filmStripOnly) {
showBrandWatermark = false;
} else {
showBrandWatermark = interfaceConfig.SHOW_BRAND_WATERMARK;
}
const showBrandWatermark = interfaceConfig.SHOW_BRAND_WATERMARK;
this.state = {
brandWatermarkLink:
@@ -237,12 +231,11 @@ function _mapStateToProps(state, ownProps) {
const {
DEFAULT_LOGO_URL,
JITSI_WATERMARK_LINK,
SHOW_JITSI_WATERMARK,
filmStripOnly
SHOW_JITSI_WATERMARK
} = interfaceConfig;
let _showJitsiWatermark = (!filmStripOnly
&& (customizationReady && !customizationFailed)
&& SHOW_JITSI_WATERMARK)
let _showJitsiWatermark = (
customizationReady && !customizationFailed
&& SHOW_JITSI_WATERMARK)
|| !isValidRoom;
let _logoUrl = logoImageUrl;
let _logoLink = logoClickUrl;

View File

@@ -152,22 +152,19 @@ StateListenerRegistry.register(
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
if ((typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly)
|| (typeof APP !== 'undefined' && !isButtonEnabled('chat'))
if ((typeof APP !== 'undefined' && !isButtonEnabled('chat'))
|| store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if we're in filmStripOnly mode, or
// the chat button is not enabled in interfaceConfig.
// We don't register anything on web if the chat button is not enabled in interfaceConfig
// or we are in iAmRecorder mode
return;
}
conference.on(
JitsiConferenceEvents.MESSAGE_RECEIVED,
(id, message, timestamp, nick) => {
(id, message, timestamp) => {
_handleReceivedMessage(store, {
id,
message,
nick,
privateMessage: false,
timestamp
});
@@ -181,8 +178,7 @@ function _addChatMsgListener(conference, store) {
id,
message,
privateMessage: true,
timestamp,
nick: undefined
timestamp
});
}
);
@@ -217,7 +213,7 @@ function _handleChatError({ dispatch }, error) {
* @param {Object} message - The message object.
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, privateMessage, timestamp }) {
function _handleReceivedMessage({ dispatch, getState }, { id, message, privateMessage, timestamp }) {
// Logic for all platforms:
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
@@ -230,10 +226,9 @@ function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, pri
// backfilled for a participant that has left the conference.
const participant = getParticipantById(state, id) || {};
const localParticipant = getLocalParticipant(getState);
const displayName = participant.name || nick || getParticipantDisplayName(state, id);
const displayName = getParticipantDisplayName(state, id);
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp
? new Date(timestamp) : new Date();
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
dispatch(addMessage({

View File

@@ -14,7 +14,7 @@ import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { fullScreenChanged, setToolboxAlwaysVisible, showToolbox } from '../../../toolbox/actions.web';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { maybeShowSuboptimalExperienceNotification } from '../../functions';
@@ -28,7 +28,6 @@ import Labels from './Labels';
import { default as Notice } from './Notice';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
/**
@@ -175,18 +174,13 @@ class Conference extends AbstractConference<Props, *> {
* @returns {ReactElement}
*/
render() {
const {
// XXX The character casing of the name filmStripOnly utilized by
// interfaceConfig is obsolete but legacy support is required.
filmStripOnly: filmstripOnly
} = interfaceConfig;
const {
_iAmRecorder,
_isLobbyScreenVisible,
_layoutClassName,
_showPrejoin
} = this.props;
const hideLabels = filmstripOnly || _iAmRecorder;
const hideLabels = _iAmRecorder;
return (
<div
@@ -198,18 +192,18 @@ class Conference extends AbstractConference<Props, *> {
<div id = 'videospace'>
<LargeVideo />
<KnockingParticipantList />
<Filmstrip filmstripOnly = { filmstripOnly } />
<Filmstrip />
{ hideLabels || <Labels /> }
</div>
{ filmstripOnly || _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
{ filmstripOnly || <Chat /> }
{ _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
<Chat />
{ this.renderNotificationsContainer() }
<CalleeInfoContainer />
{ !filmstripOnly && _showPrejoin && <Prejoin />}
{ _showPrejoin && <Prejoin />}
</div>
);
}
@@ -256,9 +250,6 @@ class Conference extends AbstractConference<Props, *> {
dispatch(connect());
maybeShowSuboptimalExperienceNotification(dispatch, t);
interfaceConfig.filmStripOnly
&& dispatch(setToolboxAlwaysVisible(true));
}
}

View File

@@ -56,7 +56,7 @@ export function maybeOpenFeedbackDialog(conference: Object) {
const state = getState();
const { feedbackPercentage = 100 } = state['features/base/config'];
if (interfaceConfig.filmStripOnly || config.iAmRecorder) {
if (config.iAmRecorder) {
// Intentionally fall through the if chain to prevent further action
// from being taken with regards to showing feedback.
} else if (state['features/base/dialog'].component === FeedbackDialog) {

View File

@@ -17,8 +17,6 @@ import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { setFilmstripHovered, setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
import Toolbar from './Toolbar';
declare var APP: Object;
declare var interfaceConfig: Object;
@@ -42,11 +40,6 @@ type Props = {
*/
_columns: number,
/**
* Whether the UI/UX is filmstrip-only.
*/
_filmstripOnly: boolean,
/**
* The width of the filmstrip.
*/
@@ -142,14 +135,12 @@ class Filmstrip extends Component <Props> {
* @inheritdoc
*/
componentDidMount() {
if (!this.props._filmstripOnly) {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
}
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
}
/**
@@ -207,7 +198,7 @@ class Filmstrip extends Component <Props> {
let toolbar = null;
if (!this.props._hideToolbar) {
toolbar = this.props._filmstripOnly ? <Toolbar /> : this._renderToggleButton();
toolbar = this._renderToggleButton();
}
return (
@@ -367,24 +358,20 @@ class Filmstrip extends Component <Props> {
function _mapStateToProps(state) {
const { iAmSipGateway } = state['features/base/config'];
const { hovered, visible } = state['features/filmstrip'];
const isFilmstripOnly = Boolean(interfaceConfig.filmStripOnly);
const reduceHeight
= !isFilmstripOnly && state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
= state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''}`.trim();
const videosClassName = `filmstrip__videos${
isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${
visible ? '' : ' hidden'}`;
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
return {
_className: className,
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripOnly: isFilmstripOnly,
_filmstripWidth: filmstripWidth,
_hideScrollbar: Boolean(iAmSipGateway),
_hideToolbar: Boolean(iAmSipGateway),

View File

@@ -1,101 +0,0 @@
// @flow
import React, { Component } from 'react';
import { connect, equals } from '../../../base/redux';
import { SettingsButton } from '../../../settings';
import {
AudioMuteButton,
HangupButton,
VideoMuteButton
} from '../../../toolbox/components';
declare var interfaceConfig: Object;
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will. This has to be retrieved from the store.
const visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
/**
* The type of the React {@code Component} props of {@link Toolbar}.
*/
type Props = {
/**
* The set of buttons which should be visible in this {@code Toolbar}.
*/
_visibleButtons: Set<string>
};
/**
* Implements the conference toolbar on React/Web for filmstrip-only mode.
*
* @extends Component
*/
class Toolbar extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'filmstrip-toolbox'
id = 'new-toolbox'>
<HangupButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('hangup') } />
<AudioMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('microphone') } />
<VideoMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('camera') } />
<SettingsButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('fodeviceselection') } />
</div>
);
}
_shouldShowButton: (string) => boolean;
/**
* Returns if a button name has been explicitly configured to be displayed.
*
* @param {string} buttonName - The name of the button, as expected in
* {@link intefaceConfig}.
* @private
* @returns {boolean} True if the button should be displayed, false
* otherwise.
*/
_shouldShowButton(buttonName) {
return this.props._visibleButtons.has(buttonName);
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _visibleButtons: Set<string>
* }}
*/
function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will.
//
// NB: We compute the buttons again here because if URL parameters were used to
// override them we'd miss it.
const buttons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
return {
_visibleButtons: equals(visibleButtons, buttons) ? visibleButtons : buttons
};
}
export default connect(_mapStateToProps)(Toolbar);

View File

@@ -56,9 +56,6 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|| ((pinnedParticipant = getPinnedParticipant(state))
&& pinnedParticipant.local)))
|| (typeof interfaceConfig === 'object'
&& interfaceConfig.filmStripOnly)
|| state['features/base/config'].disable1On1Mode);
}

View File

@@ -1,112 +0,0 @@
// @flow
import React, { Component } from 'react';
import { Avatar } from '../../../base/avatar';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import OverlayFrame from './OverlayFrame';
/**
* The type of the React {@code Component} props of
* {@link FilmstripOnlyOverlayFrame}.
*/
type Props = {
/**
* The ID of the local participant.
*/
_localParticipantId: string,
/**
* The children components to be displayed into the overlay frame for
* filmstrip only mode.
*/
children: React$Node,
/**
* The css class name for the icon that will be displayed over the avatar.
*/
icon: string,
/**
* Indicates the css style of the overlay. If true, then lighter; darker,
* otherwise.
*/
isLightOverlay: boolean
};
/**
* Implements a React Component for the frame of the overlays in filmstrip only
* mode.
*/
class FilmstripOnlyOverlayFrame extends Component<Props> {
/**
* Renders content related to the icon.
*
* @returns {ReactElement|null}
* @private
*/
_renderIcon() {
if (!this.props.icon) {
return null;
}
const iconClass = `inlay-filmstrip-only__icon ${this.props.icon}`;
const iconBGClass = 'inlay-filmstrip-only__icon-background';
return (
<div>
<div className = { iconBGClass } />
<div className = 'inlay-filmstrip-only__icon-container'>
<span className = { iconClass } />
</div>
</div>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<OverlayFrame isLightOverlay = { this.props.isLightOverlay }>
<div className = 'inlay-filmstrip-only'>
<div className = 'inlay-filmstrip-only__content'>
{
this.props.children
}
</div>
<div className = 'inlay-filmstrip-only__avatar-container'>
<Avatar participantId = { this.props._localParticipantId } />
{
this._renderIcon()
}
</div>
</div>
</OverlayFrame>
);
}
}
/**
* Maps (parts of) the Redux state to the associated FilmstripOnlyOverlayFrame
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _localParticipantId: string
* }}
*/
function _mapStateToProps(state) {
return {
_localParticipantId: (getLocalParticipant(state) || {}).id
};
}
export default connect(_mapStateToProps)(FilmstripOnlyOverlayFrame);

View File

@@ -21,44 +21,10 @@ type Props = {
isLightOverlay?: boolean
};
/**
* The type of the React {@code Component} state of {@link OverlayFrame}.
*/
type State = {
/**
* Whether or not the application is currently displaying in filmstrip only
* mode.
*/
filmstripOnly: boolean
};
/**
* Implements a React {@link Component} for the frame of the overlays.
*/
export default class OverlayFrame extends Component<Props, State> {
/**
* Initializes a new AbstractOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props: Props) {
super(props);
this.state = {
/**
* Indicates whether the filmstrip only mode is enabled or not.
*
* @type {boolean}
*/
filmstripOnly:
typeof interfaceConfig !== 'undefined'
&& interfaceConfig.filmStripOnly
};
}
export default class OverlayFrame extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
@@ -66,20 +32,11 @@ export default class OverlayFrame extends Component<Props, State> {
* @returns {ReactElement|null}
*/
render() {
let containerClass = this.props.isLightOverlay
? 'overlay__container-light' : 'overlay__container';
let contentClass = 'overlay__content';
if (this.state.filmstripOnly) {
containerClass += ' filmstrip-only';
contentClass += ' filmstrip-only';
}
return (
<div
className = { containerClass }
className = { this.props.isLightOverlay ? 'overlay__container-light' : 'overlay__container' }
id = 'overlay'>
<div className = { contentClass }>
<div className = { 'overlay__content' }>
{
this.props.children
}

View File

@@ -1,50 +0,0 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractPageReloadOverlay, { type Props, abstractMapStateToProps }
from '../AbstractPageReloadOverlay';
import FilmstripOnlyOverlayFrame from './FilmstripOnlyOverlayFrame';
/**
* Implements a React Component for page reload overlay for filmstrip only
* mode. Shown before the conference is reloaded. Shows a warning message and
* counts down towards the reload.
*/
class PageReloadFilmstripOnlyOverlay extends AbstractPageReloadOverlay<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { message, timeLeft, title } = this.state;
return (
<FilmstripOnlyOverlayFrame>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{ t(title) }
</div>
<div className = 'inlay-filmstrip-only__text'>
{ t(message, { seconds: timeLeft }) }
</div>
</div>
{ this._renderButton() }
{ this._renderProgressBar() }
</FilmstripOnlyOverlayFrame>
);
}
_renderButton: () => React$Element<*> | null
_renderProgressBar: () => React$Element<*> | null
}
export default translate(
connect(abstractMapStateToProps)(PageReloadFilmstripOnlyOverlay));

View File

@@ -1,41 +0,0 @@
// @flow
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import AbstractSuspendedOverlay from './AbstractSuspendedOverlay';
import FilmstripOnlyOverlayFrame from './FilmstripOnlyOverlayFrame';
import ReloadButton from './ReloadButton';
/**
* Implements a React Component for suspended overlay for filmstrip only mode.
* Shown when suspended is detected.
*/
class SuspendedFilmstripOnlyOverlay extends AbstractSuspendedOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<FilmstripOnlyOverlayFrame isLightOverlay = { true }>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{ t('suspendedoverlay.title') }
</div>
<div className = 'inlay-filmstrip-only__text'>
{ translateToHTML(t, 'suspendedoverlay.text') }
</div>
</div>
<ReloadButton textKey = 'suspendedoverlay.rejoinKeyTitle' />
</FilmstripOnlyOverlayFrame>
);
}
}
export default translate(SuspendedFilmstripOnlyOverlay);

View File

@@ -1,54 +0,0 @@
// @flow
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractUserMediaPermissionsOverlay, { abstractMapStateToProps }
from './AbstractUserMediaPermissionsOverlay';
import FilmstripOnlyOverlayFrame from './FilmstripOnlyOverlayFrame';
declare var interfaceConfig: Object;
/**
* Implements a React Component for overlay with guidance how to proceed with
* gUM prompt. This component will be displayed only for filmstrip only mode.
*/
class UserMediaPermissionsFilmstripOnlyOverlay
extends AbstractUserMediaPermissionsOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const textKey = `userMedia.${this.props.browser}GrantPermissions`;
return (
<FilmstripOnlyOverlayFrame
icon = 'icon-mic-camera-combined'
isLightOverlay = { true }>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{
t('startupoverlay.title',
{ app: interfaceConfig.APP_NAME })
}
</div>
<div className = 'inlay-filmstrip-only__text'>
{
translateToHTML(t, textKey)
}
</div>
</div>
</FilmstripOnlyOverlayFrame>
);
}
}
export default translate(
connect(abstractMapStateToProps)(UserMediaPermissionsFilmstripOnlyOverlay));

View File

@@ -32,8 +32,7 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
<span className = 'inlay__icon icon-camera' />
<h3 className = 'inlay__title'>
{
t('startupoverlay.title',
{ app: interfaceConfig.APP_NAME })
t('startupoverlay.genericTitle')
}
</h3>
<span className = 'inlay__text'>

View File

@@ -1,19 +1,7 @@
// @flow
export { default as FilmstripOnlyOverlayFrame } from './FilmstripOnlyOverlayFrame';
export { default as OverlayFrame } from './OverlayFrame';
export {
default as PageReloadFilmstripOnlyOverlay
} from './PageReloadFilmstripOnlyOverlay';
export { default as PageReloadOverlay } from './PageReloadOverlay';
export {
default as SuspendedFilmstripOnlyOverlay
} from './SuspendedFilmstripOnlyOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay';
export {
default as UserMediaPermissionsFilmstripOnlyOverlay
} from './UserMediaPermissionsFilmstripOnlyOverlay';
export {
default as UserMediaPermissionsOverlay
} from './UserMediaPermissionsOverlay';
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay';

View File

@@ -1,11 +1,8 @@
// @flow
import {
PageReloadFilmstripOnlyOverlay,
PageReloadOverlay,
SuspendedFilmstripOnlyOverlay,
SuspendedOverlay,
UserMediaPermissionsFilmstripOnlyOverlay,
UserMediaPermissionsOverlay
} from './components/web';
@@ -17,22 +14,9 @@ declare var interfaceConfig: Object;
* @returns {Array<Object>}
*/
export function getOverlays(): Array<Object> {
const overlays = [
return [
PageReloadOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay
];
const filmstripOnly
= typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly;
if (filmstripOnly) {
overlays.push(
PageReloadFilmstripOnlyOverlay,
SuspendedFilmstripOnlyOverlay,
UserMediaPermissionsFilmstripOnlyOverlay);
} else {
overlays.push(PageReloadOverlay);
}
return overlays;
}

View File

@@ -0,0 +1,70 @@
// @flow
/**
* The type of (redux) action which signals that the controller is capturing mouse and keyboard events.
*
* {
* type: CAPTURE_EVENTS,
* isCapturingEvents: boolean
* }
*/
export const CAPTURE_EVENTS = 'CAPTURE_EVENTS';
/**
* The type of (redux) action which signals that a remote control active state has changed.
*
* {
* type: REMOTE_CONTROL_ACTIVE,
* active: boolean
* }
*/
export const REMOTE_CONTROL_ACTIVE = 'REMOTE_CONTROL_ACTIVE';
/**
* The type of (redux) action which sets the receiver transport object.
*
* {
* type: SET_RECEIVER_TRANSPORT,
* transport: Transport
* }
*/
export const SET_RECEIVER_TRANSPORT = 'SET_RECEIVER_TRANSPORT';
/**
* The type of (redux) action which enables the receiver.
*
* {
* type: SET_RECEIVER_ENABLED,
* enabled: boolean
* }
*/
export const SET_RECEIVER_ENABLED = 'SET_RECEIVER_ENABLED';
/**
* The type of (redux) action which sets the controller participant on the receiver side.
* {
* type: SET_CONTROLLER,
* controller: string
* }
*/
export const SET_CONTROLLER = 'SET_CONTROLLER';
/**
* The type of (redux) action which sets the controlled participant on the controller side.
* {
* type: SET_CONTROLLED_PARTICIPANT,
* controlled: string
* }
*/
export const SET_CONTROLLED_PARTICIPANT = 'SET_CONTROLLED_PARTICIPANT';
/**
* The type of (redux) action which sets the requested participant on the controller side.
* {
* type: SET_REQUESTED_PARTICIPANT,
* requestedParticipant: string
* }
*/
export const SET_REQUESTED_PARTICIPANT = 'SET_REQUESTED_PARTICIPANT';

View File

@@ -1,6 +1,44 @@
import { openDialog } from '../base/dialog';
// @flow
import { openDialog } from '../base/dialog';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getParticipantDisplayName, getPinnedParticipant, pinParticipant } from '../base/participants';
import { getLocalVideoTrack } from '../base/tracks';
import { showNotification } from '../notifications';
import {
CAPTURE_EVENTS,
REMOTE_CONTROL_ACTIVE,
SET_REQUESTED_PARTICIPANT,
SET_CONTROLLER,
SET_RECEIVER_ENABLED,
SET_RECEIVER_TRANSPORT,
SET_CONTROLLED_PARTICIPANT
} from './actionTypes';
import { RemoteControlAuthorizationDialog } from './components';
import {
DISCO_REMOTE_CONTROL_FEATURE,
EVENTS,
REMOTE_CONTROL_MESSAGE_NAME,
PERMISSIONS_ACTIONS,
REQUESTS
} from './constants';
import {
getKey,
getModifiers,
getRemoteConrolEventCaptureArea,
isRemoteControlEnabled,
sendRemoteControlEndpointMessage
} from './functions';
import logger from './logger';
/**
* Listeners.
*/
let permissionsReplyListener, receiverEndpointMessageListener, stopListener;
declare var APP: Object;
declare var $: Function;
/**
* Signals that the remote control authorization dialog should be displayed.
@@ -16,6 +54,700 @@ import { RemoteControlAuthorizationDialog } from './components';
* }}
* @public
*/
export function openRemoteControlAuthorizationDialog(participantId) {
export function openRemoteControlAuthorizationDialog(participantId: string) {
return openDialog(RemoteControlAuthorizationDialog, { participantId });
}
/**
* Sets the remote control active property.
*
* @param {boolean} active - The new value for the active property.
* @returns {Function}
*/
export function setRemoteControlActive(active: boolean) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { active: oldActive } = state['features/remote-control'];
const { conference } = state['features/base/conference'];
if (active !== oldActive) {
dispatch({
type: REMOTE_CONTROL_ACTIVE,
active
});
conference.setLocalParticipantProperty('remoteControlSessionStatus', active);
}
};
}
/**
* Requests permissions from the remote control receiver side.
*
* @param {string} userId - The user id of the participant that will be
* requested.
* @returns {Function}
*/
export function requestRemoteControl(userId: string) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const enabled = isRemoteControlEnabled(state);
if (!enabled) {
return Promise.reject(new Error('Remote control is disabled!'));
}
dispatch(setRemoteControlActive(true));
logger.log(`Requsting remote control permissions from: ${userId}`);
const { conference } = state['features/base/conference'];
permissionsReplyListener = (participant, event) => {
dispatch(processPermissionRequestReply(participant.getId(), event));
};
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
dispatch({
type: SET_REQUESTED_PARTICIPANT,
requestedParticipant: userId
});
if (!sendRemoteControlEndpointMessage(
conference,
userId,
{
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.request
})) {
dispatch(clearRequest());
}
};
}
/**
* Handles permission request replies on the controller side.
*
* @param {string} participantId - The participant that sent the request.
* @param {EndpointMessage} event - The permission request event.
* @returns {Function}
*/
export function processPermissionRequestReply(participantId: string, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { action, name, type } = event;
const { requestedParticipant } = state['features/remote-control'].controller;
if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.permissions
&& participantId === requestedParticipant) {
let descriptionKey, permissionGranted = false;
switch (action) {
case PERMISSIONS_ACTIONS.grant: {
dispatch({
type: SET_CONTROLLED_PARTICIPANT,
controlled: participantId
});
logger.log('Remote control permissions granted!', participantId);
logger.log('Starting remote control controller.');
const { conference } = state['features/base/conference'];
stopListener = (participant, stopEvent) => {
dispatch(handleRemoteControlStoppedEvent(participant.getId(), stopEvent));
};
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
dispatch(resume());
permissionGranted = true;
descriptionKey = 'dialog.remoteControlAllowedMessage';
break;
}
case PERMISSIONS_ACTIONS.deny:
logger.log('Remote control permissions denied!', participantId);
descriptionKey = 'dialog.remoteControlDeniedMessage';
break;
case PERMISSIONS_ACTIONS.error:
logger.error('Error occurred on receiver side');
descriptionKey = 'dialog.remoteControlErrorMessage';
break;
default:
logger.error('Unknown reply received!');
descriptionKey = 'dialog.remoteControlErrorMessage';
}
dispatch(clearRequest());
if (!permissionGranted) {
dispatch(setRemoteControlActive(false));
}
dispatch(showNotification({
descriptionArguments: { user: getParticipantDisplayName(state, participantId) },
descriptionKey,
titleKey: 'dialog.remoteControlTitle'
}));
if (permissionGranted) {
// the remote control permissions has been granted
// pin the controlled participant
const pinnedParticipant = getPinnedParticipant(state);
const pinnedId = pinnedParticipant?.id;
if (pinnedId !== participantId) {
dispatch(pinParticipant(participantId));
}
}
} else {
// different message type or another user -> ignoring the message
}
};
}
/**
* Handles remote control stopped.
*
* @param {string} participantId - The ID of the participant that has sent the event.
* @param {EndpointMessage} event - EndpointMessage event from the data channels.
* @property {string} type - The function process only events with name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {void}
*/
export function handleRemoteControlStoppedEvent(participantId: Object, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { name, type } = event;
const { controlled } = state['features/remote-control'].controller;
if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.stop
&& participantId === controlled) {
dispatch(stopController());
}
};
}
/**
* Stops processing the mouse and keyboard events. Removes added listeners.
* Enables the keyboard shortcuts. Displays dialog to notify the user that remote control session has ended.
*
* @param {boolean} notifyRemoteParty - If true a endpoint message to the controlled participant will be sent.
* @returns {void}
*/
export function stopController(notifyRemoteParty: boolean = false) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { controlled } = state['features/remote-control'].controller;
if (!controlled) {
return;
}
const { conference } = state['features/base/conference'];
if (notifyRemoteParty) {
sendRemoteControlEndpointMessage(conference, controlled, {
type: EVENTS.stop
});
}
logger.log('Stopping remote control controller.');
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
stopListener = undefined;
dispatch(pause());
dispatch({
type: SET_CONTROLLED_PARTICIPANT,
controlled: undefined
});
dispatch(setRemoteControlActive(false));
dispatch(showNotification({
descriptionKey: 'dialog.remoteControlStopMessage',
titleKey: 'dialog.remoteControlTitle'
}));
};
}
/**
* Clears a pending permission request.
*
* @returns {Function}
*/
export function clearRequest() {
return (dispatch: Function, getState: Function) => {
const { conference } = getState()['features/base/conference'];
dispatch({
type: SET_REQUESTED_PARTICIPANT,
requestedParticipant: undefined
});
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
permissionsReplyListener = undefined;
};
}
/**
* Sets that trasnport object that is used by the receiver to communicate with the native part of the remote control
* implementation.
*
* @param {Transport} transport - The transport to be set.
* @returns {{
* type: SET_RECEIVER_TRANSPORT,
* transport: Transport
* }}
*/
export function setReceiverTransport(transport: Object) {
return {
type: SET_RECEIVER_TRANSPORT,
transport
};
}
/**
* Enables the receiver functionality.
*
* @returns {Function}
*/
export function enableReceiver() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { enabled } = state['features/remote-control'].receiver;
if (enabled) {
return;
}
const { connection } = state['features/base/connection'];
const { conference } = state['features/base/conference'];
if (!connection || !conference) {
logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
return;
}
dispatch({
type: SET_RECEIVER_ENABLED,
enabled: true
});
connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
receiverEndpointMessageListener = (participant, message) => {
dispatch(endpointMessageReceived(participant.getId(), message));
};
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
};
}
/**
* Disables the receiver functionality.
*
* @returns {Function}
*/
export function disableReceiver() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { enabled } = state['features/remote-control'].receiver;
if (!enabled) {
return;
}
const { connection } = state['features/base/connection'];
const { conference } = state['features/base/conference'];
if (!connection || !conference) {
logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
return;
}
logger.log('Remote control receiver disabled.');
dispatch({
type: SET_RECEIVER_ENABLED,
enabled: false
});
dispatch(stopReceiver(true));
connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
};
}
/**
* Stops a remote control session on the receiver side.
*
* @param {boolean} [dontNotifyLocalParty] - If true - a notification about stopping
* the remote control won't be displayed.
* @param {boolean} [dontNotifyRemoteParty] - If true a endpoint message to the controller participant will be sent.
* @returns {Function}
*/
export function stopReceiver(dontNotifyLocalParty: boolean = false, dontNotifyRemoteParty: boolean = false) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { receiver } = state['features/remote-control'];
const { controller, transport } = receiver;
if (!controller) {
return;
}
const { conference } = state['features/base/conference'];
if (!dontNotifyRemoteParty) {
sendRemoteControlEndpointMessage(conference, controller, {
type: EVENTS.stop
});
}
dispatch({
type: SET_CONTROLLER,
controller: undefined
});
transport.sendEvent({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: EVENTS.stop
});
dispatch(setRemoteControlActive(false));
if (!dontNotifyLocalParty) {
dispatch(showNotification({
descriptionKey: 'dialog.remoteControlStopMessage',
titleKey: 'dialog.remoteControlTitle'
}));
}
};
}
/**
* Handles only remote control endpoint messages.
*
* @param {string} participantId - The controller participant ID.
* @param {Object} message - EndpointMessage from the data channels.
* @param {string} message.name - The function processes only messages with
* name REMOTE_CONTROL_MESSAGE_NAME.
* @returns {Function}
*/
export function endpointMessageReceived(participantId: string, message: Object) {
return (dispatch: Function, getState: Function) => {
const { action, name, type } = message;
if (name !== REMOTE_CONTROL_MESSAGE_NAME) {
return;
}
const state = getState();
const { receiver } = state['features/remote-control'];
const { enabled, transport } = receiver;
if (enabled) {
const { controller } = receiver;
if (!controller && type === EVENTS.permissions && action === PERMISSIONS_ACTIONS.request) {
dispatch(setRemoteControlActive(true));
dispatch(openRemoteControlAuthorizationDialog(participantId));
} else if (controller === participantId) {
if (type === EVENTS.stop) {
dispatch(stopReceiver(false, true));
} else { // forward the message
transport.sendEvent(message);
}
} // else ignore
} else {
logger.log('Remote control message is ignored because remote control is disabled', message);
}
};
}
/**
* Denies remote control access for user associated with the passed user id.
*
* @param {string} participantId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {Function}
*/
export function deny(participantId: string) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
dispatch(setRemoteControlActive(false));
sendRemoteControlEndpointMessage(conference, participantId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.deny
});
};
}
/**
* Sends start remote control request to the native implementation.
*
* @returns {Function}
*/
export function sendStartRequest() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const tracks = state['features/base/tracks'];
const track = getLocalVideoTrack(tracks);
const { sourceId } = track?.jitsiTrack || {};
const { transport } = state['features/remote-control'].receiver;
return transport.sendRequest({
name: REMOTE_CONTROL_MESSAGE_NAME,
type: REQUESTS.start,
sourceId
});
};
}
/**
* Grants remote control access to user associated with the passed user id.
*
* @param {string} participantId - The id associated with the user who sent the
* request for remote control authorization.
* @returns {Function}
*/
export function grant(participantId: string) {
return (dispatch: Function, getState: Function) => {
dispatch({
type: SET_CONTROLLER,
controller: participantId
});
logger.log(`Remote control permissions granted to: ${participantId}`);
let promise;
const state = getState();
const tracks = state['features/base/tracks'];
const track = getLocalVideoTrack(tracks);
const isScreenSharing = track?.videoType === 'desktop';
const { sourceType } = track?.jitsiTrack || {};
if (isScreenSharing && sourceType === 'screen') {
promise = dispatch(sendStartRequest());
} else {
// FIXME: Use action here once toggleScreenSharing is moved to redux.
promise = APP.conference.toggleScreenSharing(
true,
{
desktopSharingSources: [ 'screen' ]
})
.then(() => dispatch(sendStartRequest()));
}
const { conference } = state['features/base/conference'];
promise
.then(() => sendRemoteControlEndpointMessage(conference, participantId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.grant
}))
.catch(error => {
logger.error(error);
sendRemoteControlEndpointMessage(conference, participantId, {
type: EVENTS.permissions,
action: PERMISSIONS_ACTIONS.error
});
dispatch(showNotification({
descriptionKey: 'dialog.startRemoteControlErrorMessage',
titleKey: 'dialog.remoteControlTitle'
}));
dispatch(stopReceiver(true));
});
};
}
/**
* Handler for mouse click events on the controller side.
*
* @param {string} type - The type of event ("mousedown"/"mouseup").
* @param {Event} event - The mouse event.
* @returns {Function}
*/
export function mouseClicked(type: string, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type,
button: event.which
});
};
}
/**
* Handles mouse moved events on the controller side.
*
* @param {Event} event - The mouse event.
* @returns {Function}
*/
export function mouseMoved(event: Object) {
return (dispatch: Function, getState: Function) => {
const area = getRemoteConrolEventCaptureArea();
if (!area) {
return;
}
const position = area.position();
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type: EVENTS.mousemove,
x: (event.pageX - position.left) / area.width(),
y: (event.pageY - position.top) / area.height()
});
};
}
/**
* Handles mouse scroll events on the controller side.
*
* @param {Event} event - The mouse event.
* @returns {Function}
*/
export function mouseScrolled(event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type: EVENTS.mousescroll,
x: event.deltaX,
y: event.deltaY
});
};
}
/**
* Handles key press events on the controller side..
*
* @param {string} type - The type of event ("keydown"/"keyup").
* @param {Event} event - The key event.
* @returns {Function}
*/
export function keyPressed(type: string, event: Object) {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = state['features/base/conference'];
const { controller } = state['features/remote-control'];
sendRemoteControlEndpointMessage(conference, controller.controlled, {
type,
key: getKey(event),
modifiers: getModifiers(event)
});
};
}
/**
* Disables the keyboatd shortcuts. Starts collecting remote control
* events. It can be used to resume an active remote control session which
* was paused with the pause action.
*
* @returns {Function}
*/
export function resume() {
return (dispatch: Function, getState: Function) => {
const area = getRemoteConrolEventCaptureArea();
const state = getState();
const { controller } = state['features/remote-control'];
const { controlled, isCapturingEvents } = controller;
if (!isRemoteControlEnabled(state) || !area || !controlled || isCapturingEvents) {
return;
}
logger.log('Resuming remote control controller.');
// FIXME: Once the keyboard shortcuts are using react/redux.
APP.keyboardshortcut.enable(false);
area.mousemove(event => {
dispatch(mouseMoved(event));
});
area.mousedown(event => dispatch(mouseClicked(EVENTS.mousedown, event)));
area.mouseup(event => dispatch(mouseClicked(EVENTS.mouseup, event)));
area.dblclick(event => dispatch(mouseClicked(EVENTS.mousedblclick, event)));
area.contextmenu(() => false);
area[0].onmousewheel = event => {
event.preventDefault();
event.stopPropagation();
dispatch(mouseScrolled(event));
return false;
};
$(window).keydown(event => dispatch(keyPressed(EVENTS.keydown, event)));
$(window).keyup(event => dispatch(keyPressed(EVENTS.keyup, event)));
dispatch({
type: CAPTURE_EVENTS,
isCapturingEvents: true
});
};
}
/**
* Pauses the collecting of events and enables the keyboard shortcus. But
* it doesn't removes any other listeners. Basically the remote control
* session will be still active after the pause action, but no events from the
* controller side will be captured and sent. You can resume the collecting
* of the events with the resume action.
*
* @returns {Function}
*/
export function pause() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { controller } = state['features/remote-control'];
const { controlled, isCapturingEvents } = controller;
if (!isRemoteControlEnabled(state) || !controlled || !isCapturingEvents) {
return;
}
logger.log('Pausing remote control controller.');
// FIXME: Once the keyboard shortcuts are using react/redux.
APP.keyboardshortcut.enable(true);
const area = getRemoteConrolEventCaptureArea();
if (area) {
area.off('contextmenu');
area.off('dblclick');
area.off('mousedown');
area.off('mousemove');
area.off('mouseup');
area[0].onmousewheel = undefined;
}
$(window).off('keydown');
$(window).off('keyup');
dispatch({
type: CAPTURE_EVENTS,
isCapturingEvents: false
});
};
}

View File

@@ -6,6 +6,8 @@ import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { getParticipantById } from '../../base/participants';
import { connect } from '../../base/redux';
import { getLocalVideoTrack } from '../../base/tracks';
import { grant, deny } from '../actions';
declare var APP: Object;
@@ -21,6 +23,9 @@ type Props = {
*/
_displayName: string,
_isScreenSharing: boolean,
_sourceType: string,
/**
* Used to show/hide the dialog on cancel.
*/
@@ -87,10 +92,9 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* @returns {ReactElement}
*/
_getAdditionalMessage() {
// FIXME: Once we have this information in redux we should
// start getting it from there.
if (APP.conference.isSharingScreen
&& APP.conference.getDesktopSharingSourceType() === 'screen') {
const { _isScreenSharing, _sourceType } = this.props;
if (_isScreenSharing && _sourceType === 'screen') {
return null;
}
@@ -112,8 +116,9 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* @returns {boolean} Returns true to close the dialog.
*/
_onCancel() {
// FIXME: This should be action one day.
APP.remoteControl.receiver.deny(this.props.participantId);
const { dispatch, participantId } = this.props;
dispatch(deny(participantId));
return true;
}
@@ -131,10 +136,10 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* picker window, the action will be ignored).
*/
_onSubmit() {
this.props.dispatch(hideDialog());
const { dispatch, participantId } = this.props;
// FIXME: This should be action one day.
APP.remoteControl.receiver.grant(this.props.participantId);
dispatch(hideDialog());
dispatch(grant(participantId));
return false;
}
@@ -149,15 +154,24 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
* (instance of) RemoteControlAuthorizationDialog.
* @private
* @returns {{
* _displayName: string
* _displayName: string,
* _isScreenSharing: boolean,
* _sourceId: string,
* _sourceType: string
* }}
*/
function _mapStateToProps(state, ownProps) {
const { _displayName, participantId } = ownProps;
const participant = getParticipantById(state, participantId);
const tracks = state['features/base/tracks'];
const track = getLocalVideoTrack(tracks);
const _isScreenSharing = track?.videoType === 'desktop';
const { sourceType } = track?.jitsiTrack || {};
return {
_displayName: participant ? participant.name : _displayName
_displayName: participant ? participant.name : _displayName,
_isScreenSharing,
_sourceType: sourceType
};
}

View File

@@ -1,8 +1,41 @@
/**
* The type of remote control messages.
*/
export const REMOTE_CONTROL_MESSAGE_NAME = 'remote-control';
/**
* The value for the "var" attribute of feature tag in disco-info packets.
*/
export const DISCO_REMOTE_CONTROL_FEATURE
= 'http://jitsi.org/meet/remotecontrol';
export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
/**
* The remote control event.
* @typedef {object} RemoteControlEvent
* @property {EVENTS | REQUESTS} type - the type of the message
* @property {number} x - avaibale for type === mousemove only. The new x
* coordinate of the mouse
* @property {number} y - For mousemove type - the new y
* coordinate of the mouse and for mousescroll - represents the vertical
* scrolling diff value
* @property {number} button - 1(left), 2(middle) or 3 (right). Supported by
* mousedown, mouseup and mousedblclick types.
* @property {KEYS} key - Represents the key related to the event. Supported by
* keydown and keyup types.
* @property {KEYS[]} modifiers - Represents the modifier related to the event.
* Supported by keydown and keyup types.
* @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions.
* Represents the action related to the permissions event.
*
* Optional properties. Supported for permissions event for action === request:
* @property {string} userId - The user id of the participant that has sent the
* request.
* @property {string} userJID - The full JID in the MUC of the user that has
* sent the request.
* @property {string} displayName - the displayName of the participant that has
* sent the request.
* @property {boolean} screenSharing - true if the SS is started for the local
* participant and false if not.
*/
/**
* Types of remote-control events.
@@ -44,36 +77,3 @@ export const PERMISSIONS_ACTIONS = {
error: 'error'
};
/**
* The type of remote control messages.
*/
export const REMOTE_CONTROL_MESSAGE_NAME = 'remote-control';
/**
* The remote control event.
* @typedef {object} RemoteControlEvent
* @property {EVENTS | REQUESTS} type - the type of the message
* @property {number} x - avaibale for type === mousemove only. The new x
* coordinate of the mouse
* @property {number} y - For mousemove type - the new y
* coordinate of the mouse and for mousescroll - represents the vertical
* scrolling diff value
* @property {number} button - 1(left), 2(middle) or 3 (right). Supported by
* mousedown, mouseup and mousedblclick types.
* @property {KEYS} key - Represents the key related to the event. Supported by
* keydown and keyup types.
* @property {KEYS[]} modifiers - Represents the modifier related to the event.
* Supported by keydown and keyup types.
* @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions.
* Represents the action related to the permissions event.
*
* Optional properties. Supported for permissions event for action === request:
* @property {string} userId - The user id of the participant that has sent the
* request.
* @property {string} userJID - The full JID in the MUC of the user that has
* sent the request.
* @property {string} displayName - the displayName of the participant that has
* sent the request.
* @property {boolean} screenSharing - true if the SS is started for the local
* participant and false if not.
*/

View File

@@ -0,0 +1,128 @@
// @flow
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { enableReceiver, stopReceiver } from './actions';
import { REMOTE_CONTROL_MESSAGE_NAME, EVENTS } from './constants';
import { keyboardEventToKey } from './keycodes';
import logger from './logger';
/**
* Checks if the remote contrrol is enabled.
*
* @param {*} state - The redux state.
* @returns {boolean} - True if the remote control is enabled and false otherwise.
*/
export function isRemoteControlEnabled(state: Object) {
return !state['features/base/config'].disableRemoteControl && JitsiMeetJS.isDesktopSharingEnabled();
}
/**
* Sends remote control message to other participant trough data channel.
*
* @param {JitsiConference} conference - The JitsiConference object.
* @param {string} to - The participant who will receive the event.
* @param {RemoteControlEvent} event - The remote control event.
* @returns {boolean} - True if the message was sent successfully and false otherwise.
*/
export function sendRemoteControlEndpointMessage(
conference: Object,
to: ?string,
event: Object) {
if (!to) {
logger.warn('Remote control: Skip sending remote control event. Params:', to);
return false;
}
try {
conference.sendEndpointMessage(to, {
name: REMOTE_CONTROL_MESSAGE_NAME,
...event
});
return true;
} catch (error) {
logger.error('Failed to send EndpointMessage via the datachannels', error);
return false;
}
}
/**
* Handles remote control events from the external app. Currently only
* events with type EVENTS.supported and EVENTS.stop are
* supported.
*
* @param {RemoteControlEvent} event - The remote control event.
* @param {Store} store - The redux store.
* @returns {void}
*/
export function onRemoteControlAPIEvent(event: Object, { getState, dispatch }: Object) {
switch (event.type) {
case EVENTS.supported:
logger.log('Remote Control supported.');
if (isRemoteControlEnabled(getState())) {
dispatch(enableReceiver());
} else {
logger.log('Remote Control disabled.');
}
break;
case EVENTS.stop: {
dispatch(stopReceiver());
break;
}
}
}
/**
* Returns the area used for capturing mouse and key events.
*
* @returns {JQuery} - A JQuery selector.
*/
export function getRemoteConrolEventCaptureArea() {
return VideoLayout.getLargeVideoWrapper();
}
/**
* Extract the keyboard key from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {KEYS} The key that is pressed or undefined.
*/
export function getKey(event: Object) {
return keyboardEventToKey(event);
}
/**
* Extract the modifiers from the keyboard event.
*
* @param {KeyboardEvent} event - The event.
* @returns {Array} With possible values: "shift", "control", "alt", "command".
*/
export function getModifiers(event: Object) {
const modifiers = [];
if (event.shiftKey) {
modifiers.push('shift');
}
if (event.ctrlKey) {
modifiers.push('control');
}
if (event.altKey) {
modifiers.push('alt');
}
if (event.metaKey) {
modifiers.push('command');
}
return modifiers;
}

View File

@@ -158,8 +158,9 @@ for (let i = 0; i < 26; i++) {
/**
* Returns key associated with the keyCode from the passed event.
* @param {KeyboardEvent} event the event
* @returns {KEYS} the key on the keyboard.
*
* @param {KeyboardEvent} event - The event.
* @returns {KEYS} - The key on the keyboard.
*/
export function keyboardEventToKey(event) {
return keyCodeToKey[event.which];

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../base/logging/functions';
export default getLogger('features/remote-control');

View File

@@ -0,0 +1,92 @@
// @flow
import { PostMessageTransportBackend, Transport } from '@jitsi/js-utils/transport';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { CONFERENCE_JOINED } from '../base/conference';
import { PARTICIPANT_LEFT } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import {
clearRequest, setReceiverTransport, setRemoteControlActive, stopController, stopReceiver
} from './actions';
import { REMOTE_CONTROL_MESSAGE_NAME } from './constants';
import { onRemoteControlAPIEvent } from './functions';
import './subscriber';
/**
* The redux middleware for the remote control feature.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => async action => {
switch (action.type) {
case APP_WILL_MOUNT: {
const { dispatch } = store;
dispatch(setReceiverTransport(new Transport({
backend: new PostMessageTransportBackend({
postisOptions: { scope: 'jitsi-remote-control' }
})
})));
break;
}
case APP_WILL_UNMOUNT: {
const { getState, dispatch } = store;
const { transport } = getState()['features/remote-control'].receiver;
if (transport) {
transport.dispose();
dispatch(setReceiverTransport());
}
break;
}
case CONFERENCE_JOINED: {
const result = next(action);
const { getState } = store;
const { transport } = getState()['features/remote-control'].receiver;
if (transport) {
// We expect here that even if we receive the supported event earlier
// it will be cached and we'll receive it.
transport.on('event', event => {
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
onRemoteControlAPIEvent(event, store);
return true;
}
return false;
});
}
return result;
}
case PARTICIPANT_LEFT: {
const { getState, dispatch } = store;
const state = getState();
const { id } = action.participant;
const { receiver, controller } = state['features/remote-control'];
const { requestedParticipant, controlled } = controller;
if (id === controlled) {
dispatch(stopController());
}
if (id === requestedParticipant) {
dispatch(clearRequest());
dispatch(setRemoteControlActive(false));
}
if (receiver?.controller === id) {
dispatch(stopReceiver(false, true));
}
break;
}
}
return next(action);
});

View File

@@ -0,0 +1,68 @@
import { ReducerRegistry, set } from '../base/redux';
import {
CAPTURE_EVENTS,
REMOTE_CONTROL_ACTIVE,
SET_CONTROLLED_PARTICIPANT,
SET_CONTROLLER,
SET_RECEIVER_ENABLED,
SET_RECEIVER_TRANSPORT,
SET_REQUESTED_PARTICIPANT
} from './actionTypes';
/**
* The default state.
*/
const DEFAULT_STATE = {
active: false,
controller: {
isCapturingEvents: false
},
receiver: {
enabled: false
}
};
/**
* Listen for actions that mutate the remote control state.
*/
ReducerRegistry.register(
'features/remote-control', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CAPTURE_EVENTS:
return {
...state,
controller: set(state.controller, 'isCapturingEvents', action.isCapturingEvents)
};
case REMOTE_CONTROL_ACTIVE:
return set(state, 'active', action.active);
case SET_RECEIVER_TRANSPORT:
return {
...state,
receiver: set(state.receiver, 'transport', action.transport)
};
case SET_RECEIVER_ENABLED:
return {
...state,
receiver: set(state.receiver, 'enabled', action.enabled)
};
case SET_REQUESTED_PARTICIPANT:
return {
...state,
controller: set(state.controller, 'requestedParticipant', action.requestedParticipant)
};
case SET_CONTROLLED_PARTICIPANT:
return {
...state,
controller: set(state.controller, 'controlled', action.controlled)
};
case SET_CONTROLLER:
return {
...state,
receiver: set(state.receiver, 'controller', action.controller)
};
}
return state;
},
);

View File

@@ -0,0 +1,33 @@
// @flow
import { StateListenerRegistry } from '../base/redux';
import { resume, pause } from './actions';
/**
* Listens for large video participant ID changes.
*/
StateListenerRegistry.register(
/* selector */ state => {
const { participantId } = state['features/large-video'];
const { controller } = state['features/remote-control'];
const { controlled } = controller;
if (!controlled) {
return undefined;
}
return controlled === participantId;
},
/* listener */ (isControlledParticipantOnStage, { dispatch }) => {
if (isControlledParticipantOnStage === true) {
dispatch(resume());
} else if (isControlledParticipantOnStage === false) {
dispatch(pause());
}
// else {
// isControlledParticipantOnStage === undefined. Ignore!
// }
}
);

View File

@@ -0,0 +1,102 @@
// @flow
import React from 'react';
import { Dialog } from '../../base/dialog';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantDialog, {
type Props as AbstractProps
} from './AbstractMuteRemoteParticipantDialog';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteEveryoneDialog}.
*/
export type Props = AbstractProps & {
content: string,
exclude: Array<string>,
title: string
};
/**
*
* An abstract Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteRemoteParticipantDialog
*/
export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRemoteParticipantDialog<P> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { content, title } = this.props;
return (
<Dialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleString = { title }
width = 'small'>
<div>
{ content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude));
return true;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryoneDialog}'s props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object, ownProps: Props) {
const { exclude, t } = ownProps;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => id === getLocalParticipant(state).id
? t('dialog.muteEveryoneSelf')
: getParticipantDisplayName(state, id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
} : {
content: t('dialog.muteEveryoneDialog'),
title: t('dialog.muteEveryoneTitle')
};
}

View File

@@ -0,0 +1,48 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { IconMuteEveryone } from '../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryoneDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to keep unmuted.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which mutes all the other participants.
*/
export default class AbstractMuteEveryoneElseButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElse';
icon = IconMuteEveryone;
label = 'videothumbnail.domuteOthers';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
}
}

View File

@@ -0,0 +1,67 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractMuteEveryoneDialog, {
abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the base/dialog feature.
*/
_dialogStyles: StyleType
}
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteEveryoneDialog
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
return (
<ConfirmDialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` }
</Text>
</ConfirmDialog>
);
}
_onSubmit: () => boolean;
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {{
* _dialogStyles: StyleType
* }}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
return {
...abstractMapStateToProps(state, ownProps),
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
};
}
export default translate(connect(_mapStateToProps)(MuteEveryoneDialog));

View File

@@ -0,0 +1,20 @@
// @flow
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneElseButton from '../AbstractMuteEveryoneElseButton';
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractMuteEveryoneElseButton));

View File

@@ -16,6 +16,7 @@ import { hideRemoteVideoMenu } from '../../actions';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import PinButton from './PinButton';
import styles from './styles';
@@ -104,6 +105,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
<GrantModeratorButton { ...buttonProps } />
<PinButton { ...buttonProps } />
<PrivateMessageButton { ...buttonProps } />
<MuteEveryoneElseButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@@ -1,12 +1,7 @@
// @flow
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';

View File

@@ -4,14 +4,13 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { IconKick } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractKickButton, {
type Props
} from '../AbstractKickButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
declare var interfaceConfig: Object;
/**
* Implements a React {@link Component} which displays a button for kicking out
* a participant from the conference.
@@ -56,4 +55,4 @@ class KickButton extends AbstractKickButton {
_handleClick: () => void
}
export default translate(KickButton);
export default translate(connect()(KickButton));

View File

@@ -5,53 +5,15 @@ import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { muteAllParticipants } from '../../actions';
import AbstractMuteRemoteParticipantDialog, {
type Props as AbstractProps
} from '../AbstractMuteRemoteParticipantDialog';
declare var APP: Object;
/**
* The type of the React {@code Component} props of
* {@link MuteEveryoneDialog}.
*/
type Props = AbstractProps & {
/**
* The IDs of the remote participants to exclude from being muted.
*/
exclude: Array<string>
};
/**
* Translations needed for dialog rendering.
*/
type Translations = {
/**
* Content text.
*/
content: string,
/**
* Title text.
*/
title: string
}
import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props } from '../AbstractMuteEveryoneDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting a remote participant.
* from the user before muting all remote participants.
*
* @extends Component
* @extends AbstractMuteEveryoneDialog
*/
class MuteEveryoneDialog extends AbstractMuteRemoteParticipantDialog<Props> {
static defaultProps = {
exclude: [],
muteLocal: false
};
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
/**
* Implements React's {@link Component#render()}.
*
@@ -59,64 +21,20 @@ class MuteEveryoneDialog extends AbstractMuteRemoteParticipantDialog<Props> {
* @returns {ReactElement}
*/
render() {
const { content, title } = this._getTranslations();
return (
<Dialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleString = { title }
titleString = { this.props.title }
width = 'small'>
<div>
{ content }
{ this.props.content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude));
return true;
}
/**
* Method to get translations depending on whether we have an exclusive
* mute or not.
*
* @returns {Translations}
* @private
*/
_getTranslations(): Translations {
const { exclude, t } = this.props;
const { conference } = APP;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => conference.isLocalId(id)
? t('dialog.muteEveryoneSelf')
: conference.getParticipantDisplayName(id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
} : {
content: t('dialog.muteEveryoneDialog'),
title: t('dialog.muteEveryoneTitle')
};
}
}
export default translate(connect()(MuteEveryoneDialog));
export default translate(connect(abstractMapStateToProps)(MuteEveryoneDialog));

View File

@@ -2,17 +2,13 @@
import React from 'react';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconMuteEveryoneElse } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteButton, {
_mapStateToProps,
import AbstractMuteEveryoneElseButton, {
type Props
} from '../AbstractMuteButton';
} from '../AbstractMuteEveryoneElseButton';
import MuteEveryoneDialog from './MuteEveryoneDialog';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
@@ -20,9 +16,9 @@ import RemoteVideoMenuButton from './RemoteVideoMenuButton';
* every participant in the conference except the one with the given
* participantID
*/
class MuteEveryoneElseButton extends AbstractMuteButton {
class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
/**
* Instantiates a new {@code MuteEveryoneElseButton}.
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
@@ -53,19 +49,6 @@ class MuteEveryoneElseButton extends AbstractMuteButton {
}
_handleClick: () => void;
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
}
}
export default translate(connect(_mapStateToProps)(MuteEveryoneElseButton));
export default translate(connect()(MuteEveryoneElseButton));

View File

@@ -4,15 +4,19 @@ import React, { Component } from 'react';
import { Icon, IconMenuThumb } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { isRemoteTrackMuted } from '../../../base/tracks';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import {
GrantModeratorButton,
MuteButton,
MuteEveryoneElseButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
@@ -49,6 +53,24 @@ type Props = {
*/
_isModerator: boolean,
/**
* The position relative to the trigger the remote menu should display
* from. Valid values are those supported by AtlasKit
* {@code InlineDialog}.
*/
_menuPosition: string,
/**
* The current state of the participant's remote control session.
*/
_remoteControlState: number,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* A value between 0 and 1 indicating the volume of the participant's
* audio element.
@@ -60,34 +82,16 @@ type Props = {
*/
onMenuDisplay: Function,
/**
* Callback to invoke choosing to start a remote control session with
* the participant.
*/
onRemoteControlToggle: Function,
/**
* Callback to invoke when changing the level of the participant's
* audio element.
*/
onVolumeChange: Function,
/**
* The position relative to the trigger the remote menu should display
* from. Valid values are those supported by AtlasKit
* {@code InlineDialog}.
*/
menuPosition: string,
/**
* The ID for the participant on which the remote video menu will act.
*/
participantID: string,
/**
* The current state of the participant's remote control session.
*/
remoteControlState: number
};
/**
@@ -137,7 +141,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
<Popover
content = { content }
onPopoverOpen = { this._onShowRemoteMenu }
position = { this.props.menuPosition }>
position = { this.props._menuPosition }>
<span
className = 'popover-trigger remote-video-menu-trigger'>
<Icon
@@ -174,10 +178,10 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
_disableRemoteMute,
_isAudioMuted,
_isModerator,
dispatch,
initialVolumeValue,
onRemoteControlToggle,
onVolumeChange,
remoteControlState,
_remoteControlState,
participantID
} = this.props;
@@ -213,13 +217,21 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
}
}
if (remoteControlState) {
if (_remoteControlState) {
let onRemoteControlToggle = null;
if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
onRemoteControlToggle = () => dispatch(stopController(true));
} else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID));
}
buttons.push(
<RemoteControlButton
key = 'remote-control'
onClick = { onRemoteControlToggle }
participantID = { participantID }
remoteControlState = { remoteControlState } />
remoteControlState = { _remoteControlState } />
);
}
@@ -257,7 +269,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {{
* _isModerator: boolean
* _isAudioMuted: boolean,
* _isModerator: boolean,
* _disableKick: boolean,
* _disableRemoteMute: boolean,
* _menuPosition: string,
* _remoteControlState: number
* }}
*/
function _mapStateToProps(state, ownProps) {
@@ -266,12 +283,46 @@ function _mapStateToProps(state, ownProps) {
const localParticipant = getLocalParticipant(state);
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const { disableKick } = remoteVideoMenu;
let _remoteControlState = null;
const participant = getParticipantById(state, participantID);
const _isRemoteControlSessionActive = participant?.remoteControlSessionStatus ?? false;
const _supportsRemoteControl = participant?.supportsRemoteControl ?? false;
const { active, controller } = state['features/remote-control'];
const { requestedParticipant, controlled } = controller;
const activeParticipant = requestedParticipant || controlled;
if (_supportsRemoteControl
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
if (requestedParticipant === participantID) {
_remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
} else if (controlled) {
_remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
} else {
_remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
}
}
const currentLayout = getCurrentLayout(state);
let _menuPosition;
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
_menuPosition = 'left top';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
_menuPosition = 'left bottom';
break;
default:
_menuPosition = 'top center';
}
return {
_isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute)
_disableRemoteMute: Boolean(disableRemoteMute),
_remoteControlState,
_menuPosition
};
}

View File

@@ -1,26 +1,15 @@
// @flow
export { default as GrantModeratorButton } from './GrantModeratorButton';
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickButton } from './KickButton';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export {
REMOTE_CONTROL_MENU_STATES,
default as RemoteControlButton
} from './RemoteControlButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
export {
default as RemoteVideoMenuTriggerButton
} from './RemoteVideoMenuTriggerButton';
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
export { default as VolumeSlider } from './VolumeSlider';

View File

@@ -104,7 +104,7 @@ class PasswordRequiredPrompt extends Component<Props, State> {
name = 'lockKey'
onChange = { this._onPasswordChanged }
shouldFitContainer = { true }
type = 'text'
type = 'password'
value = { this.state.password } />
</div>
);

View File

@@ -5,22 +5,14 @@ import { translate } from '../../../base/i18n';
import { IconSettings } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { openDeviceSelectionPopup } from '../../../device-selection';
import { openSettingsDialog } from '../../actions';
import { SETTINGS_TABS } from '../../constants';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link SettingsButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether we are in filmstrip only mode or not.
*/
_filmstripOnly: boolean,
/**
* The default tab at which the settings dialog will be opened.
*/
@@ -49,36 +41,12 @@ class SettingsButton extends AbstractButton<Props, *> {
*/
_handleClick() {
const {
_filmstripOnly,
defaultTab = SETTINGS_TABS.DEVICES,
dispatch } = this.props;
sendAnalytics(createToolbarEvent('settings'));
if (_filmstripOnly) {
dispatch(openDeviceSelectionPopup());
} else {
dispatch(openSettingsDialog(defaultTab));
}
dispatch(openSettingsDialog(defaultTab));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code SettingsButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _filmstripOnly: boolean
* }}
*/
function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will.
return {
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly)
};
}
export default translate(connect(_mapStateToProps)(SettingsButton));
export default translate(connect()(SettingsButton));

View File

@@ -25,10 +25,6 @@ export * from './actions.native';
*/
export function dockToolbox(dock: boolean): Function {
return (dispatch: Dispatch<any>, getState: Function) => {
if (interfaceConfig.filmStripOnly) {
return;
}
const { timeoutMS, visible } = getState()['features/toolbox'];
if (dock) {

View File

@@ -1,13 +1,13 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconMuteEveryone } from '../../../base/icons';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { MuteEveryoneDialog } from '../../../remote-video-menu';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { IconMuteEveryone } from '../../base/icons';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryoneDialog } from '../../remote-video-menu/components';
type Props = AbstractButtonProps & {

View File

@@ -19,6 +19,7 @@ import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import { VideoShareButton } from '../../../youtube-player/components';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
import AudioOnlyButton from './AudioOnlyButton';
import MoreOptionsButton from './MoreOptionsButton';
@@ -143,6 +144,7 @@ class OverflowMenu extends PureComponent<Props, State> {
<RoomLockButton { ...buttonProps } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<MuteEveryoneButton { ...buttonProps } />
<HelpButton { ...buttonProps } />
</Collapsible>
</BottomSheet>

View File

@@ -79,9 +79,9 @@ import { isToolboxVisible } from '../../functions';
import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
import AudioSettingsButton from './AudioSettingsButton';
import MuteEveryoneButton from './MuteEveryoneButton';
import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import ToolbarButton from './ToolbarButton';
@@ -1221,7 +1221,7 @@ class Toolbox extends Component<Props, State> {
t
} = this.props;
const overflowMenuContent = this._renderOverflowMenuContent();
const overflowHasItems = Boolean(overflowMenuContent.filter(child => child).length);
const overflowHasItems = Boolean(overflowMenuContent.length);
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const buttonsLeft = [];
const buttonsRight = [];
@@ -1232,10 +1232,10 @@ class Toolbox extends Component<Props, State> {
let minSpaceBetweenButtons = 48;
let widthPlusPaddingOfButton = 56;
if (this.state.windowWidth <= verySmallThreshold) {
if (this.state.windowWidth <= verySmallThreshold && !isMobileBrowser()) {
minSpaceBetweenButtons = 26;
widthPlusPaddingOfButton = 28;
} else if (this.state.windowWidth <= smallThreshold) {
} else if (this.state.windowWidth <= smallThreshold && !isMobileBrowser()) {
minSpaceBetweenButtons = 36;
widthPlusPaddingOfButton = 40;
}
@@ -1250,8 +1250,6 @@ class Toolbox extends Component<Props, State> {
/ 2 // divide by the number of groups(left and right group)
);
const showOverflowMenu = this.state.windowWidth >= verySmallThreshold || isMobileBrowser();
if (this._shouldShowButton('chat')) {
buttonsLeft.push('chat');
}
@@ -1265,7 +1263,7 @@ class Toolbox extends Component<Props, State> {
if (this._shouldShowButton('closedcaptions')) {
buttonsLeft.push('closedcaptions');
}
if (overflowHasItems && showOverflowMenu) {
if (overflowHasItems) {
buttonsRight.push('overflowmenu');
}
if (this._shouldShowButton('invite')) {
@@ -1288,13 +1286,13 @@ class Toolbox extends Component<Props, State> {
movedButtons.push(...buttonsLeft.splice(
maxNumberOfButtonsPerGroup,
buttonsLeft.length - maxNumberOfButtonsPerGroup));
if (buttonsRight.indexOf('overflowmenu') === -1 && showOverflowMenu) {
if (buttonsRight.indexOf('overflowmenu') === -1) {
buttonsRight.unshift('overflowmenu');
}
}
if (buttonsRight.length > maxNumberOfButtonsPerGroup) {
if (buttonsRight.indexOf('overflowmenu') === -1 && showOverflowMenu) {
if (buttonsRight.indexOf('overflowmenu') === -1) {
buttonsRight.unshift('overflowmenu');
}

View File

@@ -90,6 +90,8 @@ export function shouldDisplayTileView(state: Object = {}) {
return tileViewEnabled;
}
const { iAmRecorder } = state['features/base/config'];
// None tile view mode is easier to calculate (no need for many negations), so we do
// that and negate it only once.
const shouldDisplayNormalMode = Boolean(
@@ -99,9 +101,6 @@ export function shouldDisplayTileView(state: Object = {}) {
// Editing etherpad
state['features/etherpad']?.editing
// We're in filmstrip-only mode
|| (typeof interfaceConfig === 'object' && interfaceConfig?.filmStripOnly)
// We pinned a participant
|| getPinnedParticipant(state)
@@ -110,6 +109,9 @@ export function shouldDisplayTileView(state: Object = {}) {
// There is a shared YouTube video in the meeting
|| isYoutubeVideoPlaying(state)
// We want jibri to use stage view by default
|| iAmRecorder
);
return !shouldDisplayNormalMode;

View File

@@ -24,7 +24,7 @@ end
-- -> true, room_name, subdomain
-- -> true, room_name, nil (if no subdomain is used for the room)
local function is_moderated(room_jid)
if #moderated_subdomains == 0 and #moderated_rooms == 0 then
if moderated_subdomains:empty() and moderated_rooms:empty() then
return false;
end

View File

@@ -39,7 +39,7 @@ function filter_stanza(stanza)
end
function filter_session(session)
module:log("warn", "Session filters applied");
-- module:log("warn", "Session filters applied");
filters.add_filter(session, "stanzas/out", filter_stanza);
end

View File

@@ -239,7 +239,7 @@ end
function extract_subdomain(room_node)
-- optimization, skip matching if there is no subdomain, no [subdomain] part in the beginning of the node
if not starts_with(room_node, '[') then
return room_node;
return nil,room_node;
end
return room_node:match("^%[([^%]]+)%](.+)$");

View File

@@ -1,8 +0,0 @@
/**
* Events fired from the remote control module through the EventEmitter.
*/
/**
* Notifies about remote control active session status changes.
*/
export const ACTIVE_CHANGED = 'active-changed';