feat(prejoin_page): Add prejoin page
This commit is contained in:
parent
5b53232964
commit
a45cbf41ef
310
conference.js
310
conference.js
|
@ -27,6 +27,13 @@ import {
|
|||
redirectToStaticPage,
|
||||
reloadWithStoredParams
|
||||
} from './react/features/app';
|
||||
import {
|
||||
initPrejoin,
|
||||
isPrejoinPageEnabled,
|
||||
isPrejoinPageVisible,
|
||||
replacePrejoinAudioTrack,
|
||||
replacePrejoinVideoTrack
|
||||
} from './react/features/prejoin';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
|
@ -133,6 +140,15 @@ const eventEmitter = new EventEmitter();
|
|||
let room;
|
||||
let connection;
|
||||
|
||||
/**
|
||||
* The promise is used when the prejoin screen is shown.
|
||||
* While the user configures the devices the connection can be made.
|
||||
*
|
||||
* @type {Promise<Object>}
|
||||
* @private
|
||||
*/
|
||||
let _connectionPromise;
|
||||
|
||||
/**
|
||||
* This promise is used for chaining mutePresenterVideo calls in order to avoid calling GUM multiple times if it takes
|
||||
* a while to finish.
|
||||
|
@ -471,28 +487,13 @@ export default {
|
|||
localVideo: null,
|
||||
|
||||
/**
|
||||
* Creates local media tracks and connects to a room. Will show error
|
||||
* dialogs in case accessing the local microphone and/or camera failed. Will
|
||||
* show guidance overlay for users on how to give access to camera and/or
|
||||
* microphone.
|
||||
* @param {string} roomName
|
||||
* @param {object} options
|
||||
* @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
|
||||
* only audio track will be created and the audio only mode will be turned
|
||||
* on.
|
||||
* @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
|
||||
* should start with screensharing instead of camera video.
|
||||
* @param {boolean} options.startWithAudioMuted - will start the conference
|
||||
* without any audio tracks.
|
||||
* @param {boolean} options.startWithVideoMuted - will start the conference
|
||||
* without any video tracks.
|
||||
* @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
|
||||
* Returns an object containing a promise which resolves with the created tracks &
|
||||
* the errors resulting from that process.
|
||||
*
|
||||
* @returns {Promise<JitsiLocalTrack[]>, Object}
|
||||
*/
|
||||
createInitialLocalTracksAndConnect(roomName, options = {}) {
|
||||
let audioAndVideoError,
|
||||
audioOnlyError,
|
||||
screenSharingError,
|
||||
videoOnlyError;
|
||||
createInitialLocalTracks(options = {}) {
|
||||
const errors = {};
|
||||
const initialDevices = [ 'audio' ];
|
||||
const requestedAudio = true;
|
||||
let requestedVideo = false;
|
||||
|
@ -524,7 +525,7 @@ export default {
|
|||
// FIXME is there any simpler way to rewrite this spaghetti below ?
|
||||
if (options.startScreenSharing) {
|
||||
tryCreateLocalTracks = this._createDesktopTrack()
|
||||
.then(desktopStream => {
|
||||
.then(([ desktopStream ]) => {
|
||||
if (!requestedAudio) {
|
||||
return [ desktopStream ];
|
||||
}
|
||||
|
@ -533,21 +534,21 @@ export default {
|
|||
.then(([ audioStream ]) =>
|
||||
[ desktopStream, audioStream ])
|
||||
.catch(error => {
|
||||
audioOnlyError = error;
|
||||
errors.audioOnlyError = error;
|
||||
|
||||
return [ desktopStream ];
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('Failed to obtain desktop stream', error);
|
||||
screenSharingError = error;
|
||||
errors.screenSharingError = error;
|
||||
|
||||
return requestedAudio
|
||||
? createLocalTracksF({ devices: [ 'audio' ] }, true)
|
||||
: [];
|
||||
})
|
||||
.catch(error => {
|
||||
audioOnlyError = error;
|
||||
errors.audioOnlyError = error;
|
||||
|
||||
return [];
|
||||
});
|
||||
|
@ -560,16 +561,16 @@ export default {
|
|||
if (requestedAudio && requestedVideo) {
|
||||
|
||||
// Try audio only...
|
||||
audioAndVideoError = err;
|
||||
errors.audioAndVideoError = err;
|
||||
|
||||
return (
|
||||
createLocalTracksF({ devices: [ 'audio' ] }, true));
|
||||
} else if (requestedAudio && !requestedVideo) {
|
||||
audioOnlyError = err;
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
return [];
|
||||
} else if (requestedVideo && !requestedAudio) {
|
||||
videoOnlyError = err;
|
||||
errors.videoOnlyError = err;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
@ -580,7 +581,7 @@ export default {
|
|||
if (!requestedAudio) {
|
||||
logger.error('The impossible just happened', err);
|
||||
}
|
||||
audioOnlyError = err;
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
// Try video only...
|
||||
return requestedVideo
|
||||
|
@ -592,7 +593,7 @@ export default {
|
|||
if (!requestedVideo) {
|
||||
logger.error('The impossible just happened', err);
|
||||
}
|
||||
videoOnlyError = err;
|
||||
errors.videoOnlyError = err;
|
||||
|
||||
return [];
|
||||
});
|
||||
|
@ -603,8 +604,44 @@ export default {
|
|||
// cases, when auth is rquired, for instance, that won't happen until
|
||||
// the user inputs their credentials, but the dialog would be
|
||||
// overshadowed by the overlay.
|
||||
tryCreateLocalTracks.then(() =>
|
||||
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false)));
|
||||
tryCreateLocalTracks.then(tracks => {
|
||||
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
|
||||
|
||||
return tracks;
|
||||
});
|
||||
|
||||
return {
|
||||
tryCreateLocalTracks,
|
||||
errors
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates local media tracks and connects to a room. Will show error
|
||||
* dialogs in case accessing the local microphone and/or camera failed. Will
|
||||
* show guidance overlay for users on how to give access to camera and/or
|
||||
* microphone.
|
||||
* @param {string} roomName
|
||||
* @param {object} options
|
||||
* @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
|
||||
* only audio track will be created and the audio only mode will be turned
|
||||
* on.
|
||||
* @param {boolean} options.startScreenSharing=false - if <tt>true</tt>
|
||||
* should start with screensharing instead of camera video.
|
||||
* @param {boolean} options.startWithAudioMuted - will start the conference
|
||||
* without any audio tracks.
|
||||
* @param {boolean} options.startWithVideoMuted - will start the conference
|
||||
* without any video tracks.
|
||||
* @returns {Promise.<JitsiLocalTrack[], JitsiConnection>}
|
||||
*/
|
||||
createInitialLocalTracksAndConnect(roomName, options = {}) {
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
|
||||
const {
|
||||
audioAndVideoError,
|
||||
audioOnlyError,
|
||||
screenSharingError,
|
||||
videoOnlyError
|
||||
} = errors;
|
||||
|
||||
return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
|
||||
.then(([ tracks, con ]) => {
|
||||
|
@ -636,105 +673,132 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
startConference(con, tracks) {
|
||||
tracks.forEach(track => {
|
||||
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
||||
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
|
||||
const mediaType = track.getType();
|
||||
|
||||
sendAnalytics(
|
||||
createTrackMutedEvent(mediaType, 'initial mute'));
|
||||
logger.log(`${mediaType} mute: initially muted.`);
|
||||
track.mute();
|
||||
}
|
||||
});
|
||||
logger.log(`Initialized with ${tracks.length} local tracks`);
|
||||
|
||||
this._localTracksInitialized = true;
|
||||
con.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _connectionFailedHandler);
|
||||
APP.connection = connection = con;
|
||||
|
||||
// Desktop sharing related stuff:
|
||||
this.isDesktopSharingEnabled
|
||||
= JitsiMeetJS.isDesktopSharingEnabled();
|
||||
eventEmitter.emit(JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, this.isDesktopSharingEnabled);
|
||||
|
||||
APP.store.dispatch(
|
||||
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
|
||||
|
||||
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,
|
||||
// so that the user can try unmute later on and add audio/video
|
||||
// to the conference
|
||||
if (!tracks.find(t => t.isAudioTrack())) {
|
||||
this.setAudioMuteStatus(true);
|
||||
}
|
||||
|
||||
if (!tracks.find(t => t.isVideoTrack())) {
|
||||
this.setVideoMuteStatus(true);
|
||||
}
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
this.recorder = new Recorder();
|
||||
}
|
||||
|
||||
if (config.startSilent) {
|
||||
sendAnalytics(createStartSilentEvent());
|
||||
APP.store.dispatch(showNotification({
|
||||
descriptionKey: 'notify.startSilentDescription',
|
||||
titleKey: 'notify.startSilentTitle'
|
||||
}));
|
||||
}
|
||||
|
||||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
(new ConferenceConnector(resolve, reject)).connect();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open new connection and join to the conference.
|
||||
* @param {object} options
|
||||
* @param {string} roomName - The name of the conference.
|
||||
* Open new connection and join the conference when prejoin page is not enabled.
|
||||
* If prejoin page is enabled open an new connection in the background
|
||||
* and create local tracks.
|
||||
*
|
||||
* @param {{ roomName: string }} options
|
||||
* @returns {Promise}
|
||||
*/
|
||||
init(options) {
|
||||
this.roomName = options.roomName;
|
||||
async init({ roomName }) {
|
||||
const initialOptions = {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
startScreenSharing: config.startScreenSharing,
|
||||
startWithAudioMuted: config.startWithAudioMuted
|
||||
|| config.startSilent
|
||||
|| isUserInteractionRequiredForUnmute(APP.store.getState()),
|
||||
startWithVideoMuted: config.startWithVideoMuted
|
||||
|| isUserInteractionRequiredForUnmute(APP.store.getState())
|
||||
};
|
||||
|
||||
this.roomName = roomName;
|
||||
|
||||
window.addEventListener('hashchange', this.onHashChange.bind(this), false);
|
||||
|
||||
return (
|
||||
|
||||
try {
|
||||
// Initialize the device list first. This way, when creating tracks
|
||||
// based on preferred devices, loose label matching can be done in
|
||||
// cases where the exact ID match is no longer available, such as
|
||||
// when the camera device has switched USB ports.
|
||||
// when in startSilent mode we want to start with audio muted
|
||||
this._initDeviceList()
|
||||
.catch(error => logger.warn(
|
||||
'initial device list initialization failed', error))
|
||||
.then(() => this.createInitialLocalTracksAndConnect(
|
||||
options.roomName, {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
startScreenSharing: config.startScreenSharing,
|
||||
startWithAudioMuted: config.startWithAudioMuted
|
||||
|| config.startSilent
|
||||
|| isUserInteractionRequiredForUnmute(APP.store.getState()),
|
||||
startWithVideoMuted: config.startWithVideoMuted
|
||||
|| isUserInteractionRequiredForUnmute(APP.store.getState())
|
||||
}))
|
||||
.then(([ tracks, con ]) => {
|
||||
tracks.forEach(track => {
|
||||
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
||||
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
|
||||
const mediaType = track.getType();
|
||||
await this._initDeviceList();
|
||||
} catch (error) {
|
||||
logger.warn('initial device list initialization failed', error);
|
||||
}
|
||||
|
||||
sendAnalytics(
|
||||
createTrackMutedEvent(mediaType, 'initial mute'));
|
||||
logger.log(`${mediaType} mute: initially muted.`);
|
||||
track.mute();
|
||||
}
|
||||
});
|
||||
logger.log(`initialized with ${tracks.length} local tracks`);
|
||||
this._localTracksInitialized = true;
|
||||
con.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||
_connectionFailedHandler);
|
||||
APP.connection = connection = con;
|
||||
if (isPrejoinPageEnabled(APP.store.getState())) {
|
||||
_connectionPromise = connect(roomName);
|
||||
|
||||
// Desktop sharing related stuff:
|
||||
this.isDesktopSharingEnabled
|
||||
= JitsiMeetJS.isDesktopSharingEnabled();
|
||||
eventEmitter.emit(
|
||||
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
|
||||
this.isDesktopSharingEnabled);
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
|
||||
const tracks = await tryCreateLocalTracks;
|
||||
|
||||
APP.store.dispatch(
|
||||
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
|
||||
// Initialize device list a second time to ensure device labels
|
||||
// get populated in case of an initial gUM acceptance; otherwise
|
||||
// they may remain as empty strings.
|
||||
this._initDeviceList(true);
|
||||
|
||||
this._createRoom(tracks);
|
||||
APP.remoteControl.init();
|
||||
return APP.store.dispatch(initPrejoin(tracks, errors));
|
||||
}
|
||||
|
||||
// if user didn't give access to mic or camera or doesn't have
|
||||
// them at all, we mark corresponding toolbar buttons as muted,
|
||||
// so that the user can try unmute later on and add audio/video
|
||||
// to the conference
|
||||
if (!tracks.find(t => t.isAudioTrack())) {
|
||||
this.setAudioMuteStatus(true);
|
||||
}
|
||||
const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(
|
||||
roomName, initialOptions);
|
||||
|
||||
if (!tracks.find(t => t.isVideoTrack())) {
|
||||
this.setVideoMuteStatus(true);
|
||||
}
|
||||
this._initDeviceList(true);
|
||||
|
||||
// Initialize device list a second time to ensure device labels
|
||||
// get populated in case of an initial gUM acceptance; otherwise
|
||||
// they may remain as empty strings.
|
||||
this._initDeviceList(true);
|
||||
return this.startConference(con, tracks);
|
||||
},
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
this.recorder = new Recorder();
|
||||
}
|
||||
/**
|
||||
* Joins conference after the tracks have been configured in the prejoin screen.
|
||||
*
|
||||
* @param {Object[]} tracks - An array with the configured tracks
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async prejoinStart(tracks) {
|
||||
const con = await _connectionPromise;
|
||||
|
||||
if (config.startSilent) {
|
||||
sendAnalytics(createStartSilentEvent());
|
||||
APP.store.dispatch(showNotification({
|
||||
descriptionKey: 'notify.startSilentDescription',
|
||||
titleKey: 'notify.startSilentTitle'
|
||||
}));
|
||||
}
|
||||
|
||||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
(new ConferenceConnector(resolve, reject)).connect();
|
||||
});
|
||||
})
|
||||
);
|
||||
return this.startConference(con, tracks);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1352,6 +1416,18 @@ export default {
|
|||
useVideoStream(newStream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
_replaceLocalVideoTrackQueue.enqueue(onFinish => {
|
||||
/**
|
||||
* When the prejoin page is visible there is no conference object
|
||||
* created. The prejoin tracks are managed separately,
|
||||
* so this updates the prejoin video track.
|
||||
*/
|
||||
if (isPrejoinPageVisible(APP.store.getState())) {
|
||||
return APP.store.dispatch(replacePrejoinVideoTrack(newStream))
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.then(onFinish);
|
||||
}
|
||||
|
||||
APP.store.dispatch(
|
||||
replaceLocalTrack(this.localVideo, newStream, room))
|
||||
.then(() => {
|
||||
|
@ -1405,6 +1481,18 @@ export default {
|
|||
useAudioStream(newStream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
_replaceLocalAudioTrackQueue.enqueue(onFinish => {
|
||||
/**
|
||||
* When the prejoin page is visible there is no conference object
|
||||
* created. The prejoin tracks are managed separately,
|
||||
* so this updates the prejoin audio stream.
|
||||
*/
|
||||
if (isPrejoinPageVisible(APP.store.getState())) {
|
||||
return APP.store.dispatch(replacePrejoinAudioTrack(newStream))
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.then(onFinish);
|
||||
}
|
||||
|
||||
APP.store.dispatch(
|
||||
replaceLocalTrack(this.localAudio, newStream, room))
|
||||
.then(() => {
|
||||
|
|
|
@ -289,6 +289,9 @@ var config = {
|
|||
// and microsoftApiApplicationClientID
|
||||
// enableCalendarIntegration: false,
|
||||
|
||||
// When 'true', it shows an intermediate page before joining, where the user can configure its devices.
|
||||
// prejoinPageEnabled: false,
|
||||
|
||||
// Stats
|
||||
//
|
||||
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
.prejoin {
|
||||
&-full-page {
|
||||
background: #1C2025;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: $toolbarZ + 1;
|
||||
}
|
||||
|
||||
&-input-area-container {
|
||||
position: absolute;
|
||||
bottom: 128px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-input-area {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&-btn {
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 16px;
|
||||
padding: 7px 16px;
|
||||
text-align: center;
|
||||
width: 286px;
|
||||
|
||||
&--primary {
|
||||
background: #0376DA;
|
||||
border: 1px solid #0376DA;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: #2A3A4B;
|
||||
border: 1px solid #5E6D7A;
|
||||
}
|
||||
|
||||
&--text {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-text-btns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-input-label {
|
||||
color: #A4B8D1;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: 32px 0 8px 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin name-placeholder {
|
||||
color: #fff;
|
||||
font-weight: 300;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.prejoin-preview {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
&--no-video {
|
||||
background: radial-gradient(50% 50% at 50% 50%, #5B6F80 0%, #365067 100%), #FFFFFF;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-video {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-name {
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
line-height: 28px;
|
||||
|
||||
&--editable {
|
||||
background: none;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #D1DBE8;
|
||||
margin: 24px 0 16px 0;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
@include name-placeholder;
|
||||
}
|
||||
&::-moz-placeholder {
|
||||
@include name-placeholder;
|
||||
}
|
||||
&:-ms-input-placeholder {
|
||||
@include name-placeholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-avatar.avatar {
|
||||
background: #A4B8D1;
|
||||
margin: 200px auto 0 auto;
|
||||
}
|
||||
|
||||
&-btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
&> div {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.settings-button-small-icon {
|
||||
right: -8px;
|
||||
|
||||
&--hovered {
|
||||
right: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-overlay {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), linear-gradient(360deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 54.25%);
|
||||
}
|
||||
|
||||
&-status {
|
||||
align-items: center;
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
min-height: 24px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
&--warning {
|
||||
background: rgba(241, 173, 51, 0.5)
|
||||
}
|
||||
&--ok {
|
||||
background: rgba(49, 183, 106, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&-error-desc {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.settings-button-container {
|
||||
width: 49px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.prejoin-copy {
|
||||
&-meeting {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-url {
|
||||
max-width: 278px;
|
||||
padding: 8px 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-badge {
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
line-height: 38px;
|
||||
position: absolute;
|
||||
padding-left: 10px;
|
||||
text-align: left;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
&--hover {
|
||||
background: #1C2025;
|
||||
}
|
||||
|
||||
&--done {
|
||||
background: #31B76A;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
|
||||
&--white {
|
||||
&> svg > path {
|
||||
fill: #fff
|
||||
}
|
||||
}
|
||||
|
||||
&--light {
|
||||
&> svg > path {
|
||||
fill: #D1DBE8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-textarea {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
}
|
|
@ -57,6 +57,7 @@
|
|||
width: 16px;
|
||||
|
||||
&> svg {
|
||||
fill: #5e6d7a;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
.video-preview {
|
||||
background: none;
|
||||
max-height: 290px;
|
||||
overflow: auto;
|
||||
|
||||
&-container {
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&-entry {
|
||||
cursor: pointer;
|
||||
|
@ -61,6 +65,6 @@
|
|||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div > div:nth-child(2) > div > div {
|
||||
outline: none;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,5 +90,6 @@ $flagsImagePath: "../images/";
|
|||
@import 'meter';
|
||||
@import 'audio-preview';
|
||||
@import 'video-preview';
|
||||
@import 'prejoin';
|
||||
|
||||
/* Modules END */
|
||||
|
|
|
@ -476,6 +476,33 @@
|
|||
"passwordSetRemotely": "set by another participant",
|
||||
"passwordDigitsOnly": "Up to {{number}} digits",
|
||||
"poweredby": "powered by",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Audio and video error:",
|
||||
"audioOnlyError": "Audio error:",
|
||||
"audioTrackError": "Could not create audio track.",
|
||||
"callMe": "Call me",
|
||||
"callMeAtNumber": "Call me at this number:",
|
||||
"configuringDevices": "Configuring devices...",
|
||||
"connectedWithAudioQ": "You’re connected with audio?",
|
||||
"copyAndShare": "Copy & share meeting link",
|
||||
"dialInMeeting": "Dial into the meeting",
|
||||
"dialInPin": "Dial into the meeting and enter PIN code:",
|
||||
"dialing": "Dialing",
|
||||
"iWantToDialIn": "I want to dial in",
|
||||
"joinAudioByPhone": "Join with phone audio",
|
||||
"joinMeeting": "Join meeting",
|
||||
"joinWithoutAudio": "Join without audio",
|
||||
"initiated": "Call initiated",
|
||||
"linkCopied": "Link copied to clipboard",
|
||||
"lookGood": "Speaker and microphone look good",
|
||||
"or": "or",
|
||||
"calling": "Calling",
|
||||
"startWithPhone": "Start with phone audio",
|
||||
"screenSharingError": "Screen sharing error:",
|
||||
"videoOnlyError": "Video error:",
|
||||
"videoTrackError": "Could not create video track.",
|
||||
"viewAllNumbers": "view all numbers"
|
||||
},
|
||||
"presenceStatus": {
|
||||
"busy": "Busy",
|
||||
"calling": "Calling...",
|
||||
|
|
|
@ -246,6 +246,16 @@ export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
|
|||
return selectedLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored room name.
|
||||
*
|
||||
* @param {Object} state - The current state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRoomName(state: Object): string {
|
||||
return state['features/base/conference'].room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
|
||||
* manipulating a conference participant (e.g. Pin or select participant).
|
||||
|
|
|
@ -131,6 +131,7 @@ export default [
|
|||
'p2p',
|
||||
'pcStatsInterval',
|
||||
'preferH264',
|
||||
'prejoinPageEnabled',
|
||||
'requireDisplayName',
|
||||
'remoteVideoMenu',
|
||||
'resolution',
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE
|
||||
} from './actionTypes';
|
||||
import { replaceAudioTrackById, replaceVideoTrackById } from '../../prejoin/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { showNotification, showWarningNotification } from '../../notifications';
|
||||
import { updateSettings } from '../settings';
|
||||
import { formatDeviceLabel, setAudioOutputDeviceId } from './functions';
|
||||
|
@ -98,10 +100,18 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
}
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceAudioTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||
}
|
||||
break;
|
||||
case SET_VIDEO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceVideoTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||
}
|
||||
break;
|
||||
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
|
||||
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
|
||||
|
@ -111,7 +121,6 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
return next(action);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Does extra sync up on properties that may need to be updated after the
|
||||
* conference was joined.
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5223 18.2701C16.9328 18.6395 16.9661 19.2718 16.5966 19.6823C16.2272 20.0928 15.5949 20.1261 15.1844 19.7567L7.31769 12.6767C6.87631 12.2794 6.87631 11.5873 7.31769 11.1901L15.1844 4.11007C15.5949 3.74061 16.2272 3.77389 16.5966 4.1844C16.9661 4.59491 16.9328 5.2272 16.5223 5.59666L9.4815 11.9334L16.5223 18.2701Z" fill="#A4B8D1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 489 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z" fill="#5E6D7A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 509 B After Width: | Height: | Size: 494 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.41416 8L15.071 2.34315C15.4615 1.95262 15.4615 1.31946 15.071 0.928933C14.6805 0.538409 14.0473 0.538409 13.6568 0.928933L7.99995 6.58579L2.34309 0.928933C1.95257 0.538409 1.3194 0.538409 0.92888 0.928933C0.538355 1.31946 0.538355 1.95262 0.92888 2.34315L6.58573 8L0.92888 13.6569C0.538355 14.0474 0.538355 14.6805 0.92888 15.0711C1.3194 15.4616 1.95257 15.4616 2.34309 15.0711L7.99995 9.41421L13.6568 15.0711C14.0473 15.4616 14.6805 15.4616 15.071 15.0711C15.4615 14.6805 15.4615 14.0474 15.071 13.6569L9.41416 8Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 674 B |
|
@ -4,6 +4,7 @@ export { default as IconAdd } from './add.svg';
|
|||
export { default as IconAddPeople } from './link.svg';
|
||||
export { default as IconArrowBack } from './arrow_back.svg';
|
||||
export { default as IconArrowDown } from './arrow_down.svg';
|
||||
export { default as IconArrowLeft } from './arrow-left.svg';
|
||||
export { default as IconAudioOnly } from './visibility.svg';
|
||||
export { default as IconAudioOnlyOff } from './visibility-off.svg';
|
||||
export { default as IconAudioRoute } from './volume.svg';
|
||||
|
@ -16,6 +17,7 @@ export { default as IconChatSend } from './send.svg';
|
|||
export { default as IconChatUnread } from './chat-unread.svg';
|
||||
export { default as IconCheck } from './check.svg';
|
||||
export { default as IconClose } from './close.svg';
|
||||
export { default as IconCloseX } from './close-x.svg';
|
||||
export { default as IconClosedCaption } from './closed_caption.svg';
|
||||
export { default as IconConnectionActive } from './gsm-bars.svg';
|
||||
export { default as IconConnectionInactive } from './ninja.svg';
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Chat } from '../../../chat';
|
|||
import { Filmstrip } from '../../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../../invite';
|
||||
import { LargeVideo } from '../../../large-video';
|
||||
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
|
||||
import {
|
||||
|
@ -84,6 +85,11 @@ type Props = AbstractProps & {
|
|||
*/
|
||||
_roomName: string,
|
||||
|
||||
/**
|
||||
* If prejoin page is visible or not.
|
||||
*/
|
||||
_showPrejoin: boolean,
|
||||
|
||||
dispatch: Function,
|
||||
t: Function
|
||||
}
|
||||
|
@ -178,16 +184,22 @@ class Conference extends AbstractConference<Props, *> {
|
|||
// interfaceConfig is obsolete but legacy support is required.
|
||||
filmStripOnly: filmstripOnly
|
||||
} = interfaceConfig;
|
||||
const {
|
||||
_iAmRecorder,
|
||||
_layoutClassName,
|
||||
_showPrejoin
|
||||
} = this.props;
|
||||
const hideVideoQualityLabel
|
||||
= filmstripOnly
|
||||
|| VIDEO_QUALITY_LABEL_DISABLED
|
||||
|| this.props._iAmRecorder;
|
||||
|| _iAmRecorder;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { this.props._layoutClassName }
|
||||
className = { _layoutClassName }
|
||||
id = 'videoconference_page'
|
||||
onMouseMove = { this._onShowToolbar }>
|
||||
|
||||
<Notice />
|
||||
<Subject />
|
||||
<div id = 'videospace'>
|
||||
|
@ -197,11 +209,13 @@ class Conference extends AbstractConference<Props, *> {
|
|||
<Filmstrip filmstripOnly = { filmstripOnly } />
|
||||
</div>
|
||||
|
||||
{ filmstripOnly || <Toolbox /> }
|
||||
{ filmstripOnly || _showPrejoin || <Toolbox /> }
|
||||
{ filmstripOnly || <Chat /> }
|
||||
|
||||
{ this.renderNotificationsContainer() }
|
||||
|
||||
{ !filmstripOnly && _showPrejoin && <Prejoin />}
|
||||
|
||||
<CalleeInfoContainer />
|
||||
</div>
|
||||
);
|
||||
|
@ -268,7 +282,8 @@ function _mapStateToProps(state) {
|
|||
...abstractMapStateToProps(state),
|
||||
_iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
|
||||
_roomName: getConferenceNameForTitle(state)
|
||||
_roomName: getConferenceNameForTitle(state),
|
||||
_showPrejoin: isPrejoinPageVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -298,9 +298,8 @@ export function invitePeopleAndChatRooms( // eslint-disable-line max-params
|
|||
*/
|
||||
export function isAddPeopleEnabled(state: Object): boolean {
|
||||
const { peopleSearchUrl } = state['features/base/config'];
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
|
||||
return !isGuest && Boolean(peopleSearchUrl);
|
||||
return !isGuest(state) && Boolean(peopleSearchUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -316,6 +315,16 @@ export function isDialOutEnabled(state: Object): boolean {
|
|||
&& conference && conference.isSIPCallingSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user is guest or not.
|
||||
*
|
||||
* @param {Object} state - Current state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isGuest(state: Object): boolean {
|
||||
return state['features/base/jwt'].isGuest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string looks like it could be for a phone number.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* Action type to add a video track to the store.
|
||||
*/
|
||||
export const ADD_PREJOIN_VIDEO_TRACK = 'ADD_PREJOIN_VIDEO_TRACK';
|
||||
|
||||
/**
|
||||
* Action type to add an audio track to the store.
|
||||
*/
|
||||
export const ADD_PREJOIN_AUDIO_TRACK = 'ADD_PREJOIN_AUDIO_TRACK';
|
||||
|
||||
/**
|
||||
* Action type to add a content sharing track to the store.
|
||||
*/
|
||||
export const ADD_PREJOIN_CONTENT_SHARING_TRACK
|
||||
= 'ADD_PREJOIN_CONTENT_SHARING_TRACK';
|
||||
|
||||
/**
|
||||
* Action type to signal the start of the conference.
|
||||
*/
|
||||
export const PREJOIN_START_CONFERENCE = 'PREJOIN_START_CONFERENCE';
|
||||
|
||||
/**
|
||||
* Action type to set the status of the device.
|
||||
*/
|
||||
export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
|
||||
|
||||
/**
|
||||
* Action type to set the visiblity of the 'JoinByPhone' dialog.
|
||||
*/
|
||||
export const SET_JOIN_BY_PHONE_DIALOG_VISIBLITY = 'SET_JOIN_BY_PHONE_DIALOG_VISIBLITY';
|
||||
|
||||
/**
|
||||
* Action type to disable the audio while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_AUDIO_DISABLED = 'SET_PREJOIN_AUDIO_DISABLED';
|
||||
|
||||
/**
|
||||
* Action type to mute/unmute the audio while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_AUDIO_MUTED = 'SET_PREJOIN_AUDIO_MUTED';
|
||||
|
||||
/**
|
||||
* Action type to set the errors while creating the prejoin streams.
|
||||
*/
|
||||
export const SET_PREJOIN_DEVICE_ERRORS = 'SET_PREJOIN_DEVICE_ERRORS';
|
||||
|
||||
/**
|
||||
* Action type to set the name of the user.
|
||||
*/
|
||||
export const SET_PREJOIN_NAME = 'SET_PREJOIN_NAME';
|
||||
|
||||
/**
|
||||
* Action type to set the visibility of the prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_PAGE_VISIBILITY = 'SET_PREJOIN_PAGE_VISIBILITY';
|
||||
|
||||
/**
|
||||
* Action type to mute/unmute the video while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_VIDEO_DISABLED = 'SET_PREJOIN_VIDEO_DISABLED';
|
||||
|
||||
/**
|
||||
* Action type to mute/unmute the video while on prejoin page.
|
||||
*/
|
||||
export const SET_PREJOIN_VIDEO_MUTED = 'SET_PREJOIN_VIDEO_MUTED';
|
|
@ -0,0 +1,338 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
ADD_PREJOIN_AUDIO_TRACK,
|
||||
ADD_PREJOIN_CONTENT_SHARING_TRACK,
|
||||
ADD_PREJOIN_VIDEO_TRACK,
|
||||
PREJOIN_START_CONFERENCE,
|
||||
SET_DEVICE_STATUS,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_PREJOIN_AUDIO_DISABLED,
|
||||
SET_PREJOIN_AUDIO_MUTED,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
SET_PREJOIN_NAME,
|
||||
SET_PREJOIN_PAGE_VISIBILITY,
|
||||
SET_PREJOIN_VIDEO_DISABLED,
|
||||
SET_PREJOIN_VIDEO_MUTED
|
||||
} from './actionTypes';
|
||||
import { createLocalTrack } from '../base/lib-jitsi-meet';
|
||||
import { getAudioTrack, getVideoTrack } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Action used to add an audio track to the store.
|
||||
*
|
||||
* @param {Object} value - The track to be added.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addPrejoinAudioTrack(value: Object) {
|
||||
return {
|
||||
type: ADD_PREJOIN_AUDIO_TRACK,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to add a video track to the store.
|
||||
*
|
||||
* @param {Object} value - The track to be added.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addPrejoinVideoTrack(value: Object) {
|
||||
return {
|
||||
type: ADD_PREJOIN_VIDEO_TRACK,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to add a content sharing track to the store.
|
||||
*
|
||||
* @param {Object} value - The track to be added.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addPrejoinContentSharingTrack(value: Object) {
|
||||
return {
|
||||
type: ADD_PREJOIN_CONTENT_SHARING_TRACK,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all the newly created tracks to store on init.
|
||||
*
|
||||
* @param {Object[]} tracks - The newly created tracks.
|
||||
* @param {Object} errors - The errors from creating the tracks.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function initPrejoin(tracks: Object[], errors: Object) {
|
||||
return async function(dispatch: Function) {
|
||||
const audioTrack = tracks.find(t => t.isAudioTrack());
|
||||
const videoTrack = tracks.find(t => t.isVideoTrack());
|
||||
|
||||
dispatch(setPrejoinDeviceErrors(errors));
|
||||
|
||||
if (audioTrack) {
|
||||
dispatch(addPrejoinAudioTrack(audioTrack));
|
||||
} else {
|
||||
dispatch(setAudioDisabled());
|
||||
}
|
||||
|
||||
if (videoTrack) {
|
||||
if (videoTrack.videoType === 'desktop') {
|
||||
dispatch(addPrejoinContentSharingTrack(videoTrack));
|
||||
dispatch(setPrejoinVideoDisabled(true));
|
||||
} else {
|
||||
dispatch(addPrejoinVideoTrack(videoTrack));
|
||||
}
|
||||
} else {
|
||||
dispatch(setPrejoinVideoDisabled(true));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the conference.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConference() {
|
||||
return function(dispatch: Function) {
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
dispatch(startConference());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the conference without audio.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConferenceWithoutAudio() {
|
||||
return async function(dispatch: Function, getState: Function) {
|
||||
const audioTrack = getAudioTrack(getState());
|
||||
|
||||
if (audioTrack) {
|
||||
await dispatch(replacePrejoinAudioTrack(null));
|
||||
}
|
||||
dispatch(setAudioDisabled());
|
||||
dispatch(joinConference());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the existing audio track with a new one.
|
||||
*
|
||||
* @param {Object} track - The new track.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replacePrejoinAudioTrack(track: Object) {
|
||||
return async (dispatch: Function, getState: Function) => {
|
||||
const oldTrack = getAudioTrack(getState());
|
||||
|
||||
oldTrack && await oldTrack.dispose();
|
||||
dispatch(addPrejoinAudioTrack(track));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new audio track based on a device id and replaces the current one.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId of the microphone.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replaceAudioTrackById(deviceId: string) {
|
||||
return async (dispatch: Function) => {
|
||||
try {
|
||||
const track = await createLocalTrack('audio', deviceId);
|
||||
|
||||
dispatch(replacePrejoinAudioTrack(track));
|
||||
} catch (err) {
|
||||
dispatch(setDeviceStatusWarning('prejoin.audioTrackError'));
|
||||
logger.log('Error replacing audio track', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the existing video track with a new one.
|
||||
*
|
||||
* @param {Object} track - The new track.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replacePrejoinVideoTrack(track: Object) {
|
||||
return async (dispatch: Function, getState: Function) => {
|
||||
const oldTrack = getVideoTrack(getState());
|
||||
|
||||
oldTrack && await oldTrack.dispose();
|
||||
dispatch(addPrejoinVideoTrack(track));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new video track based on a device id and replaces the current one.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId of the camera.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function replaceVideoTrackById(deviceId: Object) {
|
||||
return async (dispatch: Function) => {
|
||||
try {
|
||||
const track = await createLocalTrack('video', deviceId);
|
||||
|
||||
dispatch(replacePrejoinVideoTrack(track));
|
||||
} catch (err) {
|
||||
dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
|
||||
logger.log('Error replacing video track', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to mark audio muted.
|
||||
*
|
||||
* @param {boolean} value - True for muted.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinAudioMuted(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_AUDIO_MUTED,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark video disabled.
|
||||
*
|
||||
* @param {boolean} value - True for muted.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinVideoDisabled(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_VIDEO_DISABLED,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to mark video muted.
|
||||
*
|
||||
* @param {boolean} value - True for muted.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinVideoMuted(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_VIDEO_MUTED,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark audio as disabled.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setAudioDisabled() {
|
||||
return {
|
||||
type: SET_PREJOIN_AUDIO_DISABLED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the device status as OK with the corresponding text.
|
||||
*
|
||||
* @param {string} deviceStatusText - The text to be set.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setDeviceStatusOk(deviceStatusText: string) {
|
||||
return {
|
||||
type: SET_DEVICE_STATUS,
|
||||
value: {
|
||||
deviceStatusText,
|
||||
deviceStatusType: 'ok'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the device status as 'warning' with the corresponding text.
|
||||
*
|
||||
* @param {string} deviceStatusText - The text to be set.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setDeviceStatusWarning(deviceStatusText: string) {
|
||||
return {
|
||||
type: SET_DEVICE_STATUS,
|
||||
value: {
|
||||
deviceStatusText,
|
||||
deviceStatusType: 'warning'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to set the visiblitiy of the 'JoinByPhoneDialog'.
|
||||
*
|
||||
* @param {boolean} value - The value.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setJoinByPhoneDialogVisiblity(value: boolean) {
|
||||
return {
|
||||
type: SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the initial errors after creating the tracks.
|
||||
*
|
||||
* @param {Object} value - The track errors.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinDeviceErrors(value: Object) {
|
||||
return {
|
||||
type: SET_PREJOIN_DEVICE_ERRORS,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the name of the guest user.
|
||||
*
|
||||
* @param {string} value - The name.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinName(value: string) {
|
||||
return {
|
||||
type: SET_PREJOIN_NAME,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the visiblity of the prejoin page.
|
||||
*
|
||||
* @param {boolean} value - The value.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setPrejoinPageVisibility(value: boolean) {
|
||||
return {
|
||||
type: SET_PREJOIN_PAGE_VISIBILITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark the start of the conference.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function startConference() {
|
||||
return {
|
||||
type: PREJOIN_START_CONFERENCE
|
||||
};
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
joinConference as joinConferenceAction,
|
||||
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
|
||||
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction,
|
||||
setPrejoinName
|
||||
} from '../actions';
|
||||
import { getRoomName } from '../../base/conference';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import ActionButton from './buttons/ActionButton';
|
||||
import {
|
||||
areJoinByPhoneButtonsVisible,
|
||||
getPrejoinName,
|
||||
isDeviceStatusVisible,
|
||||
isJoinByPhoneDialogVisible
|
||||
} from '../functions';
|
||||
import { isGuest } from '../../invite';
|
||||
import CopyMeetingUrl from './preview/CopyMeetingUrl';
|
||||
import DeviceStatus from './preview/DeviceStatus';
|
||||
import ParticipantName from './preview/ParticipantName';
|
||||
import Preview from './preview/Preview';
|
||||
import { VideoSettingsButton, AudioSettingsButton } from '../../toolbox';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Flag signaling if the device status is visible or not.
|
||||
*/
|
||||
deviceStatusVisible: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling if a user is logged in or not.
|
||||
*/
|
||||
isAnonymousUser: boolean,
|
||||
|
||||
/**
|
||||
* Joins the current meeting.
|
||||
*/
|
||||
joinConference: Function,
|
||||
|
||||
/**
|
||||
* Joins the current meeting without audio.
|
||||
*/
|
||||
joinConferenceWithoutAudio: Function,
|
||||
|
||||
/**
|
||||
* The name of the user that is about to join.
|
||||
*/
|
||||
name: string,
|
||||
|
||||
/**
|
||||
* Sets the name for the joining user.
|
||||
*/
|
||||
setName: Function,
|
||||
|
||||
/**
|
||||
* The name of the meeting that is about to be joined.
|
||||
*/
|
||||
roomName: string,
|
||||
|
||||
/**
|
||||
* Sets visibilit of the 'JoinByPhoneDialog'.
|
||||
*/
|
||||
setJoinByPhoneDialogVisiblity: Function,
|
||||
|
||||
/**
|
||||
* If 'JoinByPhoneDialog' is visible or not.
|
||||
*/
|
||||
showDialog: boolean,
|
||||
|
||||
/**
|
||||
* If join by phone buttons should be visible.
|
||||
*/
|
||||
showJoinByPhoneButtons: boolean,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* This component is displayed before joining a meeting.
|
||||
*/
|
||||
class Prejoin extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code Prejoin} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._showDialog = this._showDialog.bind(this);
|
||||
}
|
||||
|
||||
_showDialog: () => void;
|
||||
|
||||
/**
|
||||
* Displays the dialog for joining a meeting by phone.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
_showDialog() {
|
||||
this.props.setJoinByPhoneDialogVisiblity(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
deviceStatusVisible,
|
||||
isAnonymousUser,
|
||||
joinConference,
|
||||
joinConferenceWithoutAudio,
|
||||
name,
|
||||
setName,
|
||||
showJoinByPhoneButtons,
|
||||
t
|
||||
} = this.props;
|
||||
const { _showDialog } = this;
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-full-page'>
|
||||
<Preview />
|
||||
<div className = 'prejoin-input-area-container'>
|
||||
<div className = 'prejoin-input-area'>
|
||||
<div className = 'prejoin-title'>
|
||||
{t('prejoin.joinMeeting')}
|
||||
</div>
|
||||
<CopyMeetingUrl />
|
||||
<ParticipantName
|
||||
isEditable = { isAnonymousUser }
|
||||
setName = { setName }
|
||||
value = { name } />
|
||||
<ActionButton
|
||||
onClick = { joinConference }
|
||||
type = 'primary'>
|
||||
{ t('calendarSync.join') }
|
||||
</ActionButton>
|
||||
{showJoinByPhoneButtons
|
||||
&& <div className = 'prejoin-text-btns'>
|
||||
<ActionButton
|
||||
onClick = { joinConferenceWithoutAudio }
|
||||
type = 'text'>
|
||||
{ t('prejoin.joinWithoutAudio') }
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick = { _showDialog }
|
||||
type = 'text'>
|
||||
{ t('prejoin.joinAudioByPhone') }
|
||||
</ActionButton>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'prejoin-preview-btn-container'>
|
||||
<AudioSettingsButton visible = { true } />
|
||||
<VideoSettingsButton visible = { true } />
|
||||
</div>
|
||||
{ deviceStatusVisible && <DeviceStatus /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state): Object {
|
||||
return {
|
||||
isAnonymousUser: isGuest(state),
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
name: getPrejoinName(state),
|
||||
roomName: getRoomName(state),
|
||||
showDialog: isJoinByPhoneDialogVisible(state),
|
||||
showJoinByPhoneButtons: areJoinByPhoneButtonsVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
|
||||
joinConference: joinConferenceAction,
|
||||
setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
|
||||
setName: setPrejoinName
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(translate(Prejoin));
|
|
@ -0,0 +1,51 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
const classNameByType = {
|
||||
primary: 'prejoin-btn--primary',
|
||||
secondary: 'prejoin-btn--secondary',
|
||||
text: 'prejoin-btn--text'
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Text of the button.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Text css class of the button.
|
||||
*/
|
||||
className?: string,
|
||||
|
||||
/**
|
||||
* The type of th button: primary, secondary, text.
|
||||
*/
|
||||
type: string,
|
||||
|
||||
/**
|
||||
* OnClick button handler.
|
||||
*/
|
||||
onClick: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Button used for prejoin actions: Join/Join without audio/Join by phone.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ActionButton({ children, className, type, onClick }: Props) {
|
||||
const ownClassName = `prejoin-btn ${classNameByType[type]}`;
|
||||
const cls = className ? `${className} ${ownClassName}` : ownClassName;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cls }
|
||||
onClick = { onClick }>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionButton;
|
|
@ -0,0 +1,197 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { getCurrentConferenceUrl } from '../../../base/connection';
|
||||
import { Icon, IconCopy, IconCheck } from '../../../base/icons';
|
||||
import logger from '../../logger';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The meeting url.
|
||||
*/
|
||||
url: string,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* If true it shows the 'copy link' message.
|
||||
*/
|
||||
showCopyLink: boolean,
|
||||
|
||||
/**
|
||||
* If true it shows the 'link copied' message.
|
||||
*/
|
||||
showLinkCopied: boolean,
|
||||
};
|
||||
|
||||
const COPY_TIMEOUT = 2000;
|
||||
|
||||
/**
|
||||
* Component used to copy meeting url on prejoin page.
|
||||
*/
|
||||
class CopyMeetingUrl extends Component<Props, State> {
|
||||
|
||||
textarea: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code Prejoin} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.textarea = React.createRef();
|
||||
this.state = {
|
||||
showCopyLink: false,
|
||||
showLinkCopied: false
|
||||
};
|
||||
this._copyUrl = this._copyUrl.bind(this);
|
||||
this._hideCopyLink = this._hideCopyLink.bind(this);
|
||||
this._hideLinkCopied = this._hideLinkCopied.bind(this);
|
||||
this._showCopyLink = this._showCopyLink.bind(this);
|
||||
this._showLinkCopied = this._showLinkCopied.bind(this);
|
||||
}
|
||||
|
||||
_copyUrl: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to copy the url to clipboard.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_copyUrl() {
|
||||
const textarea = this.textarea.current;
|
||||
|
||||
try {
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
textarea.blur();
|
||||
this._showLinkCopied();
|
||||
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
|
||||
} catch (err) {
|
||||
logger.error('error when copying the meeting url');
|
||||
}
|
||||
}
|
||||
|
||||
_hideLinkCopied: () => void;
|
||||
|
||||
/**
|
||||
* Hides the 'Link copied' message.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_hideLinkCopied() {
|
||||
this.setState({
|
||||
showLinkCopied: false
|
||||
});
|
||||
}
|
||||
|
||||
_hideCopyLink: () => void;
|
||||
|
||||
/**
|
||||
* Hides the 'Copy link' text.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_hideCopyLink() {
|
||||
this.setState({
|
||||
showCopyLink: false
|
||||
});
|
||||
}
|
||||
|
||||
_showCopyLink: () => void;
|
||||
|
||||
/**
|
||||
* Shows the dark 'Copy link' text on hover.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_showCopyLink() {
|
||||
this.setState({
|
||||
showCopyLink: true
|
||||
});
|
||||
}
|
||||
|
||||
_showLinkCopied: () => void;
|
||||
|
||||
/**
|
||||
* Shows the green 'Link copied' message.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_showLinkCopied() {
|
||||
this.setState({
|
||||
showLinkCopied: true,
|
||||
showCopyLink: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { showCopyLink, showLinkCopied } = this.state;
|
||||
const { url, t } = this.props;
|
||||
const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
|
||||
const src = showLinkCopied ? IconCheck : IconCopy;
|
||||
const iconCls = showCopyLink || showCopyLink ? 'prejoin-copy-icon--white' : 'prejoin-copy-icon--light';
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'prejoin-copy-meeting'
|
||||
onMouseEnter = { _showCopyLink }
|
||||
onMouseLeave = { _hideCopyLink }>
|
||||
<div className = 'prejoin-copy-url'>{url}</div>
|
||||
{showCopyLink && <div
|
||||
className = 'prejoin-copy-badge prejoin-copy-badge--hover'
|
||||
onClick = { _copyUrl }>
|
||||
{t('prejoin.copyAndShare')}
|
||||
</div>}
|
||||
{showLinkCopied && <div
|
||||
className = 'prejoin-copy-badge prejoin-copy-badge--done'>
|
||||
{t('prejoin.linkCopied')}
|
||||
</div>}
|
||||
<Icon
|
||||
className = { `prejoin-copy-icon ${iconCls}` }
|
||||
size = { 24 }
|
||||
src = { src } />
|
||||
<textarea
|
||||
className = 'prejoin-copy-textarea'
|
||||
readOnly = { true }
|
||||
ref = { this.textarea }
|
||||
tabIndex = '-1'
|
||||
value = { url } />
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
url: getCurrentConferenceUrl(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(translate(CopyMeetingUrl));
|
|
@ -0,0 +1,83 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconCheck, IconExclamation } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
getDeviceStatusType,
|
||||
getDeviceStatusText,
|
||||
getRawError
|
||||
} from '../../functions';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The text to be displayed in relation to the status of the audio/video devices.
|
||||
*/
|
||||
deviceStatusText: string,
|
||||
|
||||
/**
|
||||
* The type of status for current devices, controlling the background color of the text.
|
||||
* Can be `ok` or `warning`.
|
||||
*/
|
||||
deviceStatusType: string,
|
||||
|
||||
/**
|
||||
* The error coming from device configuration.
|
||||
*/
|
||||
rawError: string,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
const iconMap = {
|
||||
warning: {
|
||||
src: IconExclamation,
|
||||
className: 'prejoin-preview-status--warning'
|
||||
},
|
||||
ok: {
|
||||
src: IconCheck,
|
||||
className: 'prejoin-preview-status--ok'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip showing the current status of the devices.
|
||||
* User is informed if there are missing or malfunctioning devices.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function DeviceStatus({ deviceStatusType, deviceStatusText, rawError, t }: Props) {
|
||||
const { src, className } = iconMap[deviceStatusType];
|
||||
|
||||
return (
|
||||
<div className = { `prejoin-preview-status ${className}` }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-icon'
|
||||
size = { 16 }
|
||||
src = { src } />
|
||||
<span className = 'prejoin-preview-error-desc'>{t(deviceStatusText)}</span>
|
||||
<span>{rawError}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{ deviceStatusText: string, deviceStatusText: string }}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
deviceStatusText: getDeviceStatusText(state),
|
||||
deviceStatusType: getDeviceStatusType(state),
|
||||
rawError: getRawError(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(DeviceStatus));
|
|
@ -0,0 +1,80 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Flag signaling if the name is ediable or not.
|
||||
*/
|
||||
isEditable: boolean,
|
||||
|
||||
/**
|
||||
* Sets the name for the joining user.
|
||||
*/
|
||||
setName: Function,
|
||||
|
||||
/**
|
||||
* Used to obtain translations.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The text to be displayed.
|
||||
*/
|
||||
value: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Participant name - can be an editable input or just the text name.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class ParticipantName extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ParticipantName} instance.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onNameChange = this._onNameChange.bind(this);
|
||||
}
|
||||
|
||||
_onNameChange: () => void;
|
||||
|
||||
/**
|
||||
* Handler used for changing the guest user name.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
_onNameChange({ target: { value } }) {
|
||||
this.props.setName(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { value, isEditable, t } = this.props;
|
||||
|
||||
return isEditable ? (
|
||||
<input
|
||||
className = 'prejoin-preview-name prejoin-preview-name--editable'
|
||||
onChange = { this._onNameChange }
|
||||
placeholder = { t('dialog.enterDisplayName') }
|
||||
value = { value } />
|
||||
)
|
||||
: <div className = 'prejoin-preview-name'>{value}</div>
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default translate(ParticipantName);
|
|
@ -0,0 +1,75 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { Video } from '../../../base/media';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getActiveVideoTrack, getPrejoinName, isPrejoinVideoMuted } from '../../functions';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The name of the user that is about to join.
|
||||
*/
|
||||
name: string,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
showCameraPreview: boolean,
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
videoTrack: ?Object,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component showing the video preview and device status.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function Preview(props: Props) {
|
||||
const {
|
||||
name,
|
||||
showCameraPreview,
|
||||
videoTrack
|
||||
} = props;
|
||||
|
||||
if (showCameraPreview && videoTrack) {
|
||||
return (
|
||||
<div className = 'prejoin-preview'>
|
||||
<div className = 'prejoin-preview-overlay' />
|
||||
<Video
|
||||
className = 'flipVideoX prejoin-preview-video'
|
||||
videoTrack = {{ jitsiTrack: videoTrack }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-preview prejoin-preview--no-video'>
|
||||
<Avatar
|
||||
className = 'prejoin-preview-avatar'
|
||||
displayName = { name }
|
||||
size = { 200 } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
name: getPrejoinName(state),
|
||||
videoTrack: getActiveVideoTrack(state),
|
||||
showCameraPreview: !isPrejoinVideoMuted(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Preview);
|
|
@ -0,0 +1,228 @@
|
|||
// @flow
|
||||
|
||||
|
||||
/**
|
||||
* Mutes or unmutes a track.
|
||||
*
|
||||
* @param {Object} track - The track to be configured.
|
||||
* @param {boolean} shouldMute - If it should mute or not.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function applyMuteOptionsToTrack(track, shouldMute) {
|
||||
if (track.isMuted() === shouldMute) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldMute) {
|
||||
return track.mute();
|
||||
}
|
||||
|
||||
return track.unmute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for the visibility of the 'join by phone' buttons.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function areJoinByPhoneButtonsVisible(state: Object): boolean {
|
||||
return state['features/prejoin'].buttonsVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the device status strip is visible or not.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDeviceStatusVisible(state: Object): boolean {
|
||||
return !((isAudioDisabled(state) && isPrejoinVideoDisabled(state))
|
||||
|| (isPrejoinAudioMuted(state) && isPrejoinVideoMuted(state)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the active video/content sharing track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getActiveVideoTrack(state: Object): Object {
|
||||
const track = getVideoTrack(state) || getContentSharingTrack(state);
|
||||
|
||||
if (track && track.isActive()) {
|
||||
return track;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list with all the prejoin tracks configured according to
|
||||
* user's preferences.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
export async function getAllPrejoinConfiguredTracks(state: Object): Promise<Object[]> {
|
||||
const tracks = [];
|
||||
const audioTrack = getAudioTrack(state);
|
||||
const videoTrack = getVideoTrack(state);
|
||||
const csTrack = getContentSharingTrack(state);
|
||||
|
||||
if (csTrack) {
|
||||
tracks.push(csTrack);
|
||||
} else if (videoTrack) {
|
||||
await applyMuteOptionsToTrack(videoTrack, isPrejoinVideoMuted(state));
|
||||
tracks.push(videoTrack);
|
||||
}
|
||||
|
||||
if (audioTrack) {
|
||||
await applyMuteOptionsToTrack(audioTrack, isPrejoinAudioMuted(state));
|
||||
isPrejoinAudioMuted(state) && audioTrack.mute();
|
||||
tracks.push(audioTrack);
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the prejoin audio track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getAudioTrack(state: Object): Object {
|
||||
return state['features/prejoin'].audioTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the prejoin content sharing track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getContentSharingTrack(state: Object): Object {
|
||||
return state['features/prejoin'].contentSharingTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text for the prejoin status bar.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDeviceStatusText(state: Object): string {
|
||||
return state['features/prejoin'].deviceStatusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the prejoin status bar: 'ok'|'warning'.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDeviceStatusType(state: Object): string {
|
||||
return state['features/prejoin'].deviceStatusType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the prejoin video track.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getVideoTrack(state: Object): Object {
|
||||
return state['features/prejoin'].videoTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the mute status of the prejoin audio.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinAudioMuted(state: Object): boolean {
|
||||
return state['features/prejoin'].audioMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the name that the user filled while configuring.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getPrejoinName(state: Object): string {
|
||||
return state['features/prejoin'].name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the mute status of the prejoin video.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinVideoMuted(state: Object): boolean {
|
||||
return state['features/prejoin'].videoMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the error if any while creating streams.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRawError(state: Object): string {
|
||||
return state['features/prejoin'].rawError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting state of the prejoin audio.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAudioDisabled(state: Object): Object {
|
||||
return state['features/prejoin'].audioDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting state of the prejoin video.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinVideoDisabled(state: Object): Object {
|
||||
return state['features/prejoin'].videoDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for getting the visiblity state for the 'JoinByPhoneDialog'.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isJoinByPhoneDialogVisible(state: Object): boolean {
|
||||
return state['features/prejoin'].showJoinByPhoneDialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the prejoin page is enabled and no flag
|
||||
* to bypass showing the page is present.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinPageEnabled(state: Object): boolean {
|
||||
return state['features/base/config'].prejoinPageEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the prejoin page is visible & active.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinPageVisible(state: Object): boolean {
|
||||
return isPrejoinPageEnabled(state) && state['features/prejoin'].showPrejoin;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export * from './actions';
|
||||
export * from './functions';
|
||||
|
||||
export { default as Prejoin } from './components/Prejoin';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/prejoin');
|
|
@ -0,0 +1,95 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
ADD_PREJOIN_AUDIO_TRACK,
|
||||
ADD_PREJOIN_VIDEO_TRACK,
|
||||
PREJOIN_START_CONFERENCE
|
||||
} from './actionTypes';
|
||||
import { setPrejoinAudioMuted, setPrejoinVideoMuted } from './actions';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
|
||||
import { participantUpdated, getLocalParticipant } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { updateSettings } from '../base/settings';
|
||||
import { getAllPrejoinConfiguredTracks, getPrejoinName } from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* The redux middleware for {@link PrejoinPage}.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => async action => {
|
||||
switch (action.type) {
|
||||
case ADD_PREJOIN_AUDIO_TRACK: {
|
||||
const { value: audioTrack } = action;
|
||||
|
||||
if (audioTrack) {
|
||||
store.dispatch(
|
||||
updateSettings({
|
||||
micDeviceId: audioTrack.getDeviceId()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ADD_PREJOIN_VIDEO_TRACK: {
|
||||
const { value: videoTrack } = action;
|
||||
|
||||
if (videoTrack) {
|
||||
store.dispatch(
|
||||
updateSettings({
|
||||
cameraDeviceId: videoTrack.getDeviceId()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PREJOIN_START_CONFERENCE: {
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
_syncParticipantName(dispatch, getState);
|
||||
const tracks = await getAllPrejoinConfiguredTracks(getState());
|
||||
|
||||
APP.conference.prejoinStart(tracks);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_AUDIO_MUTED: {
|
||||
store.dispatch(setPrejoinAudioMuted(Boolean(action.muted)));
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_VIDEO_MUTED: {
|
||||
store.dispatch(setPrejoinVideoMuted(Boolean(action.muted)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the local participant name if one is present.
|
||||
*
|
||||
* @param {Function} dispatch - The redux dispatch function.
|
||||
* @param {Function} getState - Gets the current state.
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function _syncParticipantName(dispatch, getState) {
|
||||
const state = getState();
|
||||
const name = getPrejoinName(state);
|
||||
|
||||
name && dispatch(
|
||||
participantUpdated({
|
||||
...getLocalParticipant(state),
|
||||
name
|
||||
}),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ADD_PREJOIN_AUDIO_TRACK,
|
||||
ADD_PREJOIN_CONTENT_SHARING_TRACK,
|
||||
ADD_PREJOIN_VIDEO_TRACK,
|
||||
SET_DEVICE_STATUS,
|
||||
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
|
||||
SET_PREJOIN_AUDIO_DISABLED,
|
||||
SET_PREJOIN_AUDIO_MUTED,
|
||||
SET_PREJOIN_DEVICE_ERRORS,
|
||||
SET_PREJOIN_NAME,
|
||||
SET_PREJOIN_PAGE_VISIBILITY,
|
||||
SET_PREJOIN_VIDEO_DISABLED,
|
||||
SET_PREJOIN_VIDEO_MUTED
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
audioDisabled: false,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
videoDisabled: false,
|
||||
deviceStatusText: 'prejoin.configuringDevices',
|
||||
deviceStatusType: 'ok',
|
||||
showPrejoin: true,
|
||||
showJoinByPhoneDialog: false,
|
||||
videoTrack: null,
|
||||
audioTrack: null,
|
||||
contentSharingTrack: null,
|
||||
rawError: '',
|
||||
name: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the prejoin state
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/prejoin', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ADD_PREJOIN_AUDIO_TRACK: {
|
||||
return {
|
||||
...state,
|
||||
audioTrack: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_PREJOIN_CONTENT_SHARING_TRACK: {
|
||||
return {
|
||||
...state,
|
||||
contentSharingTrack: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_PREJOIN_VIDEO_TRACK: {
|
||||
return {
|
||||
...state,
|
||||
videoTrack: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_NAME: {
|
||||
return {
|
||||
...state,
|
||||
name: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_PAGE_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
showPrejoin: action.value
|
||||
};
|
||||
|
||||
case SET_PREJOIN_VIDEO_DISABLED: {
|
||||
return {
|
||||
...state,
|
||||
videoDisabled: action.value
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_VIDEO_MUTED:
|
||||
return {
|
||||
...state,
|
||||
videoMuted: action.value
|
||||
};
|
||||
|
||||
case SET_PREJOIN_AUDIO_MUTED:
|
||||
return {
|
||||
...state,
|
||||
audioMuted: action.value
|
||||
};
|
||||
|
||||
case SET_PREJOIN_DEVICE_ERRORS: {
|
||||
const status = getStatusFromErrors(action.value);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...status
|
||||
};
|
||||
}
|
||||
|
||||
case SET_DEVICE_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
deviceStatusText: action.text,
|
||||
deviceStatusType: action.type
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PREJOIN_AUDIO_DISABLED: {
|
||||
return {
|
||||
...state,
|
||||
audioDisabled: true
|
||||
};
|
||||
}
|
||||
|
||||
case SET_JOIN_BY_PHONE_DIALOG_VISIBLITY: {
|
||||
return {
|
||||
...state,
|
||||
showJoinByPhoneDialog: action.value
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a suitable error object based on the track errors.
|
||||
*
|
||||
* @param {Object} errors - The errors got while creating local tracks.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getStatusFromErrors(errors) {
|
||||
const { audioOnlyError, videoOnlyError, audioAndVideoError } = errors;
|
||||
|
||||
if (audioAndVideoError) {
|
||||
if (audioOnlyError) {
|
||||
if (videoOnlyError) {
|
||||
return {
|
||||
deviceStatusType: 'warning',
|
||||
deviceStatusText: 'prejoin.audioAndVideoError',
|
||||
rawError: audioAndVideoError.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deviceStatusType: 'warning',
|
||||
deviceStatusText: 'prejoin.audioOnlyError',
|
||||
rawError: audioOnlyError.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deviceStatusType: 'warning',
|
||||
deviceStatusText: 'prejoin.videoOnlyError',
|
||||
rawError: audioAndVideoError.message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deviceStatusType: 'ok',
|
||||
deviceStatusText: 'prejoin.lookGood',
|
||||
rawError: ''
|
||||
};
|
||||
}
|
|
@ -211,8 +211,10 @@ class VideoSettingsContent extends Component<Props, State> {
|
|||
const { trackData } = this.state;
|
||||
|
||||
return (
|
||||
<div className = 'video-preview'>
|
||||
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
|
||||
<div className = 'video-preview-container'>
|
||||
<div className = 'video-preview'>
|
||||
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,11 @@ import { connect } from '../../base/redux';
|
|||
import { AbstractAudioMuteButton } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
import { isLocalTrackMuted } from '../../base/tracks';
|
||||
import {
|
||||
isPrejoinAudioMuted,
|
||||
isAudioDisabled,
|
||||
isPrejoinPageVisible
|
||||
} from '../../prejoin';
|
||||
import { muteLocal } from '../../remote-video-menu/actions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
@ -144,15 +149,27 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
|
|||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _audioMuted: boolean
|
||||
* _audioMuted: boolean,
|
||||
* _disabled: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state): Object {
|
||||
const tracks = state['features/base/tracks'];
|
||||
let _audioMuted;
|
||||
let _disabled;
|
||||
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
_audioMuted = isPrejoinAudioMuted(state);
|
||||
_disabled = state['features/base/config'].startSilent;
|
||||
} else {
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
_audioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
|
||||
_disabled = state['features/base/config'].startSilent || isAudioDisabled(state);
|
||||
}
|
||||
|
||||
return {
|
||||
_audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO),
|
||||
_disabled: state['features/base/config'].startSilent
|
||||
_audioMuted,
|
||||
_disabled
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,11 @@ import { connect } from '../../base/redux';
|
|||
import { AbstractVideoMuteButton } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
import { getLocalVideoType, isLocalVideoTrackMuted } from '../../base/tracks';
|
||||
import {
|
||||
isPrejoinPageVisible,
|
||||
isPrejoinVideoDisabled,
|
||||
isPrejoinVideoMuted
|
||||
} from '../../prejoin';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
declare var APP: Object;
|
||||
|
@ -41,6 +46,11 @@ type Props = AbstractButtonProps & {
|
|||
*/
|
||||
_videoMuted: boolean,
|
||||
|
||||
/**
|
||||
* Whether video button is disabled or not.
|
||||
*/
|
||||
_videoDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
|
@ -96,6 +106,17 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
|
|||
|| APP.keyboardshortcut.unregisterShortcut('V');
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if video is currently disabled or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isDisabled() {
|
||||
return this.props._videoDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if video is currently muted ot nor.
|
||||
*
|
||||
|
@ -170,11 +191,19 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
|
|||
function _mapStateToProps(state): Object {
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
const tracks = state['features/base/tracks'];
|
||||
let _videoMuted = isLocalVideoTrackMuted(tracks);
|
||||
let _videoDisabled = false;
|
||||
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
_videoMuted = isPrejoinVideoMuted(state);
|
||||
_videoDisabled = isPrejoinVideoDisabled(state);
|
||||
}
|
||||
|
||||
return {
|
||||
_audioOnly: Boolean(audioOnly),
|
||||
_videoDisabled,
|
||||
_videoMediaType: getLocalVideoType(tracks),
|
||||
_videoMuted: isLocalVideoTrackMuted(tracks)
|
||||
_videoMuted
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import AudioMuteButton from '../AudioMuteButton';
|
||||
import { hasAvailableDevices } from '../../../base/devices';
|
||||
import { isAudioSettingsButtonDisabled } from '../../functions';
|
||||
import { IconArrowDown } from '../../../base/icons';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
|
||||
|
@ -25,9 +25,9 @@ type Props = {
|
|||
permissionPromptVisibility: boolean,
|
||||
|
||||
/**
|
||||
* If the user has audio input or audio output devices.
|
||||
* If the button should be disabled.
|
||||
*/
|
||||
hasDevices: boolean,
|
||||
isDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the button.
|
||||
|
@ -49,6 +49,8 @@ type State = {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
class AudioSettingsButton extends Component<Props, State> {
|
||||
_isMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AudioSettingsButton} instance.
|
||||
*
|
||||
|
@ -58,6 +60,7 @@ class AudioSettingsButton extends Component<Props, State> {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._isMounted = true;
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
|
@ -73,7 +76,7 @@ class AudioSettingsButton extends Component<Props, State> {
|
|||
'audio',
|
||||
);
|
||||
|
||||
this.setState({
|
||||
this._isMounted && this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
|
@ -98,14 +101,23 @@ class AudioSettingsButton extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { hasDevices, onAudioOptionsClick, visible } = this.props;
|
||||
const settingsDisabled = !this.state.hasPermissions || !hasDevices;
|
||||
const { isDisabled, onAudioOptionsClick, visible } = this.props;
|
||||
const settingsDisabled = !this.state.hasPermissions || isDisabled;
|
||||
|
||||
return visible ? (
|
||||
<AudioSettingsPopup>
|
||||
|
@ -128,9 +140,7 @@ class AudioSettingsButton extends Component<Props, State> {
|
|||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasDevices:
|
||||
hasAvailableDevices(state, 'audioInput')
|
||||
|| hasAvailableDevices(state, 'audioOutput'),
|
||||
isDisabled: isAudioSettingsButtonDisabled(state),
|
||||
permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isVideoSettingsButtonDisabled } from '../../functions';
|
||||
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
|
||||
import VideoMuteButton from '../VideoMuteButton';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { hasAvailableDevices } from '../../../base/devices';
|
||||
import { IconArrowDown } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
|
||||
|
@ -25,9 +24,9 @@ type Props = {
|
|||
permissionPromptVisibility: boolean,
|
||||
|
||||
/**
|
||||
* If the user has any video devices.
|
||||
* If the button should be disabled
|
||||
*/
|
||||
hasDevices: boolean,
|
||||
isDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the button.
|
||||
|
@ -49,6 +48,8 @@ type State = {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
class VideoSettingsButton extends Component<Props, State> {
|
||||
_isMounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VideoSettingsButton} instance.
|
||||
*
|
||||
|
@ -58,6 +59,7 @@ class VideoSettingsButton extends Component<Props, State> {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._isMounted = true;
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
|
@ -73,7 +75,7 @@ class VideoSettingsButton extends Component<Props, State> {
|
|||
'video',
|
||||
);
|
||||
|
||||
this.setState({
|
||||
this._isMounted && this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
|
@ -98,14 +100,23 @@ class VideoSettingsButton extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { hasDevices, onVideoOptionsClick, visible } = this.props;
|
||||
const iconDisabled = !this.state.hasPermissions || !hasDevices;
|
||||
const { isDisabled, onVideoOptionsClick, visible } = this.props;
|
||||
const iconDisabled = !this.state.hasPermissions || isDisabled;
|
||||
|
||||
return visible ? (
|
||||
<VideoSettingsPopup>
|
||||
|
@ -128,7 +139,7 @@ class VideoSettingsButton extends Component<Props, State> {
|
|||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasDevices: hasAvailableDevices(state, 'videoInput'),
|
||||
isDisabled: isVideoSettingsButtonDisabled(state),
|
||||
permissionPromptVisibility: getMediaPermissionPromptVisibility(state)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
export { default as AudioSettingsButton } from './AudioSettingsButton';
|
||||
export { default as VideoSettingsButton } from './VideoSettingsButton';
|
||||
export { default as ToolbarButton } from './ToolbarButton';
|
||||
export { default as Toolbox } from './Toolbox';
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
isAudioDisabled,
|
||||
isPrejoinPageVisible,
|
||||
isPrejoinVideoDisabled
|
||||
} from '../prejoin';
|
||||
import { hasAvailableDevices } from '../base/devices';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
|
@ -45,3 +52,32 @@ export function isToolboxVisible(state: Object) {
|
|||
return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible
|
||||
|| audioSettingsVisible || videoSettingsVisible));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio settings button is disabled or not.
|
||||
*
|
||||
* @param {string} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAudioSettingsButtonDisabled(state: Object) {
|
||||
const devicesMissing = !hasAvailableDevices(state, 'audioInput')
|
||||
&& !hasAvailableDevices(state, 'audioOutput');
|
||||
|
||||
return isPrejoinPageVisible(state)
|
||||
? devicesMissing || isAudioDisabled(state)
|
||||
: devicesMissing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the video settings button is disabled or not.
|
||||
*
|
||||
* @param {string} state - The state from the Redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVideoSettingsButtonDisabled(state: Object) {
|
||||
const devicesMissing = !hasAvailableDevices(state, 'videoInput');
|
||||
|
||||
return isPrejoinPageVisible(state)
|
||||
? devicesMissing || isPrejoinVideoDisabled(state)
|
||||
: devicesMissing;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue