mirror of https://github.com/jitsi/jitsi-meet
Refactor settings modal (#3121)
* feat(settings): setting dialog - Move device selection, profile edit, language select, moderator options, and server auth into one modal with tabs. - Remove side panel profile and settings and logic used to update them. - Pipe server auth status into redux to display in the settings dialog. - Change filmstrip only device selection popup to use the new stateless settings dialog component. * squash: do not show profile tab if not guest * squash: profile button not clickable if no profile to show * squash: nits * ref: Settings dialog.pull/3168/head jitsi-meet_3163
parent
0acc9187ed
commit
1f8fa3b6d4
@ -0,0 +1,43 @@ |
||||
.settings-pane { |
||||
display: flex; |
||||
width: 100%; |
||||
|
||||
&.profile-pane { |
||||
flex-direction: column; |
||||
} |
||||
|
||||
.auth-name { |
||||
margin-bottom: 4px; |
||||
} |
||||
|
||||
.device-selection { |
||||
margin-top: 20px; |
||||
} |
||||
|
||||
.mock-atlaskit-label { |
||||
color: #56637A; |
||||
font-size: 12px; |
||||
font-weight: 600; |
||||
line-height: 1.33; |
||||
padding: 20px 0px 4px 0px; |
||||
} |
||||
|
||||
.more-tab, |
||||
.profile-edit { |
||||
display: flex; |
||||
width: 100%; |
||||
} |
||||
|
||||
.profile-edit-field, |
||||
.settings-sub-pane { |
||||
flex: 1; |
||||
} |
||||
|
||||
.profile-edit-field { |
||||
margin-right: 20px; |
||||
} |
||||
|
||||
.language-settings { |
||||
max-width: 50%; |
||||
} |
||||
} |
@ -1,194 +0,0 @@ |
||||
/* global $, APP */ |
||||
import UIUtil from '../../util/UIUtil'; |
||||
import UIEvents from '../../../../service/UI/UIEvents'; |
||||
|
||||
import { |
||||
createProfilePanelButtonEvent, |
||||
sendAnalytics |
||||
} from '../../../../react/features/analytics'; |
||||
|
||||
const sidePanelsContainerId = 'sideToolbarContainer'; |
||||
const htmlStr = ` |
||||
<div id='profile_container' class='sideToolbarContainer__inner'> |
||||
<div class='title' data-i18n='profile.title'></div> |
||||
<div class='sideToolbarBlock first'> |
||||
<label class='first' data-i18n='profile.setDisplayNameLabel'> |
||||
</label> |
||||
<input class='input-control' type='text' id='setDisplayName' |
||||
data-i18n='[placeholder]settings.name'> |
||||
</div> |
||||
<div class='sideToolbarBlock'> |
||||
<label data-i18n='profile.setEmailLabel'></label> |
||||
<input id='setEmail' type='text' class='input-control' |
||||
data-i18n='[placeholder]profile.setEmailInput'> |
||||
</div> |
||||
<div id='profile_auth_container' |
||||
class='sideToolbarBlock auth_container'> |
||||
<p data-i18n='toolbar.authenticate'></p> |
||||
<ul> |
||||
<li id='profile_auth_identity'></li> |
||||
<li id='profile_button_login'> |
||||
<a class='authButton' data-i18n='toolbar.login'></a> |
||||
</li> |
||||
<li id='profile_button_logout'> |
||||
<a class='authButton' data-i18n='toolbar.logout'></a> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div>`; |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
function initHTML() { |
||||
$(`#${sidePanelsContainerId}`) |
||||
.append(htmlStr); |
||||
|
||||
// make sure we translate the panel, as adding it can be after i18n
|
||||
// library had initialized and translated already present html
|
||||
APP.translation.translateElement($(`#${sidePanelsContainerId}`)); |
||||
} |
||||
|
||||
export default { |
||||
init(emitter) { |
||||
initHTML(); |
||||
|
||||
const settings = APP.store.getState()['features/base/settings']; |
||||
|
||||
/** |
||||
* Updates display name. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function updateDisplayName() { |
||||
emitter.emit(UIEvents.NICKNAME_CHANGED, $('#setDisplayName').val()); |
||||
} |
||||
|
||||
$('#setDisplayName') |
||||
.val(settings.displayName) |
||||
.keyup(event => { |
||||
if (event.keyCode === 13) { // enter
|
||||
updateDisplayName(); |
||||
} |
||||
}) |
||||
.focusout(updateDisplayName); |
||||
|
||||
/** |
||||
* Updates the email. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function updateEmail() { |
||||
emitter.emit(UIEvents.EMAIL_CHANGED, $('#setEmail').val()); |
||||
} |
||||
|
||||
$('#setEmail') |
||||
.val(settings.email) |
||||
.keyup(event => { |
||||
if (event.keyCode === 13) { // enter
|
||||
updateEmail(); |
||||
} |
||||
}) |
||||
.focusout(updateEmail); |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
function loginClicked() { |
||||
sendAnalytics(createProfilePanelButtonEvent('login.button')); |
||||
emitter.emit(UIEvents.AUTH_CLICKED); |
||||
} |
||||
|
||||
$('#profile_button_login').click(loginClicked); |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
function logoutClicked() { |
||||
const titleKey = 'dialog.logoutTitle'; |
||||
const msgKey = 'dialog.logoutQuestion'; |
||||
|
||||
sendAnalytics(createProfilePanelButtonEvent('logout.button')); |
||||
|
||||
// Ask for confirmation
|
||||
APP.UI.messageHandler.openTwoButtonDialog({ |
||||
titleKey, |
||||
msgKey, |
||||
leftButtonKey: 'dialog.Yes', |
||||
submitFunction(evt, yes) { |
||||
if (yes) { |
||||
emitter.emit(UIEvents.LOGOUT); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
$('#profile_button_logout').click(logoutClicked); |
||||
}, |
||||
|
||||
/** |
||||
* Check if settings menu is visible or not. |
||||
* @returns {boolean} |
||||
*/ |
||||
isVisible() { |
||||
return UIUtil.isVisible(document.getElementById('profile_container')); |
||||
}, |
||||
|
||||
/** |
||||
* Change user display name in the settings menu. |
||||
* @param {string} newDisplayName |
||||
*/ |
||||
changeDisplayName(newDisplayName) { |
||||
$('#setDisplayName').val(newDisplayName); |
||||
}, |
||||
|
||||
/** |
||||
* Change the value of the field for the user email. |
||||
* @param {string} email the new value that will be displayed in the field. |
||||
*/ |
||||
changeEmail(email) { |
||||
$('#setEmail').val(email); |
||||
}, |
||||
|
||||
/** |
||||
* Shows or hides authentication related buttons |
||||
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide |
||||
*/ |
||||
showAuthenticationButtons(show) { |
||||
const id = 'profile_auth_container'; |
||||
|
||||
UIUtil.setVisible(id, show); |
||||
}, |
||||
|
||||
/** |
||||
* Shows/hides login button. |
||||
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide |
||||
*/ |
||||
showLoginButton(show) { |
||||
const id = 'profile_button_login'; |
||||
|
||||
UIUtil.setVisible(id, show); |
||||
}, |
||||
|
||||
/** |
||||
* Shows/hides logout button. |
||||
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide |
||||
*/ |
||||
showLogoutButton(show) { |
||||
const id = 'profile_button_logout'; |
||||
|
||||
UIUtil.setVisible(id, show); |
||||
}, |
||||
|
||||
/** |
||||
* Displays user's authenticated identity name (login). |
||||
* @param {string} authIdentity identity name to be displayed. |
||||
*/ |
||||
setAuthenticatedIdentity(authIdentity) { |
||||
const id = 'profile_auth_identity'; |
||||
|
||||
UIUtil.setVisible(id, Boolean(authIdentity)); |
||||
|
||||
$(`#${id}`).text(authIdentity ? authIdentity : ''); |
||||
} |
||||
}; |
@ -1,52 +0,0 @@ |
||||
/* global $, APP, interfaceConfig */ |
||||
|
||||
/* eslint-disable no-unused-vars */ |
||||
|
||||
import React from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import { I18nextProvider } from 'react-i18next'; |
||||
import { Provider } from 'react-redux'; |
||||
|
||||
import { i18next } from '../../../../react/features/base/i18n'; |
||||
import { |
||||
SettingsMenu, |
||||
isSettingEnabled |
||||
} from '../../../../react/features/settings'; |
||||
import UIUtil from '../../util/UIUtil'; |
||||
|
||||
/* eslint-enable no-unused-vars */ |
||||
|
||||
export default { |
||||
init() { |
||||
const settingsMenuContainer = document.createElement('div'); |
||||
|
||||
settingsMenuContainer.id = 'settings_container'; |
||||
settingsMenuContainer.className = 'sideToolbarContainer__inner'; |
||||
|
||||
$('#sideToolbarContainer').append(settingsMenuContainer); |
||||
|
||||
const props = { |
||||
showDeviceSettings: isSettingEnabled('devices'), |
||||
showLanguageSettings: isSettingEnabled('language'), |
||||
showModeratorSettings: isSettingEnabled('moderator'), |
||||
showTitles: interfaceConfig.SETTINGS_SECTIONS.length > 1 |
||||
}; |
||||
|
||||
ReactDOM.render( |
||||
<Provider store = { APP.store }> |
||||
<I18nextProvider i18n = { i18next }> |
||||
<SettingsMenu { ...props } /> |
||||
</I18nextProvider> |
||||
</Provider>, |
||||
settingsMenuContainer |
||||
); |
||||
}, |
||||
|
||||
/** |
||||
* Check if settings menu is visible or not. |
||||
* @returns {boolean} |
||||
*/ |
||||
isVisible() { |
||||
return UIUtil.isVisible(document.getElementById('settings_container')); |
||||
} |
||||
}; |
@ -0,0 +1,67 @@ |
||||
// @flow
|
||||
|
||||
import { Component } from 'react'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link AbstractDialogTab}. |
||||
*/ |
||||
export type Props = { |
||||
|
||||
/** |
||||
* Function that closes the dialog. |
||||
*/ |
||||
closeDialog: Function, |
||||
|
||||
/** |
||||
* Callback to invoke on change. |
||||
*/ |
||||
onTabStateChange: Function, |
||||
|
||||
/** |
||||
* The id of the tab. |
||||
*/ |
||||
tabId: number |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* Abstract React {@code Component} for tabs of the DialogWithTabs component. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class AbstractDialogTab extends Component<Props> { |
||||
/** |
||||
* Initializes a new {@code AbstractDialogTab} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onChange = this._onChange.bind(this); |
||||
} |
||||
|
||||
_onChange: (Object) => {}; |
||||
|
||||
/** |
||||
* Uses the onTabStateChange function to pass the changed state of the |
||||
* controlled tab component to the controlling DialogWithTabs component. |
||||
* |
||||
* @param {Object} change - Object that contains the changed property and |
||||
* value. |
||||
* @returns {void} |
||||
*/ |
||||
_onChange(change) { |
||||
const { onTabStateChange, tabId } = this.props; |
||||
|
||||
onTabStateChange(tabId, { |
||||
...this.props, |
||||
...change |
||||
}); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default AbstractDialogTab; |
@ -0,0 +1,199 @@ |
||||
// @flow
|
||||
|
||||
import Tabs from '@atlaskit/tabs'; |
||||
import React, { Component } from 'react'; |
||||
|
||||
import { StatelessDialog } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename); |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link DialogWithTabs}. |
||||
*/ |
||||
export type Props = { |
||||
|
||||
/** |
||||
* Function that closes the dialog. |
||||
*/ |
||||
closeDialog: Function, |
||||
|
||||
/** |
||||
* Which settings tab should be initially displayed. If not defined then |
||||
* the first tab will be displayed. |
||||
*/ |
||||
defaultTab: number, |
||||
|
||||
/** |
||||
* Disables dismissing the dialog when the blanket is clicked. Enabled |
||||
* by default. |
||||
*/ |
||||
disableBlanketClickDismiss: boolean, |
||||
|
||||
/** |
||||
* Callback invoked when the Save button has been pressed. |
||||
*/ |
||||
onSubmit: Function, |
||||
|
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* Information about the tabs that will be rendered. |
||||
*/ |
||||
tabs: Array<Object> |
||||
|
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of {@link DialogWithTabs}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* An array of the states of the tabs. |
||||
*/ |
||||
tabStates: Array<Object> |
||||
}; |
||||
|
||||
/** |
||||
* A React {@code Component} for displaying a dialog with tabs. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class DialogWithTabs extends Component<Props, State> { |
||||
/** |
||||
* Initializes a new {@code DialogWithTabs} instance. |
||||
* |
||||
* @param {Object} props - The read-only React {@code Component} props with |
||||
* which the new instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
this.state = { |
||||
tabStates: this.props.tabs.map(tab => tab.props) |
||||
}; |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
this._onTabStateChange = this._onTabStateChange.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const onCancel = this.props.closeDialog; |
||||
|
||||
return ( |
||||
<StatelessDialog |
||||
disableBlanketClickDismiss |
||||
= { this.props.disableBlanketClickDismiss } |
||||
onCancel = { onCancel } |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = 'settings.title'> |
||||
<div className = 'settings-dialog'> |
||||
{ this._renderTabs() } |
||||
</div> |
||||
</StatelessDialog> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the tabs from the tab information passed on props. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_renderTabs() { |
||||
const { defaultTab = 0, t, tabs } = this.props; |
||||
|
||||
if (tabs.length === 1) { |
||||
return this._renderTab({ |
||||
...tabs[0], |
||||
tabId: 0 |
||||
}); |
||||
} |
||||
|
||||
if (tabs.length > 1) { |
||||
return ( |
||||
<Tabs |
||||
tabs = { |
||||
tabs.map(({ component, label, styles }, idx) => { |
||||
return { |
||||
content: this._renderTab({ |
||||
component, |
||||
styles, |
||||
tabId: idx |
||||
}), |
||||
defaultSelected: defaultTab === idx, |
||||
label: t(label) |
||||
}; |
||||
}) |
||||
} />); |
||||
} |
||||
|
||||
logger.warn('No settings tabs configured to display.'); |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Renders a tab from the tab information passed as parameters. |
||||
* |
||||
* @param {Object} tabInfo - Information about the tab. |
||||
* @returns {Component} - The tab. |
||||
*/ |
||||
_renderTab({ component, styles, tabId }) { |
||||
const { closeDialog } = this.props; |
||||
const TabComponent = component; |
||||
|
||||
return ( |
||||
<div className = { styles }> |
||||
<TabComponent |
||||
closeDialog = { closeDialog } |
||||
onTabStateChange |
||||
= { this._onTabStateChange } |
||||
tabId = { tabId } |
||||
{ ...this.state.tabStates[tabId] } /> |
||||
</div>); |
||||
} |
||||
|
||||
_onTabStateChange: (number, Object) => void; |
||||
|
||||
/** |
||||
* Changes the state for a tab. |
||||
* |
||||
* @param {number} tabId - The id of the tab which state will be changed. |
||||
* @param {Object} state - The new state. |
||||
* @returns {void} |
||||
*/ |
||||
_onTabStateChange(tabId, state) { |
||||
const tabStates = [ ...this.state.tabStates ]; |
||||
|
||||
tabStates[tabId] = state; |
||||
this.setState({ tabStates }); |
||||
} |
||||
|
||||
_onSubmit: () => void; |
||||
|
||||
/** |
||||
* Submits the information filled in the dialog. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onSubmit() { |
||||
const { onSubmit, tabs } = this.props; |
||||
|
||||
tabs.forEach(({ submit }, idx) => { |
||||
submit(this.state.tabStates[idx]); |
||||
}); |
||||
|
||||
onSubmit(); |
||||
} |
||||
} |
||||
|
||||
export default translate(DialogWithTabs); |
@ -1,4 +1,9 @@ |
||||
// @flow
|
||||
|
||||
export { default as BottomSheet } from './BottomSheet'; |
||||
export { default as DialogContainer } from './DialogContainer'; |
||||
export { default as Dialog } from './Dialog'; |
||||
export { default as StatelessDialog } from './StatelessDialog'; |
||||
export { default as DialogWithTabs } from './DialogWithTabs'; |
||||
export { default as AbstractDialogTab } from './AbstractDialogTab'; |
||||
export type { Props as AbstractDialogTabProps } from './AbstractDialogTab'; |
||||
|
@ -0,0 +1,354 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { AbstractDialogTab } from '../../base/dialog'; |
||||
import type { Props as AbstractDialogTabProps } from '../../base/dialog'; |
||||
import { translate } from '../../base/i18n'; |
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet'; |
||||
|
||||
import AudioInputPreview from './AudioInputPreview'; |
||||
import AudioOutputPreview from './AudioOutputPreview'; |
||||
import DeviceSelector from './DeviceSelector'; |
||||
import VideoInputPreview from './VideoInputPreview'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link DeviceSelection}. |
||||
*/ |
||||
export type Props = { |
||||
...$Exact<AbstractDialogTabProps>, |
||||
|
||||
/** |
||||
* All known audio and video devices split by type. This prop comes from |
||||
* the app state. |
||||
*/ |
||||
availableDevices: Object, |
||||
|
||||
/** |
||||
* Whether or not the audio selector can be interacted with. If true, |
||||
* the audio input selector will be rendered as disabled. This is |
||||
* specifically used to prevent audio device changing in Firefox, which |
||||
* currently does not work due to a browser-side regression. |
||||
*/ |
||||
disableAudioInputChange: boolean, |
||||
|
||||
/** |
||||
* True if device changing is configured to be disallowed. Selectors |
||||
* will display as disabled. |
||||
*/ |
||||
disableDeviceChange: boolean, |
||||
|
||||
/** |
||||
* Function that checks whether or not a new audio input source can be |
||||
* selected. |
||||
*/ |
||||
hasAudioPermission: Function, |
||||
|
||||
/** |
||||
* Function that checks whether or not a new video input sources can be |
||||
* selected. |
||||
*/ |
||||
hasVideoPermission: Function, |
||||
|
||||
/** |
||||
* If true, the audio meter will not display. Necessary for browsers or |
||||
* configurations that do not support local stats to prevent a |
||||
* non-responsive mic preview from displaying. |
||||
*/ |
||||
hideAudioInputPreview: boolean, |
||||
|
||||
/** |
||||
* Whether or not the audio output source selector should display. If |
||||
* true, the audio output selector and test audio link will not be |
||||
* rendered. This is specifically used for hiding audio output on |
||||
* temasys browsers which do not support such change. |
||||
*/ |
||||
hideAudioOutputSelect: boolean, |
||||
|
||||
/** |
||||
* The id of the audio input device to preview. |
||||
*/ |
||||
selectedAudioInputId: string, |
||||
|
||||
/** |
||||
* The id of the audio output device to preview. |
||||
*/ |
||||
selectedAudioOutputId: string, |
||||
|
||||
/** |
||||
* The id of the video input device to preview. |
||||
*/ |
||||
selectedVideoInputId: string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of {@link DeviceSelection}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* The JitsiTrack to use for previewing audio input. |
||||
*/ |
||||
previewAudioTrack: ?Object, |
||||
|
||||
/** |
||||
* The JitsiTrack to use for previewing video input. |
||||
*/ |
||||
previewVideoTrack: ?Object, |
||||
|
||||
/** |
||||
* The error message from trying to use a video input device. |
||||
*/ |
||||
previewVideoTrackError: ?string |
||||
}; |
||||
|
||||
/** |
||||
* React {@code Component} for previewing audio and video input/output devices. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class DeviceSelection extends AbstractDialogTab<Props, State> { |
||||
/** |
||||
* Initializes a new DeviceSelection instance. |
||||
* |
||||
* @param {Object} props - The read-only React Component props with which |
||||
* the new instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
previewAudioTrack: null, |
||||
previewVideoTrack: null, |
||||
previewVideoTrackError: null |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Generate the initial previews for audio input and video input. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._createAudioInputTrack(this.props.selectedAudioInputId); |
||||
this._createVideoInputTrack(this.props.selectedVideoInputId); |
||||
} |
||||
|
||||
/** |
||||
* Updates audio input and video input previews. |
||||
* |
||||
* @inheritdoc |
||||
* @param {Object} nextProps - The read-only props which this Component will |
||||
* receive. |
||||
* @returns {void} |
||||
*/ |
||||
componentWillReceiveProps(nextProps: Object) { |
||||
const { selectedAudioInputId, selectedVideoInputId } = this.props; |
||||
|
||||
if (selectedAudioInputId !== nextProps.selectedAudioInputId) { |
||||
this._createAudioInputTrack(nextProps.selectedAudioInputId); |
||||
} |
||||
|
||||
if (selectedVideoInputId !== nextProps.selectedVideoInputId) { |
||||
this._createVideoInputTrack(nextProps.selectedVideoInputId); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Ensure preview tracks are destroyed to prevent continued use. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
this._disposeAudioInputPreview(); |
||||
this._disposeVideoInputPreview(); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { |
||||
hideAudioInputPreview, |
||||
hideAudioOutputSelect, |
||||
selectedAudioOutputId |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<div className = 'device-selection'> |
||||
<div className = 'device-selection-column column-video'> |
||||
<div className = 'device-selection-video-container'> |
||||
<VideoInputPreview |
||||
error = { this.state.previewVideoTrackError } |
||||
track = { this.state.previewVideoTrack } /> |
||||
</div> |
||||
{ !hideAudioInputPreview |
||||
&& <AudioInputPreview |
||||
track = { this.state.previewAudioTrack } /> } |
||||
</div> |
||||
<div className = 'device-selection-column column-selectors'> |
||||
<div className = 'device-selectors'> |
||||
{ this._renderSelectors() } |
||||
</div> |
||||
{ !hideAudioOutputSelect |
||||
&& <AudioOutputPreview |
||||
deviceId = { selectedAudioOutputId } /> } |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Creates the JitiTrack for the audio input preview. |
||||
* |
||||
* @param {string} deviceId - The id of audio input device to preview. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_createAudioInputTrack(deviceId) { |
||||
this._disposeAudioInputPreview() |
||||
.then(() => createLocalTrack('audio', deviceId)) |
||||
.then(jitsiLocalTrack => { |
||||
this.setState({ |
||||
previewAudioTrack: jitsiLocalTrack |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
this.setState({ |
||||
previewAudioTrack: null |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Creates the JitiTrack for the video input preview. |
||||
* |
||||
* @param {string} deviceId - The id of video device to preview. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_createVideoInputTrack(deviceId) { |
||||
this._disposeVideoInputPreview() |
||||
.then(() => createLocalTrack('video', deviceId)) |
||||
.then(jitsiLocalTrack => { |
||||
if (!jitsiLocalTrack) { |
||||
return Promise.reject(); |
||||
} |
||||
|
||||
this.setState({ |
||||
previewVideoTrack: jitsiLocalTrack, |
||||
previewVideoTrackError: null |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
this.setState({ |
||||
previewVideoTrack: null, |
||||
previewVideoTrackError: |
||||
this.props.t('deviceSelection.previewUnavailable') |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Utility function for disposing the current audio input preview. |
||||
* |
||||
* @private |
||||
* @returns {Promise} |
||||
*/ |
||||
_disposeAudioInputPreview(): Promise<*> { |
||||
return this.state.previewAudioTrack |
||||
? this.state.previewAudioTrack.dispose() : Promise.resolve(); |
||||
} |
||||
|
||||
/** |
||||
* Utility function for disposing the current video input preview. |
||||
* |
||||
* @private |
||||
* @returns {Promise} |
||||
*/ |
||||
_disposeVideoInputPreview(): Promise<*> { |
||||
return this.state.previewVideoTrack |
||||
? this.state.previewVideoTrack.dispose() : Promise.resolve(); |
||||
} |
||||
|
||||
/** |
||||
* Creates a DeviceSelector instance based on the passed in configuration. |
||||
* |
||||
* @private |
||||
* @param {Object} deviceSelectorProps - The props for the DeviceSelector. |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_renderSelector(deviceSelectorProps) { |
||||
return ( |
||||
<div key = { deviceSelectorProps.label }> |
||||
<div className = 'device-selector-label'> |
||||
{ this.props.t(deviceSelectorProps.label) } |
||||
</div> |
||||
<DeviceSelector { ...deviceSelectorProps } /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Creates DeviceSelector instances for video output, audio input, and audio |
||||
* output. |
||||
* |
||||
* @private |
||||
* @returns {Array<ReactElement>} DeviceSelector instances. |
||||
*/ |
||||
_renderSelectors() { |
||||
const { availableDevices } = this.props; |
||||
|
||||
const configurations = [ |
||||
{ |
||||
devices: availableDevices.videoInput, |
||||
hasPermission: this.props.hasVideoPermission(), |
||||
icon: 'icon-camera', |
||||
isDisabled: this.props.disableDeviceChange, |
||||
key: 'videoInput', |
||||
label: 'settings.selectCamera', |
||||
onSelect: selectedVideoInputId => |
||||
super._onChange({ selectedVideoInputId }), |
||||
selectedDeviceId: this.props.selectedVideoInputId |
||||
}, |
||||
{ |
||||
devices: availableDevices.audioInput, |
||||
hasPermission: this.props.hasAudioPermission(), |
||||
icon: 'icon-microphone', |
||||
isDisabled: this.props.disableAudioInputChange |
||||
|| this.props.disableDeviceChange, |
||||
key: 'audioInput', |
||||
label: 'settings.selectMic', |
||||
onSelect: selectedAudioInputId => |
||||
super._onChange({ selectedAudioInputId }), |
||||
selectedDeviceId: this.props.selectedAudioInputId |
||||
} |
||||
]; |
||||
|
||||
if (!this.props.hideAudioOutputSelect) { |
||||
configurations.push({ |
||||
devices: availableDevices.audioOutput, |
||||
hasPermission: this.props.hasAudioPermission() |
||||
|| this.props.hasVideoPermission(), |
||||
icon: 'icon-volume', |
||||
isDisabled: this.props.disableDeviceChange, |
||||
key: 'audioOutput', |
||||
label: 'settings.selectAudioOutput', |
||||
onSelect: selectedAudioOutputId => |
||||
super._onChange({ selectedAudioOutputId }), |
||||
selectedDeviceId: this.props.selectedAudioOutputId |
||||
}); |
||||
} |
||||
|
||||
return configurations.map(config => this._renderSelector(config)); |
||||
} |
||||
} |
||||
|
||||
export default translate(DeviceSelection); |
@ -1,165 +0,0 @@ |
||||
import PropTypes from 'prop-types'; |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { |
||||
setAudioInputDevice, |
||||
setAudioOutputDevice, |
||||
setVideoInputDevice |
||||
} from '../../base/devices'; |
||||
import { hideDialog } from '../../base/dialog'; |
||||
|
||||
import DeviceSelectionDialogBase from './DeviceSelectionDialogBase'; |
||||
|
||||
/** |
||||
* React component for previewing and selecting new audio and video sources. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class DeviceSelectionDialog extends Component { |
||||
/** |
||||
* DeviceSelectionDialog component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* All known audio and video devices split by type. This prop comes from |
||||
* the app state. |
||||
*/ |
||||
_availableDevices: PropTypes.object, |
||||
|
||||
/** |
||||
* Device id for the current audio input device. This device will be set |
||||
* as the default audio input device to preview. |
||||
*/ |
||||
currentAudioInputId: PropTypes.string, |
||||
|
||||
/** |
||||
* Device id for the current audio output device. This device will be |
||||
* set as the default audio output device to preview. |
||||
*/ |
||||
currentAudioOutputId: PropTypes.string, |
||||
|
||||
/** |
||||
* Device id for the current video input device. This device will be set |
||||
* as the default video input device to preview. |
||||
*/ |
||||
currentVideoInputId: PropTypes.string, |
||||
|
||||
/** |
||||
* Whether or not the audio selector can be interacted with. If true, |
||||
* the audio input selector will be rendered as disabled. This is |
||||
* specifically used to prevent audio device changing in Firefox, which |
||||
* currently does not work due to a browser-side regression. |
||||
*/ |
||||
disableAudioInputChange: PropTypes.bool, |
||||
|
||||
/** |
||||
* True if device changing is configured to be disallowed. Selectors |
||||
* will display as disabled. |
||||
*/ |
||||
disableDeviceChange: PropTypes.bool, |
||||
|
||||
/** |
||||
* Invoked to notify the store of app state changes. |
||||
*/ |
||||
dispatch: PropTypes.func, |
||||
|
||||
/** |
||||
* Function that checks whether or not a new audio input source can be |
||||
* selected. |
||||
*/ |
||||
hasAudioPermission: PropTypes.func, |
||||
|
||||
/** |
||||
* Function that checks whether or not a new video input sources can be |
||||
* selected. |
||||
*/ |
||||
hasVideoPermission: PropTypes.func, |
||||
|
||||
/** |
||||
* If true, the audio meter will not display. Necessary for browsers or |
||||
* configurations that do not support local stats to prevent a |
||||
* non-responsive mic preview from displaying. |
||||
*/ |
||||
hideAudioInputPreview: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or not the audio output source selector should display. If |
||||
* true, the audio output selector and test audio link will not be |
||||
* rendered. This is specifically used for hiding audio output on |
||||
* temasys browsers which do not support such change. |
||||
*/ |
||||
hideAudioOutputSelect: PropTypes.bool |
||||
}; |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { |
||||
currentAudioInputId, |
||||
currentAudioOutputId, |
||||
currentVideoInputId, |
||||
disableAudioInputChange, |
||||
disableDeviceChange, |
||||
dispatch, |
||||
hasAudioPermission, |
||||
hasVideoPermission, |
||||
hideAudioInputPreview, |
||||
hideAudioOutputSelect |
||||
} = this.props; |
||||
|
||||
const props = { |
||||
availableDevices: this.props._availableDevices, |
||||
closeModal: () => dispatch(hideDialog()), |
||||
currentAudioInputId, |
||||
currentAudioOutputId, |
||||
currentVideoInputId, |
||||
disableAudioInputChange, |
||||
disableDeviceChange, |
||||
hasAudioPermission, |
||||
hasVideoPermission, |
||||
hideAudioInputPreview, |
||||
hideAudioOutputSelect, |
||||
setAudioInputDevice: id => { |
||||
dispatch(setAudioInputDevice(id)); |
||||
|
||||
return Promise.resolve(); |
||||
}, |
||||
setAudioOutputDevice: id => { |
||||
dispatch(setAudioOutputDevice(id)); |
||||
|
||||
return Promise.resolve(); |
||||
}, |
||||
setVideoInputDevice: id => { |
||||
dispatch(setVideoInputDevice(id)); |
||||
|
||||
return Promise.resolve(); |
||||
} |
||||
}; |
||||
|
||||
return <DeviceSelectionDialogBase { ...props } />; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated DeviceSelectionDialog's |
||||
* props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _availableDevices: Object |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
return { |
||||
_availableDevices: state['features/base/devices'] |
||||
}; |
||||
} |
||||
|
||||
export default connect(_mapStateToProps)(DeviceSelectionDialog); |
@ -1,560 +0,0 @@ |
||||
import PropTypes from 'prop-types'; |
||||
import React, { Component } from 'react'; |
||||
|
||||
import { StatelessDialog } from '../../base/dialog'; |
||||
import { translate } from '../../base/i18n'; |
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet'; |
||||
|
||||
import { shouldShowOnlyDeviceSelection } from '../../settings'; |
||||
|
||||
import AudioInputPreview from './AudioInputPreview'; |
||||
import AudioOutputPreview from './AudioOutputPreview'; |
||||
import DeviceSelector from './DeviceSelector'; |
||||
import VideoInputPreview from './VideoInputPreview'; |
||||
|
||||
/** |
||||
* React component for previewing and selecting new audio and video sources. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class DeviceSelectionDialogBase extends Component { |
||||
/** |
||||
* DeviceSelectionDialogBase component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* All known audio and video devices split by type. This prop comes from |
||||
* the app state. |
||||
*/ |
||||
availableDevices: PropTypes.object, |
||||
|
||||
/** |
||||
* Closes the dialog. |
||||
*/ |
||||
closeModal: PropTypes.func, |
||||
|
||||
/** |
||||
* Device id for the current audio input device. This device will be set |
||||
* as the default audio input device to preview. |
||||
*/ |
||||
currentAudioInputId: PropTypes.string, |
||||
|
||||
/** |
||||
* Device id for the current audio output device. This device will be |
||||
* set as the default audio output device to preview. |
||||
*/ |
||||
currentAudioOutputId: PropTypes.string, |
||||
|
||||
/** |
||||
* Device id for the current video input device. This device will be set |
||||
* as the default video input device to preview. |
||||
*/ |
||||
currentVideoInputId: PropTypes.string, |
||||
|
||||
/** |
||||
* Whether or not the audio selector can be interacted with. If true, |
||||
* the audio input selector will be rendered as disabled. This is |
||||
* specifically used to prevent audio device changing in Firefox, which |
||||
* currently does not work due to a browser-side regression. |
||||
*/ |
||||
disableAudioInputChange: PropTypes.bool, |
||||
|
||||
/** |
||||
* Disables dismissing the dialog when the blanket is clicked. Enabled |
||||
* by default. |
||||
*/ |
||||
disableBlanketClickDismiss: PropTypes.bool, |
||||
|
||||
/** |
||||
* True if device changing is configured to be disallowed. Selectors |
||||
* will display as disabled. |
||||
*/ |
||||
disableDeviceChange: PropTypes.bool, |
||||
|
||||
/** |
||||
* Function that checks whether or not a new audio input source can be |
||||
* selected. |
||||
*/ |
||||
hasAudioPermission: PropTypes.func, |
||||
|
||||
/** |
||||
* Function that checks whether or not a new video input sources can be |
||||
* selected. |
||||
*/ |
||||
hasVideoPermission: PropTypes.func, |
||||
|
||||
/** |
||||
* If true, the audio meter will not display. Necessary for browsers or |
||||
* configurations that do not support local stats to prevent a |
||||
* non-responsive mic preview from displaying. |
||||
*/ |
||||
hideAudioInputPreview: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or not the audio output source selector should display. If |
||||
* true, the audio output selector and test audio link will not be |
||||
* rendered. This is specifically used for hiding audio output on |
||||
* temasys browsers which do not support such change. |
||||
*/ |
||||
hideAudioOutputSelect: PropTypes.bool, |
||||
|
||||
/** |
||||
* Function that sets the audio input device. |
||||
*/ |
||||
setAudioInputDevice: PropTypes.func, |
||||
|
||||
/** |
||||
* Function that sets the audio output device. |
||||
*/ |
||||
setAudioOutputDevice: PropTypes.func, |
||||
|
||||
/** |
||||
* Function that sets the video input device. |
||||
*/ |
||||
setVideoInputDevice: PropTypes.func, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: PropTypes.func |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new DeviceSelectionDialogBase instance. |
||||
* |
||||
* @param {Object} props - The read-only React Component props with which |
||||
* the new instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
const { availableDevices } = this.props; |
||||
|
||||
this.state = { |
||||
// JitsiLocalTrack to use for live previewing of audio input.
|
||||
previewAudioTrack: null, |
||||
|
||||
// JitsiLocalTrack to use for live previewing of video input.
|
||||
previewVideoTrack: null, |
||||
|
||||
// An message describing a problem with obtaining a video preview.
|
||||
previewVideoTrackError: null, |
||||
|
||||
// The audio input device id to show as selected by default.
|
||||
selectedAudioInputId: this.props.currentAudioInputId || '', |
||||
|
||||
// The audio output device id to show as selected by default.
|
||||
selectedAudioOutputId: this.props.currentAudioOutputId || '', |
||||
|
||||
// The video input device id to show as selected by default.
|
||||
// FIXME: On temasys, without a device selected and put into local
|
||||
// storage as the default device to use, the current video device id
|
||||
// is a blank string. This is because the library gets a local video
|
||||
// track and then maps the track's device id by matching the track's
|
||||
// label to the MediaDeviceInfos returned from enumerateDevices. In
|
||||
// WebRTC, the track label is expected to return the camera device
|
||||
// label. However, temasys video track labels refer to track id, not
|
||||
// device label, so the library cannot match the track to a device.
|
||||
// The workaround of defaulting to the first videoInput available
|
||||
// is re-used from the previous device settings implementation.
|
||||
selectedVideoInputId: this.props.currentVideoInputId |
||||
|| (availableDevices.videoInput |
||||
&& availableDevices.videoInput[0] |
||||
&& availableDevices.videoInput[0].deviceId) |
||||
|| '' |
||||
}; |
||||
|
||||
// Preventing closing while cleaning up previews is important for
|
||||
// supporting temasys video cleanup. Temasys requires its video object
|
||||
// to be in the dom and visible for proper detaching of tracks. Delaying
|
||||
// closure until cleanup is complete ensures no errors in the process.
|
||||
this._isClosing = false; |
||||
|
||||
this._setDevicesAndClose = this._setDevicesAndClose.bind(this); |
||||
this._onCancel = this._onCancel.bind(this); |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
this._updateAudioOutput = this._updateAudioOutput.bind(this); |
||||
this._updateAudioInput = this._updateAudioInput.bind(this); |
||||
this._updateVideoInput = this._updateVideoInput.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Sets default device choices so a choice is pre-selected in the dropdowns |
||||
* and live previews are created. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
this._updateAudioOutput(this.state.selectedAudioOutputId); |
||||
this._updateAudioInput(this.state.selectedAudioInputId); |
||||
this._updateVideoInput(this.state.selectedVideoInputId); |
||||
} |
||||
|
||||
/** |
||||
* Disposes preview tracks that might not already be disposed. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
// This handles the case where neither submit nor cancel were triggered,
|
||||
// such as on modal switch. In that case, make a dying attempt to clean
|
||||
// up previews.
|
||||
if (!this._isClosing) { |
||||
this._attemptPreviewTrackCleanup(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<StatelessDialog |
||||
cancelTitleKey = { 'dialog.Cancel' } |
||||
disableBlanketClickDismiss |
||||
= { this.props.disableBlanketClickDismiss } |
||||
okTitleKey = { 'dialog.Save' } |
||||
onCancel = { this._onCancel } |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = { this._getModalTitle() }> |
||||
<div className = 'device-selection'> |
||||
<div className = 'device-selection-column column-video'> |
||||
<div className = 'device-selection-video-container'> |
||||
<VideoInputPreview |
||||
error = { this.state.previewVideoTrackError } |
||||
track = { this.state.previewVideoTrack } /> |
||||
</div> |
||||
{ this._renderAudioInputPreview() } |
||||
</div> |
||||
<div className = 'device-selection-column column-selectors'> |
||||
<div className = 'device-selectors'> |
||||
{ this._renderSelectors() } |
||||
</div> |
||||
{ this._renderAudioOutputPreview() } |
||||
</div> |
||||
</div> |
||||
</StatelessDialog> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Cleans up preview tracks if they are not active tracks. |
||||
* |
||||
* @private |
||||
* @returns {Array<Promise>} Zero to two promises will be returned. One |
||||
* promise can be for video cleanup and another for audio cleanup. |
||||
*/ |
||||
_attemptPreviewTrackCleanup() { |
||||
return Promise.all([ |
||||
this._disposeVideoPreview(), |
||||
this._disposeAudioPreview() |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Utility function for disposing the current audio preview. |
||||
* |
||||
* @private |
||||
* @returns {Promise} |
||||
*/ |
||||
_disposeAudioPreview() { |
||||
return this.state.previewAudioTrack |
||||
? this.state.previewAudioTrack.dispose() : Promise.resolve(); |
||||
} |
||||
|
||||
/** |
||||
* Utility function for disposing the current video preview. |
||||
* |
||||
* @private |
||||
* @returns {Promise} |
||||
*/ |
||||
_disposeVideoPreview() { |
||||
return this.state.previewVideoTrack |
||||
? this.state.previewVideoTrack.dispose() : Promise.resolve(); |
||||
} |
||||
|
||||
/** |
||||
* Returns what the title of the device selection modal should be. |
||||
* |
||||
* Note: This is temporary logic to appease design sooner. Device selection |
||||
* and all other settings will be combined into one modal. |
||||
* |
||||
* @returns {string} |
||||
*/ |
||||
_getModalTitle() { |
||||
if (shouldShowOnlyDeviceSelection()) { |
||||
return 'settings.title'; |
||||
} |
||||
|
||||
return 'deviceSelection.deviceSettings'; |
||||
} |
||||
|
||||
/** |
||||
* Disposes preview tracks and signals to |
||||
* close DeviceSelectionDialogBase. |
||||
* |
||||
* @private |
||||
* @returns {boolean} Returns false to prevent closure until cleanup is |
||||
* complete. |
||||
*/ |
||||
_onCancel() { |
||||
if (this._isClosing) { |
||||
return false; |
||||
} |
||||
|
||||
this._isClosing = true; |
||||
|
||||
const cleanupPromises = this._attemptPreviewTrackCleanup(); |
||||
|
||||
Promise.all(cleanupPromises) |
||||
.then(this.props.closeModal) |
||||
.catch(this.props.closeModal); |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Identifies changes to the preferred input/output devices and perform |
||||
* necessary cleanup and requests to use those devices. Closes the modal |
||||
* after cleanup and device change requests complete. |
||||
* |
||||
* @private |
||||
* @returns {boolean} Returns false to prevent closure until cleanup is |
||||
* complete. |
||||
*/ |
||||
_onSubmit() { |
||||
if (this._isClosing) { |
||||
return false; |
||||
} |
||||
|
||||
this._isClosing = true; |
||||
|
||||
this._attemptPreviewTrackCleanup() |
||||
.then(this._setDevicesAndClose, this._setDevicesAndClose); |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Creates an AudioInputPreview for previewing if audio is being received. |
||||
* Null will be returned if local stats for tracking audio input levels |
||||
* cannot be obtained. |
||||
* |
||||
* @private |
||||
* @returns {ReactComponent|null} |
||||
*/ |
||||
_renderAudioInputPreview() { |
||||
if (this.props.hideAudioInputPreview) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<AudioInputPreview |
||||
track = { this.state.previewAudioTrack } /> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Creates an AudioOutputPreview instance for playing a test sound with the |
||||
* passed in device id. Null will be returned if hideAudioOutput is truthy. |
||||
* |
||||
* @private |
||||
* @returns {ReactComponent|null} |
||||
*/ |
||||
_renderAudioOutputPreview() { |
||||
if (this.props.hideAudioOutputSelect) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<AudioOutputPreview |
||||
deviceId = { this.state.selectedAudioOutputId } /> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Creates a DeviceSelector instance based on the passed in configuration. |
||||
* |
||||
* @private |
||||
* @param {Object} deviceSelectorProps - The props for the DeviceSelector. |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_renderSelector(deviceSelectorProps) { |
||||
|
||||
return ( |
||||
<div key = { deviceSelectorProps.label }> |
||||
<div className = 'device-selector-label'> |
||||
{ this.props.t(deviceSelectorProps.label) } |
||||
</div> |
||||
<DeviceSelector { ...deviceSelectorProps } /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Creates DeviceSelector instances for video output, audio input, and audio |
||||
* output. |
||||
* |
||||
* @private |
||||
* @returns {Array<ReactElement>} DeviceSelector instances. |
||||
*/ |
||||
_renderSelectors() { |
||||
const { availableDevices } = this.props; |
||||
|
||||
const configurations = [ |
||||
{ |
||||
devices: availableDevices.videoInput, |
||||
hasPermission: this.props.hasVideoPermission(), |
||||
icon: 'icon-camera', |
||||
isDisabled: this.props.disableDeviceChange, |
||||
key: 'videoInput', |
||||
label: 'settings.selectCamera', |
||||
onSelect: this._updateVideoInput, |
||||
selectedDeviceId: this.state.selectedVideoInputId |
||||
}, |
||||
{ |
||||
devices: availableDevices.audioInput, |
||||
hasPermission: this.props.hasAudioPermission(), |
||||
icon: 'icon-microphone', |
||||
isDisabled: this.props.disableAudioInputChange |
||||
|| this.props.disableDeviceChange, |
||||
key: 'audioInput', |
||||
label: 'settings.selectMic', |
||||
onSelect: this._updateAudioInput, |
||||
selectedDeviceId: this.state.selectedAudioInputId |
||||
} |
||||
]; |
||||
|
||||
if (!this.props.hideAudioOutputSelect) { |
||||
configurations.push({ |
||||
devices: availableDevices.audioOutput, |
||||
hasPermission: this.props.hasAudioPermission() |
||||
|| this.props.hasVideoPermission(), |
||||
icon: 'icon-volume', |
||||
isDisabled: this.props.disableDeviceChange, |
||||
key: 'audioOutput', |
||||
label: 'settings.selectAudioOutput', |
||||
onSelect: this._updateAudioOutput, |
||||
selectedDeviceId: this.state.selectedAudioOutputId |
||||
}); |
||||
} |
||||
|
||||
return configurations.map(config => this._renderSelector(config)); |
||||
} |
||||
|
||||
/** |
||||
* Sets the selected devices and closes the dialog. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_setDevicesAndClose() { |
||||
const { |
||||
setVideoInputDevice, |
||||
setAudioInputDevice, |
||||
setAudioOutputDevice, |
||||
closeModal |
||||
} = this.props; |
||||
|
||||
const promises = []; |
||||
|
||||
if (this.state.selectedVideoInputId |
||||
!== this.props.currentVideoInputId) { |
||||
promises.push(setVideoInputDevice(this.state.selectedVideoInputId)); |
||||
} |
||||
|
||||
if (this.state.selectedAudioInputId |
||||
!== this.props.currentAudioInputId) { |
||||
promises.push(setAudioInputDevice(this.state.selectedAudioInputId)); |
||||
} |
||||
|
||||
if (this.state.selectedAudioOutputId |
||||
!== this.props.currentAudioOutputId) { |
||||
promises.push( |
||||
setAudioOutputDevice(this.state.selectedAudioOutputId)); |
||||
} |
||||
Promise.all(promises).then(closeModal, closeModal); |
||||
} |
||||
|
||||
/** |
||||
* Callback invoked when a new audio input device has been selected. Updates |
||||
* the internal state of the user's selection as well as the audio track |
||||
* that should display in the preview. |
||||
* |
||||
* @param {string} deviceId - The id of the chosen audio input device. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_updateAudioInput(deviceId) { |
||||
this.setState({ |
||||
selectedAudioInputId: deviceId |
||||
}, () => { |
||||
this._disposeAudioPreview() |
||||
.then(() => createLocalTrack('audio', deviceId)) |
||||
.then(jitsiLocalTrack => { |
||||
this.setState({ |
||||
previewAudioTrack: jitsiLocalTrack |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
this.setState({ |
||||
previewAudioTrack: null |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Callback invoked when a new audio output device has been selected. |
||||
* Updates the internal state of the user's selection. |
||||
* |
||||
* @param {string} deviceId - The id of the chosen audio output device. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_updateAudioOutput(deviceId) { |
||||
this.setState({ |
||||
selectedAudioOutputId: deviceId |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Callback invoked when a new video input device has been selected. Updates |
||||
* the internal state of the user's selection as well as the video track |
||||
* that should display in the preview. |
||||
* |
||||
* @param {string} deviceId - The id of the chosen video input device. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_updateVideoInput(deviceId) { |
||||
this.setState({ |
||||
selectedVideoInputId: deviceId |
||||
}, () => { |
||||
this._disposeVideoPreview() |
||||
.then(() => createLocalTrack('video', deviceId)) |
||||
.then(jitsiLocalTrack => { |
||||
if (!jitsiLocalTrack) { |
||||
return Promise.reject(); |
||||
} |
||||
|
||||
this.setState({ |
||||
previewVideoTrack: jitsiLocalTrack, |
||||
previewVideoTrackError: null |
||||
}); |
||||
}) |
||||
.catch(() => { |
||||
this.setState({ |
||||
previewVideoTrack: null, |
||||
previewVideoTrackError: |
||||
this.props.t('deviceSelection.previewUnavailable') |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export default translate(DeviceSelectionDialogBase); |
@ -1,3 +1,4 @@ |
||||
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog'; |
||||
export { default as DeviceSelectionDialogBase } |
||||
from './DeviceSelectionDialogBase'; |
||||
// @flow
|
||||
|
||||
export { default as DeviceSelection } from './DeviceSelection'; |
||||
export type { Props as DeviceSelectionProps } from './DeviceSelection'; |
||||
|
@ -0,0 +1,35 @@ |
||||
// @flow
|
||||
import { getAudioOutputDeviceId } from '../base/devices'; |
||||
import JitsiMeetJS from '../base/lib-jitsi-meet'; |
||||
import { toState } from '../base/redux'; |
||||
|
||||
/** |
||||
* Returns the properties for the device selection dialog from Redux state. |
||||
* |
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's |
||||
* {@code getState} function to be used to retrieve the state. |
||||
* @returns {Object} - The properties for the device selection dialog. |
||||
*/ |
||||
export function getDeviceSelectionDialogProps(stateful: Object | Function) { |
||||
const state = toState(stateful); |
||||
const settings = state['features/base/settings']; |
||||
|
||||
return { |
||||
availableDevices: state['features/base/devices'], |
||||
disableAudioInputChange: |
||||
!JitsiMeetJS.isMultipleAudioInputSupported(), |
||||
disableDeviceChange: |
||||
!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(), |
||||
hasAudioPermission: JitsiMeetJS.mediaDevices |
||||
.isDevicePermissionGranted.bind(null, 'audio'), |
||||
hasVideoPermission: JitsiMeetJS.mediaDevices |
||||
.isDevicePermissionGranted.bind(null, 'video'), |
||||
hideAudioInputPreview: |
||||
!JitsiMeetJS.isCollectingLocalStats(), |
||||
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices |
||||
.isDeviceChangeAvailable('output'), |
||||
selectedAudioInputId: settings.micDeviceId, |
||||
selectedAudioOutputId: getAudioOutputDeviceId(), |
||||
selectedVideoInputId: settings.cameraDeviceId |
||||
}; |
||||
} |
@ -1,6 +1,7 @@ |
||||
export * from './actions'; |
||||
export * from './actionTypes'; |
||||
export * from './components'; |
||||
export * from './functions'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
||||
|
@ -1,87 +0,0 @@ |
||||
import Button from '@atlaskit/button'; |
||||
import PropTypes from 'prop-types'; |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import { openDeviceSelectionDialog } from '../../../device-selection'; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displays a button for opening the |
||||
* {@code DeviceSelectionDialog}. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class DeviceSelectionButton extends Component { |
||||
/** |
||||
* {@code DeviceSelectionButton} component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* Invoked to display the {@code DeviceSelectionDialog}. |
||||
*/ |
||||
dispatch: PropTypes.func, |
||||
|
||||
/** |
||||
* Whether or not the button's title should be displayed. |
||||
*/ |
||||
showTitle: PropTypes.bool, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: PropTypes.func |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code DeviceSelectionButton} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onOpenDeviceSelectionDialog |
||||
= this._onOpenDeviceSelectionDialog.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<div> |
||||
{ this.props.showTitle |
||||
? <div className = 'subTitle'> |
||||
{ this.props.t('settings.audioVideo') } |
||||
</div> |
||||
: null } |
||||
<Button |
||||
appearance = 'primary' |
||||
onClick = { this._onOpenDeviceSelectionDialog } |
||||
shouldFitContainer = { true }> |
||||
{ this.props.t('deviceSelection.deviceSettings') } |
||||
</Button> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Opens the {@code DeviceSelectionDialog}. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onOpenDeviceSelectionDialog() { |
||||
this.props.dispatch(openDeviceSelectionDialog()); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(DeviceSelectionButton)); |
@ -1,179 +0,0 @@ |
||||
import DropdownMenu, { |
||||
DropdownItem, |
||||
DropdownItemGroup |
||||
} from '@atlaskit/dropdown-menu'; |
||||
import PropTypes from 'prop-types'; |
||||
import React, { Component } from 'react'; |
||||
|
||||
import { DEFAULT_LANGUAGE, LANGUAGES, translate } from '../../../base/i18n'; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displays a dropdown for changing |
||||
* application text to another language. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class LanguageSelectDropdown extends Component { |
||||
/** |
||||
* {@code LanguageSelectDropdown} component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The translation service. |
||||
*/ |
||||
i18n: PropTypes.object, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: PropTypes.func |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* {@code LanguageSelectDropdown} component's local state. |
||||
* |
||||
* @type {Object} |
||||
* @property {string|null} currentLanguage - The currently selected language |
||||
* the application should be displayed in. |
||||
* @property {boolean} isLanguageSelectOpen - Whether or not the dropdown |
||||
* should be displayed as open. |
||||
*/ |
||||
state = { |
||||
currentLanguage: null, |
||||
isLanguageSelectOpen: false |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code LanguageSelectDropdown} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state.currentLanguage |
||||
= this.props.i18n.language || DEFAULT_LANGUAGE; |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onLanguageSelected = this._onLanguageSelected.bind(this); |
||||
this._onSetDropdownOpen = this._onSetDropdownOpen.bind(this); |
||||
this._setCurrentLanguage = this._setCurrentLanguage.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Sets a listener to update the currently selected language if it is |
||||
* changed from somewhere else. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentDidMount() { |
||||
this.props.i18n.on('languageChanged', this._setCurrentLanguage); |
||||
} |
||||
|
||||
/** |
||||
* Removes all listeners. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentWillUnmount() { |
||||
this.props.i18n.off('languageChanged', this._setCurrentLanguage); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { t } = this.props; |
||||
const { currentLanguage } = this.state; |
||||
|
||||
const languageItems = LANGUAGES.map(language => |
||||
// eslint-disable-next-line react/jsx-wrap-multilines
|
||||
<DropdownItem |
||||
key = { language } |
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { () => this._onLanguageSelected(language) }> |
||||
{ t(`languages:${language}`) } |
||||
</DropdownItem> |
||||
); |
||||
|
||||
return ( |
||||
<div> |
||||
<DropdownMenu |
||||
isOpen = { this.state.isLanguageSelectOpen } |
||||
onOpenChange = { this._onSetDropdownOpen } |
||||
shouldFitContainer = { true } |
||||
trigger = { currentLanguage |
||||
? t(`languages:${currentLanguage}`) |
||||
: '' } |
||||
triggerButtonProps = {{ |
||||
appearance: 'primary', |
||||
shouldFitContainer: true |
||||
}} |
||||
triggerType = 'button'> |
||||
<DropdownItemGroup> |
||||
{ languageItems } |
||||
</DropdownItemGroup> |
||||
</DropdownMenu> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Updates the application's currently displayed language. |
||||
* |
||||
* @param {string} language - The language code for the language to display. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onLanguageSelected(language) { |
||||
const previousLanguage = this.state.currentLanguage; |
||||
|
||||
this.setState({ |
||||
currentLanguage: language, |
||||
isLanguageSelectOpen: false |
||||
}); |
||||
|
||||
this.props.i18n.changeLanguage(language, error => { |
||||
if (error) { |
||||
this._setCurrentLanguage(previousLanguage); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Set whether or not the dropdown should be open. |
||||
* |
||||
* @param {Object} dropdownEvent - The event returned from requesting the |
||||
* open state of the dropdown be changed. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSetDropdownOpen(dropdownEvent) { |
||||
this.setState({ |
||||
isLanguageSelectOpen: dropdownEvent.isOpen |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Updates the known current language of the application. |
||||
* |
||||
* @param {string} currentLanguage - The language code for the current |
||||
* language. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_setCurrentLanguage(currentLanguage) { |
||||
this.setState({ currentLanguage }); |
||||
} |
||||
} |
||||
|
||||
export default translate(LanguageSelectDropdown); |
@ -1,199 +0,0 @@ |
||||
import PropTypes from 'prop-types'; |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { setFollowMe, setStartMutedPolicy } from '../../../base/conference'; |
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displays checkboxes for enabling |
||||
* and disabling moderator-only conference features. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class ModeratorCheckboxes extends Component { |
||||
/** |
||||
* {@code ModeratorCheckboxes} component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* Whether or not the Follow Me feature is currently enabled. |
||||
*/ |
||||
_followMeEnabled: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or not new members will join the conference as audio muted. |
||||
*/ |
||||
_startAudioMutedPolicy: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or note new member will join the conference as video muted. |
||||
*/ |
||||
_startVideoMutedPolicy: PropTypes.bool, |
||||
|
||||
/** |
||||
* Invoked to enable and disable moderator-only conference features. |
||||
*/ |
||||
dispatch: PropTypes.func, |
||||
|
||||
/** |
||||
* Whether or not the title should be displayed. |
||||
*/ |
||||
showTitle: PropTypes.bool, |
||||
|
||||
/** |
||||
* Invokted to obtain translated strings. |
||||
*/ |
||||
t: PropTypes.func |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code ModeratorCheckboxes} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onSetFollowMeSetting |
||||
= this._onSetFollowMeSetting.bind(this); |
||||
this._onSetStartAudioMutedPolicy |
||||
= this._onSetStartAudioMutedPolicy.bind(this); |
||||
this._onSetStartVideoMutedPolicy |
||||
= this._onSetStartVideoMutedPolicy.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
_followMeEnabled, |
||||
_startAudioMutedPolicy, |
||||
_startVideoMutedPolicy, |
||||
showTitle, |
||||
t |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
{ showTitle |
||||
? <div className = 'subTitle'> |
||||
{ t('settings.moderator') } |
||||
</div> |
||||
: null } |
||||
<div className = 'moderator-option'> |
||||
<input |
||||
checked = { _startAudioMutedPolicy } |
||||
className = 'moderator-checkbox' |
||||
id = 'startAudioMuted' |
||||
onChange = { this._onSetStartAudioMutedPolicy } |
||||
type = 'checkbox' /> |
||||
<label |
||||
className = 'moderator-checkbox-label' |
||||
htmlFor = 'startAudioMuted'> |
||||
{ t('settings.startAudioMuted') } |
||||
</label> |
||||
</div> |
||||
<div className = 'moderator-option'> |
||||
<input |
||||
checked = { _startVideoMutedPolicy } |
||||
className = 'moderator-checkbox' |
||||
id = 'startVideoMuted' |
||||
onChange = { this._onSetStartVideoMutedPolicy } |
||||
type = 'checkbox' /> |
||||
<label |
||||
className = 'moderator-checkbox-label' |
||||
htmlFor = 'startVideoMuted'> |
||||
{ t('settings.startVideoMuted') } |
||||
</label> |
||||
</div> |
||||
<div className = 'moderator-option'> |
||||
<input |
||||
checked = { _followMeEnabled } |
||||
className = 'moderator-checkbox' |
||||
id = 'followMeCheckBox' |
||||
onChange = { this._onSetFollowMeSetting } |
||||
type = 'checkbox' /> |
||||
<label |
||||
className = 'moderator-checkbox-label' |
||||
htmlFor = 'followMeCheckBox'> |
||||
{ t('settings.followMe') } |
||||
</label> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Toggles the Follow Me feature. |
||||
* |
||||
* @param {Object} event - The dom event returned from changes the checkbox. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSetFollowMeSetting(event) { |
||||
this.props.dispatch(setFollowMe(event.target.checked)); |
||||
} |
||||
|
||||
/** |
||||
* Toggles whether or not new members should join the conference as audio |
||||
* muted. |
||||
* |
||||
* @param {Object} event - The dom event returned from changes the checkbox. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSetStartAudioMutedPolicy(event) { |
||||
this.props.dispatch(setStartMutedPolicy( |
||||
event.target.checked, this.props._startVideoMutedPolicy)); |
||||
} |
||||
|
||||
/** |
||||
* Toggles whether or not new members should join the conference as video |
||||
* muted. |
||||
* |
||||
* @param {Object} event - The dom event returned from changes the checkbox. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSetStartVideoMutedPolicy(event) { |
||||
this.props.dispatch(setStartMutedPolicy( |
||||
this.props._startAudioMutedPolicy, event.target.checked)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated props for the |
||||
* {@code ModeratorCheckboxes} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _followMeEnabled: boolean, |
||||
* _startAudioMutedPolicy: boolean, |
||||
* _startVideoMutedPolicy: boolean |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { |
||||
followMeEnabled, |
||||
startAudioMutedPolicy, |
||||
startVideoMutedPolicy |
||||
} = state['features/base/conference']; |
||||
|
||||
return { |
||||
_followMeEnabled: Boolean(followMeEnabled), |
||||
_startAudioMutedPolicy: Boolean(startAudioMutedPolicy), |
||||
_startVideoMutedPolicy: Boolean(startVideoMutedPolicy) |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(ModeratorCheckboxes)); |
@ -0,0 +1,239 @@ |
||||
// @flow
|
||||
|
||||
import { CheckboxGroup, CheckboxStateless } from '@atlaskit/checkbox'; |
||||
import DropdownMenu, { |
||||
DropdownItem, |
||||
DropdownItemGroup |
||||
} from '@atlaskit/dropdown-menu'; |
||||
import React from 'react'; |
||||
|
||||
import { AbstractDialogTab } from '../../../base/dialog'; |
||||
import type { Props as AbstractDialogTabProps } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link MoreTab}. |
||||
*/ |
||||
export type Props = { |
||||
...$Exact<AbstractDialogTabProps>, |
||||
|
||||
/** |
||||
* The currently selected language to display in the language select |
||||
* dropdown. |
||||
*/ |
||||
currentLanguage: string, |
||||
|
||||
/** |
||||
* Whether or not the user has selected the Follow Me feature to be enabled. |
||||
*/ |
||||
followMeEnabled: boolean, |
||||
|
||||
/** |
||||
* All available languages to display in the language select dropdown. |
||||
*/ |
||||
languages: Array<string>, |
||||
|
||||
/** |
||||
* Whether or not to display the language select dropdown. |
||||
*/ |
||||
showLanguageSettings: boolean, |
||||
|
||||
/** |
||||
* Whether or not to display moderator-only settings. |
||||
*/ |
||||
showModeratorSettings: boolean, |
||||
|
||||
/** |
||||
* Whether or not the user has selected the Start Audio Muted feature to be |
||||
* enabled. |
||||
*/ |
||||
startAudioMuted: boolean, |
||||
|
||||
/** |
||||
* Whether or not the user has selected the Start Video Muted feature to be |
||||
* enabled. |
||||
*/ |
||||
startVideoMuted: boolean, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of {@link MoreTab}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* Whether or not the language select dropdown is open. |
||||
*/ |
||||
isLanguageSelectOpen: boolean |
||||
}; |
||||
|
||||
/** |
||||
* React {@code Component} for modifying language and moderator settings. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class MoreTab extends AbstractDialogTab<Props, State> { |
||||
/** |
||||
* Initializes a new {@code MoreTab} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
isLanguageSelectOpen: false |
||||
}; |
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onLanguageDropdownOpenChange |
||||
= this._onLanguageDropdownOpenChange.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { showModeratorSettings, showLanguageSettings } = this.props; |
||||
const content = []; |
||||
|
||||
if (showModeratorSettings) { |
||||
content.push(this._renderModeratorSettings()); |
||||
} |
||||
|
||||
if (showLanguageSettings) { |
||||
content.push(this._renderLangaugeSelect()); |
||||
} |
||||
|
||||
return <div className = 'more-tab'>{ content }</div>; |
||||
} |
||||
|
||||
_onLanguageDropdownOpenChange: (Object) => void; |
||||
|
||||
/** |
||||
* Callback invoked to toggle display of the language select dropdown. |
||||
* |
||||
* @param {Object} event - The event for opening or closing the dropdown. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onLanguageDropdownOpenChange({ isOpen }) { |
||||
this.setState({ isLanguageSelectOpen: isOpen }); |
||||
} |
||||
|
||||
/** |
||||
* Returns the menu item for changing displayed language. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_renderLangaugeSelect() { |
||||
const { |
||||
currentLanguage, |
||||
languages, |
||||
t |
||||
} = this.props; |
||||
|
||||
const languageItems = languages.map(language => |
||||
// eslint-disable-next-line react/jsx-wrap-multilines
|
||||
<DropdownItem |
||||
key = { language } |
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { |
||||
() => super._onChange({ currentLanguage: language }) }> |
||||
{ t(`languages:${language}`) } |
||||
</DropdownItem> |
||||
); |
||||
|
||||
return ( |
||||
<div |
||||
className = 'settings-sub-pane language-settings' |
||||
key = 'language'> |
||||
<div className = 'mock-atlaskit-label'> |
||||
{ t('settings.language') } |
||||
</div> |
||||
<DropdownMenu |
||||
isOpen = { this.state.isLanguageSelectOpen } |
||||
onOpenChange = { this._onLanguageDropdownOpenChange } |
||||
shouldFitContainer = { true } |
||||
trigger = { currentLanguage |
||||
? t(`languages:${currentLanguage}`) |
||||
: '' } |
||||
triggerButtonProps = {{ |
||||
appearance: 'primary', |
||||
shouldFitContainer: true |
||||
}} |
||||
triggerType = 'button'> |
||||
<DropdownItemGroup> |
||||
{ languageItems } |
||||
</DropdownItemGroup> |
||||
</DropdownMenu> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Returns the React Element for modifying conference-wide settings. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_renderModeratorSettings() { |
||||
const { |
||||
followMeEnabled, |
||||
startAudioMuted, |
||||
startVideoMuted, |
||||
t |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<div |
||||
className = 'settings-sub-pane' |
||||
key = 'moderator'> |
||||
<div className = 'mock-atlaskit-label'> |
||||
{ t('settings.moderator') } |
||||
</div> |
||||
<CheckboxGroup> |
||||
<CheckboxStateless |
||||
isChecked = { startAudioMuted } |
||||
label = { t('settings.startAudioMuted') } |
||||
name = 'start-audio-muted' |
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { |
||||
({ target: { checked } }) => |
||||
super._onChange({ startAudioMuted: checked }) |
||||
} /> |
||||
<CheckboxStateless |
||||
isChecked = { startVideoMuted } |
||||
label = { t('settings.startVideoMuted') } |
||||
name = 'start-video-muted' |
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { |
||||
({ target: { checked } }) => |
||||
super._onChange({ startVideoMuted: checked }) |
||||
} /> |
||||
<CheckboxStateless |
||||
isChecked = { followMeEnabled } |
||||
label = { t('settings.followMe') } |
||||
name = 'follow-me' |
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { |
||||
({ target: { checked } }) => |
||||
super._onChange({ followMeEnabled: checked }) |
||||
} /> |
||||
</CheckboxGroup> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(MoreTab); |
@ -0,0 +1,188 @@ |
||||
// @flow
|
||||
|
||||
import Button from '@atlaskit/button'; |
||||
import { FieldTextStateless } from '@atlaskit/field-text'; |
||||
import React from 'react'; |
||||
|
||||
import { AbstractDialogTab } from '../../../base/dialog'; |
||||
import type { Props as AbstractDialogTabProps } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import UIEvents from '../../../../../service/UI/UIEvents'; |
||||
import { |
||||
sendAnalytics, |
||||
createProfilePanelButtonEvent |
||||
} from '../../../analytics'; |
||||
|
||||
declare var APP: Object; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link ProfileTab}. |
||||
*/ |
||||
export type Props = { |
||||
...$Exact<AbstractDialogTabProps>, |
||||
|
||||
/** |
||||
* Whether or not server-side authentication is available. |
||||
*/ |
||||
authEnabled: boolean, |
||||
|
||||
/** |
||||
* The name of the currently (server-side) authenticated user. |
||||
*/ |
||||
authLogin: string, |
||||
|
||||
/** |
||||
* The display name to display for the local participant. |
||||
*/ |
||||
displayName: string, |
||||
|
||||
/** |
||||
* The email to display for the local participant. |
||||
*/ |
||||
email: string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
} |
||||
|
||||
/** |
||||
* React {@code Component} for modifying the local user's profile. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class ProfileTab extends AbstractDialogTab<Props> { |
||||
/** |
||||
* Initializes a new {@code ConnectedSettingsDialog} instance. |
||||
* |
||||
* @param {Props} props - The React {@code Component} props to initialize |
||||
* the new {@code ConnectedSettingsDialog} instance with. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onAuthToggle = this._onAuthToggle.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
authEnabled, |
||||
displayName, |
||||
email, |
||||
t |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
<div className = 'profile-edit'> |
||||
<div className = 'profile-edit-field'> |
||||
<FieldTextStateless |
||||
autoFocus = { true } |
||||
compact = { true } |
||||
id = 'setDisplayName' |
||||
label = { t('profile.setDisplayNameLabel') } |
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { |
||||
({ target: { value } }) => |
||||
super._onChange({ displayName: value }) |
||||
} |
||||
placeholder = { t('settings.name') } |
||||
shouldFitContainer = { true } |
||||
type = 'text' |
||||
value = { displayName } /> |
||||
</div> |
||||
<div className = 'profile-edit-field'> |
||||
<FieldTextStateless |
||||
compact = { true } |
||||
id = 'setEmail' |
||||
label = { t('profile.setEmailLabel') } |
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { |
||||
({ target: { value } }) => |
||||
super._onChange({ email: value }) |
||||
} |
||||
placeholder = { t('profile.setEmailInput') } |
||||
shouldFitContainer = { true } |
||||
type = 'text' |
||||
value = { email } /> |
||||
</div> |
||||
</div> |
||||
{ authEnabled && this._renderAuth() } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
_onAuthToggle: () => void; |
||||
|
||||
/** |
||||
* Shows the dialog for logging in or out of a server and closes this |
||||
* dialog. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onAuthToggle() { |
||||
if (this.props.authLogin) { |
||||
sendAnalytics(createProfilePanelButtonEvent('logout.button')); |
||||
|
||||
APP.UI.messageHandler.openTwoButtonDialog({ |
||||
leftButtonKey: 'dialog.Yes', |
||||
msgKey: 'dialog.logoutQuestion', |
||||
submitFunction(evt, yes) { |
||||
if (yes) { |
||||
APP.UI.emitEvent(UIEvents.LOGOUT); |
||||
} |
||||
}, |
||||
titleKey: 'dialog.logoutTitle' |
||||
}); |
||||
} else { |
||||
sendAnalytics(createProfilePanelButtonEvent('login.button')); |
||||
|
||||
APP.UI.emitEvent(UIEvents.AUTH_CLICKED); |
||||
} |
||||
|
||||
this.props.closeDialog(); |
||||
} |
||||
|
||||
/** |
||||
* Returns a React Element for interacting with server-side authentication. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_renderAuth() { |
||||
const { |
||||
authLogin, |
||||
t |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<div> |
||||
<div className = 'mock-atlaskit-label'> |
||||
{ t('toolbar.authenticate') } |
||||
</div> |
||||
{ authLogin |
||||
&& <div className = 'auth-name'> |
||||
{ t('settings.loggedIn', { name: authLogin }) } |
||||
</div> } |
||||
<Button |
||||
appearance = 'primary' |
||||
id = 'login_button' |
||||
onClick = { this._onAuthToggle } |
||||
type = 'button'> |
||||
{ authLogin ? t('toolbar.logout') : t('toolbar.login') } |
||||
</Button> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(ProfileTab); |
@ -0,0 +1,167 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { DialogWithTabs, hideDialog } from '../../../base/dialog'; |
||||
import { |
||||
DeviceSelection, |
||||
getDeviceSelectionDialogProps, |
||||
submitDeviceSelectionTab |
||||
} from '../../../device-selection'; |
||||
|
||||
import MoreTab from './MoreTab'; |
||||
import ProfileTab from './ProfileTab'; |
||||
import { getMoreTabProps, getProfileTabProps } from '../../functions'; |
||||
import { submitMoreTab, submitProfileTab } from '../../actions'; |
||||
import { SETTINGS_TABS } from '../../constants'; |
||||
|
||||
declare var APP: Object; |
||||
declare var interfaceConfig: Object; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link ConnectedSettingsDialog}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Which settings tab should be initially displayed. If not defined then |
||||
* the first tab will be displayed. |
||||
*/ |
||||
defaultTab: string, |
||||
|
||||
/** |
||||
* Information about the tabs to be rendered. |
||||
*/ |
||||
_tabs: Array<Object>, |
||||
|
||||
/** |
||||
* Invoked to save changed settings. |
||||
*/ |
||||
dispatch: Function, |
||||
}; |
||||
|
||||
/** |
||||
* A React {@code Component} for displaying a dialog to modify local settings |
||||
* and conference-wide (moderator) settings. This version is connected to |
||||
* redux to get the current settings. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class SettingsDialog extends Component<Props> { |
||||
/** |
||||
* Initializes a new {@code ConnectedSettingsDialog} instance. |
||||
* |
||||
* @param {Props} props - The React {@code Component} props to initialize |
||||
* the new {@code ConnectedSettingsDialog} instance with. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._closeDialog = this._closeDialog.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { _tabs, defaultTab, dispatch } = this.props; |
||||
const onSubmit = this._closeDialog; |
||||
const defaultTabIdx |
||||
= _tabs.findIndex(({ name }) => name === defaultTab); |
||||
const tabs = _tabs.map(tab => { |
||||
return { |
||||
...tab, |
||||
submit: (...args) => dispatch(tab.submit(...args)) |
||||
}; |
||||
}); |
||||
|
||||
return ( |
||||
<DialogWithTabs |
||||
closeDialog = { this._closeDialog } |
||||
defaultTab = { |
||||
defaultTabIdx === -1 ? undefined : defaultTabIdx |
||||
} |
||||
onSubmit = { onSubmit } |
||||
tabs = { tabs } /> |
||||
); |
||||
} |
||||
|
||||
_closeDialog: () => void; |
||||
|
||||
/** |
||||
* Callback invoked to close the dialog without saving changes. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_closeDialog() { |
||||
this.props.dispatch(hideDialog()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated props for the |
||||
* {@code ConnectedSettingsDialog} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* tabs: Array<Object> |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || []; |
||||
const jwt = state['features/base/jwt']; |
||||
|
||||
// The settings sections to display.
|
||||
const showDeviceSettings = configuredTabs.includes('devices'); |
||||
const moreTabProps = getMoreTabProps(state); |
||||
const { showModeratorSettings, showLanguageSettings } = moreTabProps; |
||||
const showProfileSettings |
||||
= configuredTabs.includes('profile') && jwt.isGuest; |
||||
|
||||
const tabs = []; |
||||
|
||||
if (showDeviceSettings) { |
||||
tabs.push({ |
||||
name: SETTINGS_TABS.DEVICES, |
||||
component: DeviceSelection, |
||||
label: 'settings.devices', |
||||
props: getDeviceSelectionDialogProps(state), |
||||
styles: 'settings-pane devices-pane', |
||||
submit: submitDeviceSelectionTab |
||||
}); |
||||
} |
||||
|
||||
if (showProfileSettings) { |
||||
tabs.push({ |
||||
name: SETTINGS_TABS.PROFILE, |
||||
component: ProfileTab, |
||||
label: 'profile.title', |
||||
props: getProfileTabProps(state), |
||||
styles: 'settings-pane profile-pane', |
||||
submit: submitProfileTab |
||||
}); |
||||
} |
||||
|
||||
if (showModeratorSettings || showLanguageSettings) { |
||||
tabs.push({ |
||||
name: SETTINGS_TABS.MORE, |
||||
component: MoreTab, |
||||
label: 'settings.more', |
||||
props: moreTabProps, |
||||
styles: 'settings-pane more-pane', |
||||
submit: submitMoreTab |
||||
}); |
||||
} |
||||
|
||||
return { _tabs: tabs }; |
||||
} |
||||
|
||||
export default connect(_mapStateToProps)(SettingsDialog); |
@ -1,111 +0,0 @@ |
||||
import PropTypes from 'prop-types'; |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import { |
||||
getLocalParticipant, |
||||
PARTICIPANT_ROLE |
||||
} from '../../../base/participants'; |
||||
|
||||
import DeviceSelectionButton from './DeviceSelectionButton'; |
||||
import LanguageSelectDropdown from './LanguageSelectDropdown'; |
||||
import ModeratorCheckboxes from './ModeratorCheckboxes'; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which various ways to change application |
||||
* settings. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class SettingsMenu extends Component { |
||||
/** |
||||
* {@code SettingsMenu} component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* Whether or not the local user is a moderator. |
||||
*/ |
||||
_isModerator: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or not the button to open device selection should display. |
||||
*/ |
||||
showDeviceSettings: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or not the dropdown to change the current translated language |
||||
* should display. |
||||
*/ |
||||
showLanguageSettings: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or not moderator-only actions that affect the conference |
||||
* should display. |
||||
*/ |
||||
showModeratorSettings: PropTypes.bool, |
||||
|
||||
/** |
||||
* Whether or not menu section should have section titles displayed. |
||||
*/ |
||||
showTitles: PropTypes.bool, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: PropTypes.func |
||||
}; |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
_isModerator, |
||||
showDeviceSettings, |
||||
showLanguageSettings, |
||||
showModeratorSettings, |
||||
showTitles, |
||||
t |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<div className = 'settings-menu'> |
||||
<div className = 'title'> |
||||
{ t('settings.title') } |
||||
</div> |
||||
{ showLanguageSettings |
||||
? <LanguageSelectDropdown /> |
||||
: null } |
||||
{ showDeviceSettings |
||||
? <DeviceSelectionButton showTitle = { showTitles } /> |
||||
: null } |
||||
{ _isModerator && showModeratorSettings |
||||
? <ModeratorCheckboxes showTitle = { showTitles } /> |
||||
: null } |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps parts of Redux store to component prop types. |
||||
* |
||||
* @param {Object} state - Snapshot of Redux store. |
||||
* @returns {{ |
||||
* _isModerator: boolean |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
return { |
||||
_isModerator: |
||||
getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(SettingsMenu)); |
@ -1,2 +1,2 @@ |
||||
export { default as SettingsButton } from './SettingsButton'; |
||||
export { default as SettingsMenu } from './SettingsMenu'; |
||||
export { default as SettingsDialog } from './SettingsDialog'; |
||||
|
@ -0,0 +1,5 @@ |
||||
export const SETTINGS_TABS = { |
||||
DEVICES: 'devices_tab', |
||||
MORE: 'more_tab', |
||||
PROFILE: 'profile_tab' |
||||
}; |
Loading…
Reference in new issue