mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-12 17:50:19 +00:00
Compare commits
16 Commits
4530
...
remote-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9235000a7f | ||
|
|
d97f46c163 | ||
|
|
5510138944 | ||
|
|
29fa4c935e | ||
|
|
7de1e6d89e | ||
|
|
c4ef7d8601 | ||
|
|
9379bb3c5b | ||
|
|
101a40a8da | ||
|
|
36871fa37e | ||
|
|
c09ed4c8ef | ||
|
|
08dce76763 | ||
|
|
d03173e827 | ||
|
|
12c835dd91 | ||
|
|
f6127d45e9 | ||
|
|
9219e80a2a | ||
|
|
71fb5aef6c |
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Notifies interested parties that hangup procedure will start.
|
||||
*/
|
||||
export const BEFORE_HANGUP = 'conference.before_hangup';
|
||||
2
app.js
2
app.js
@@ -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
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -678,7 +678,6 @@ var config = {
|
||||
forceJVB121Ratio
|
||||
hiddenDomain
|
||||
ignoreStartMuted
|
||||
nick
|
||||
startBitrate
|
||||
*/
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -224,6 +224,7 @@ body.welcome-page {
|
||||
&.without-content {
|
||||
.welcome-card {
|
||||
min-width: 500px;
|
||||
max-width: 580px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
height: 180,
|
||||
parentNode: undefined,
|
||||
configOverwrite: {},
|
||||
interfaceConfigOverwrite: {
|
||||
filmStripOnly: true
|
||||
}
|
||||
interfaceConfigOverwrite: {}
|
||||
}
|
||||
var api = new JitsiMeetExternalAPI(domain, options);
|
||||
</script>
|
||||
|
||||
@@ -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,
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
112
modules/UI/UI.js
112
modules/UI/UI.js
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
'extends': '../../react/.eslintrc.js'
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -128,7 +128,6 @@ export default [
|
||||
'localRecording',
|
||||
'maxFullResolutionParticipants',
|
||||
'minParticipants',
|
||||
'nick',
|
||||
'openBridgeChannel',
|
||||
'opusMaxAverageBitrate',
|
||||
'p2p',
|
||||
|
||||
@@ -54,6 +54,5 @@ export default [
|
||||
'UNSUPPORTED_BROWSERS',
|
||||
'VERTICAL_FILMSTRIP',
|
||||
'VIDEO_LAYOUT_FIT',
|
||||
'VIDEO_QUALITY_LABEL_DISABLED',
|
||||
'filmStripOnly'
|
||||
'VIDEO_QUALITY_LABEL_DISABLED'
|
||||
];
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
5
react/features/base/participants/logger.js
Normal file
5
react/features/base/participants/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/participants');
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
@@ -56,9 +56,6 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|
||||
|| ((pinnedParticipant = getPinnedParticipant(state))
|
||||
&& pinnedParticipant.local)))
|
||||
|
||||
|| (typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.filmStripOnly)
|
||||
|
||||
|| state['features/base/config'].disable1On1Mode);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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'>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
70
react/features/remote-control/actionTypes.js
Normal file
70
react/features/remote-control/actionTypes.js
Normal 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';
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
128
react/features/remote-control/functions.js
Normal file
128
react/features/remote-control/functions.js
Normal 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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
5
react/features/remote-control/logger.js
Normal file
5
react/features/remote-control/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/remote-control');
|
||||
92
react/features/remote-control/middleware.js
Normal file
92
react/features/remote-control/middleware.js
Normal 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);
|
||||
});
|
||||
68
react/features/remote-control/reducer.js
Normal file
68
react/features/remote-control/reducer.js
Normal 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;
|
||||
},
|
||||
);
|
||||
33
react/features/remote-control/subscriber.js
Normal file
33
react/features/remote-control/subscriber.js
Normal 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!
|
||||
// }
|
||||
}
|
||||
);
|
||||
@@ -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')
|
||||
};
|
||||
}
|
||||
@@ -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 ] }));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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("^%[([^%]]+)%](.+)$");
|
||||
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user