diff --git a/lang/main.json b/lang/main.json index b90f0af8a..72a84c68b 100644 --- a/lang/main.json +++ b/lang/main.json @@ -226,7 +226,7 @@ "liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active", "liveStreamingDisabledTooltip": "Start live stream disabled.", "lockMessage": "Failed to lock the conference.", - "lockRoom": "Add meeting $t(lockRoomPasswordUppercase)", + "lockRoom": "Add meeting $t(lockRoomPassword)", "lockTitle": "Lock failed", "logoutQuestion": "Are you sure you want to logout and stop the conference?", "login": "Login", @@ -525,7 +525,7 @@ "oldElectronClientDescription2": "latest build", "oldElectronClientDescription3": " now!" }, - "passwordSetRemotely": "set by another participant", + "passwordSetRemotely": "Set by another participant", "passwordDigitsOnly": "Up to {{number}} digits", "poweredby": "powered by", "prejoin": { diff --git a/modules/API/API.js b/modules/API/API.js index cc27775c1..a72a80db4 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -38,7 +38,7 @@ import { resizeLargeVideo, selectParticipantInLargeVideo } from '../../react/features/large-video/actions'; -import { toggleLobbyMode } from '../../react/features/lobby/actions.web'; +import { toggleLobbyMode } from '../../react/features/lobby/actions'; import { RECORDING_TYPES } from '../../react/features/recording/constants'; import { getActiveSession } from '../../react/features/recording/functions'; import { toggleTileView, setTileView } from '../../react/features/video-layout'; diff --git a/react/features/base/dialog/components/native/styles.js b/react/features/base/dialog/components/native/styles.js index a356b356d..fd119d640 100644 --- a/react/features/base/dialog/components/native/styles.js +++ b/react/features/base/dialog/components/native/styles.js @@ -116,6 +116,24 @@ const brandedDialogText = { textAlign: 'center' }; +const brandedDialogLabelStyle = { + color: schemeColor('text'), + flexShrink: 1, + fontSize: MD_FONT_SIZE, + opacity: 0.90 +}; + +const brandedDialogItemContainerStyle = { + alignItems: 'center', + flexDirection: 'row', + height: MD_ITEM_HEIGHT +}; + +const brandedDialogIconStyle = { + color: schemeColor('icon'), + fontSize: 24 +}; + export const inputDialog = { bottomField: { marginBottom: 0 @@ -145,28 +163,22 @@ ColorSchemeRegistry.register('BottomSheet', { * Style for the {@code Icon} element in a generic item of the menu. */ iconStyle: { - color: schemeColor('icon'), - fontSize: 24 + ...brandedDialogIconStyle }, /** * Style for the label in a generic item rendered in the menu. */ labelStyle: { - color: schemeColor('text'), - flexShrink: 1, - fontSize: MD_FONT_SIZE, - marginLeft: 32, - opacity: 0.90 + ...brandedDialogLabelStyle, + marginLeft: 32 }, /** * Container style for a generic item rendered in the menu. */ style: { - alignItems: 'center', - flexDirection: 'row', - height: MD_ITEM_HEIGHT + ...brandedDialogItemContainerStyle }, /** @@ -258,3 +270,28 @@ ColorSchemeRegistry.register('Dialog', { borderTopWidth: 1 } }); + +ColorSchemeRegistry.register('SecurityDialog', { + /** + * Field on an input dialog. + */ + field: { + borderBottomWidth: 1, + borderColor: schemeColor('border'), + color: schemeColor('text'), + fontSize: 14, + paddingBottom: 8 + }, + + text: { + color: schemeColor('text'), + fontSize: 14, + marginTop: 8 + }, + + title: { + color: schemeColor('text'), + fontSize: 18, + fontWeight: 'bold' + } +}); diff --git a/react/features/lobby/actions.any.js b/react/features/lobby/actions.any.js new file mode 100644 index 000000000..75f6f5b89 --- /dev/null +++ b/react/features/lobby/actions.any.js @@ -0,0 +1,25 @@ +// @flow + +import { type Dispatch } from 'redux'; + +import { + getCurrentConference +} from '../base/conference'; + +/** + * Action to toggle lobby mode on or off. + * + * @param {boolean} enabled - The desired (new) state of the lobby mode. + * @returns {Function} + */ +export function toggleLobbyMode(enabled: boolean) { + return async (dispatch: Dispatch, getState: Function) => { + const conference = getCurrentConference(getState); + + if (enabled) { + conference.enableLobby(); + } else { + conference.disableLobby(); + } + }; +} diff --git a/react/features/lobby/actions.web.js b/react/features/lobby/actions.web.js index fa7a3874a..f312156ac 100644 --- a/react/features/lobby/actions.web.js +++ b/react/features/lobby/actions.web.js @@ -11,6 +11,7 @@ import { } from '../base/conference'; import { hideDialog, openDialog } from '../base/dialog'; import { getLocalParticipant } from '../base/participants'; +export * from './actions.any'; import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED, @@ -198,20 +199,3 @@ export function startKnocking() { }; } -/** - * Action to toggle lobby mode on or off. - * - * @param {boolean} enabled - The desired (new) state of the lobby mode. - * @returns {Function} - */ -export function toggleLobbyMode(enabled: boolean) { - return async (dispatch: Dispatch, getState: Function) => { - const conference = getCurrentConference(getState); - - if (enabled) { - conference.enableLobby(); - } else { - conference.disableLobby(); - } - }; -} diff --git a/react/features/lobby/components/native/LobbyModeButton.js b/react/features/lobby/components/native/LobbyModeButton.js deleted file mode 100644 index 97018d2ea..000000000 --- a/react/features/lobby/components/native/LobbyModeButton.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow - -import { getCurrentConference } from '../../../base/conference'; -import { translate } from '../../../base/i18n'; -import { IconMeetingUnlocked, IconMeetingLocked } from '../../../base/icons'; -import { isLocalParticipantModerator } from '../../../base/participants'; -import { connect } from '../../../base/redux'; -import AbstractButton, { type Props as AbstractProps } from '../../../base/toolbox/components/AbstractButton'; -import { showDisableLobbyModeDialog, showEnableLobbyModeDialog } from '../../actions.native'; - -type Props = AbstractProps & { - - /** - * The Redux Dispatch function. - */ - dispatch: Function, - - /** - * True if the lobby mode is currently enabled for this conference. - */ - lobbyEnabled: boolean -}; - -/** - * Component to render the lobby mode initiator button. - */ -class LobbyModeButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.lobbyButton'; - icon = IconMeetingUnlocked; - label = 'toolbar.lobbyButtonEnable'; - toggledLabel = 'toolbar.lobbyButtonDisable' - toggledIcon = IconMeetingLocked; - - /** - * Callback for the click event of the button. - * - * @returns {void} - */ - _handleClick() { - const { dispatch } = this.props; - - if (this._isToggled()) { - dispatch(showDisableLobbyModeDialog()); - } else { - dispatch(showEnableLobbyModeDialog()); - } - } - - /** - * Function to define the button state. - * - * @returns {boolean} - */ - _isToggled() { - return this.props.lobbyEnabled; - } -} - -/** - * Maps part of the Redux store to the props of this component. - * - * @param {Object} state - The Redux state. - * @param {Props} ownProps - The own props of the component. - * @returns {Props} - */ -export function _mapStateToProps(state: Object): $Shape { - const conference = getCurrentConference(state); - const { lobbyEnabled } = state['features/lobby']; - const { hideLobbyButton } = state['features/base/config']; - const lobbySupported = conference && conference.isLobbySupported(); - - return { - lobbyEnabled, - visible: lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton - }; -} - -export default translate(connect(_mapStateToProps)(LobbyModeButton)); diff --git a/react/features/lobby/components/native/LobbyModeSwitch.js b/react/features/lobby/components/native/LobbyModeSwitch.js new file mode 100644 index 000000000..b59517564 --- /dev/null +++ b/react/features/lobby/components/native/LobbyModeSwitch.js @@ -0,0 +1,58 @@ +// @flow + +import React from 'react'; +import { Switch, View } from 'react-native'; + +import { translate } from '../../../base/i18n'; +import { connect } from '../../../base/redux'; + +import styles, { + ENABLED_THUMB_COLOR, + ENABLED_TRACK_COLOR, + DISABLED_THUMB_COLOR +} from './styles'; + +/** + * The type of the React {@code Component} props of {@link LobbyModeSwitch}. + */ +type Props = { + + /** + * True if the lobby mode is currently enabled for this conference. + */ + lobbyEnabled: boolean, + + /** + * Callback to be invoked when handling enable-disable lobby mode switch. + */ + onToggleLobbyMode: Function +}; + +/** + * Component meant to Enable/Disable lobby mode. + * + * @returns {React$Element} + */ +function LobbyModeSwitch( + { + lobbyEnabled, + onToggleLobbyMode + }: Props) { + + return ( + + + + ); +} + +export default translate(connect()(LobbyModeSwitch)); diff --git a/react/features/lobby/components/native/index.js b/react/features/lobby/components/native/index.js index 52555b4bf..af5b7f633 100644 --- a/react/features/lobby/components/native/index.js +++ b/react/features/lobby/components/native/index.js @@ -3,5 +3,4 @@ export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog'; export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog'; export { default as KnockingParticipantList } from './KnockingParticipantList'; -export { default as LobbyModeButton } from './LobbyModeButton'; export { default as LobbyScreen } from './LobbyScreen'; diff --git a/react/features/lobby/components/native/styles.js b/react/features/lobby/components/native/styles.js index fff9daea8..18845509a 100644 --- a/react/features/lobby/components/native/styles.js +++ b/react/features/lobby/components/native/styles.js @@ -4,6 +4,10 @@ import { ColorPalette } from '../../../base/styles'; const SECONDARY_COLOR = '#B8C7E0'; +export const ENABLED_THUMB_COLOR = ColorPalette.blueHighlight; +export const ENABLED_TRACK_COLOR = ColorPalette.blue; +export const DISABLED_THUMB_COLOR = ColorPalette.darkGrey; + export default { button: { alignItems: 'center', @@ -138,5 +142,14 @@ export default { knockingParticipantListText: { color: 'white' + }, + + lobbySwitchContainer: { + flexDirection: 'column', + marginTop: 16 + }, + + lobbySwitchIcon: { + alignSelf: 'flex-end' } }; diff --git a/react/features/room-lock/actions.js b/react/features/room-lock/actions.js index d6f0a37f9..a734d34d4 100644 --- a/react/features/room-lock/actions.js +++ b/react/features/room-lock/actions.js @@ -12,34 +12,12 @@ import { setPassword } from '../base/conference'; import { hideDialog, openDialog } from '../base/dialog'; +import { SecurityDialog } from '../security/components/security-dialog'; -import { PasswordRequiredPrompt, RoomLockPrompt } from './components'; +import { PasswordRequiredPrompt } from './components'; declare var APP: Object; -/** - * Begins a (user) request to lock a specific conference/room. - * - * @param {JitsiConference|undefined} conference - The JitsiConference to lock - * if specified or undefined if the current JitsiConference is to be locked. - * @returns {Function} - */ -export function beginRoomLockRequest(conference: ?Object) { - return (dispatch: Function, getState: Function) => { - if (typeof conference === 'undefined') { - // eslint-disable-next-line no-param-reassign - conference = getState()['features/base/conference'].conference; - } - if (conference) { - const passwordNumberOfDigits = getState()['features/base/config'].roomPasswordNumberOfDigits; - - dispatch(openDialog(RoomLockPrompt, { - conference, - passwordNumberOfDigits })); - } - }; -} - /** * Cancels a prompt for a password to join a specific conference/room. * @@ -99,7 +77,7 @@ export function endRoomLockRequest( = password ? dispatch(setPassword(conference, conference.lock, password)) : Promise.resolve(); - const endRoomLockRequest_ = () => dispatch(hideDialog(RoomLockPrompt)); + const endRoomLockRequest_ = () => dispatch(hideDialog(SecurityDialog)); setPassword_.then(endRoomLockRequest_, endRoomLockRequest_); }; @@ -137,3 +115,5 @@ export function unlockRoom() { )); }; } + + diff --git a/react/features/room-lock/components/RoomLockButton.js b/react/features/room-lock/components/RoomLockButton.js deleted file mode 100644 index fdae565c9..000000000 --- a/react/features/room-lock/components/RoomLockButton.js +++ /dev/null @@ -1,101 +0,0 @@ -// @flow - -import { MEETING_PASSWORD_ENABLED, getFeatureFlag } from '../../base/flags'; -import { translate } from '../../base/i18n'; -import { IconRoomLock, IconRoomUnlock } from '../../base/icons'; -import { isLocalParticipantModerator } from '../../base/participants'; -import { connect } from '../../base/redux'; -import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; -import { beginRoomLockRequest, unlockRoom } from '../actions'; - -type Props = AbstractButtonProps & { - - /** - * Whether the current local participant is a moderator, therefore is - * allowed to lock or unlock the conference. - */ - _localParticipantModerator: boolean, - - /** - * Whether the current conference is locked or not. - */ - _locked: boolean, - - /** - * The redux {@code dispatch} function. - */ - dispatch: Function -}; - -/** - * An implementation of a button for locking / unlocking a room. - */ -class RoomLockButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.lockRoom'; - icon = IconRoomLock; - label = 'dialog.lockRoom'; - toggledIcon = IconRoomUnlock; - toggledLabel = 'dialog.unlockRoom'; - - /** - * Handles clicking / pressing the button. - * - * @override - * @protected - * @returns {void} - */ - _handleClick() { - const { dispatch, _locked } = this.props; - - if (_locked) { - dispatch(unlockRoom()); - } else { - dispatch(beginRoomLockRequest()); - } - } - - /** - * Indicates whether this button is disabled or not. - * - * @override - * @protected - * @returns {boolean} - */ - _isDisabled() { - return !this.props._localParticipantModerator; - } - - /** - * Indicates whether this button is in toggled state or not. - * - * @override - * @protected - * @returns {boolean} - */ - _isToggled() { - return this.props._locked; - } -} - -/** - * Maps (parts of) the redux state to the associated props for the - * {@code RoomLockButton} component. - * - * @param {Object} state - The Redux state. - * @param {Object} ownProps - The properties explicitly passed to the component instance. - * @private - * @returns {Props} - */ -function _mapStateToProps(state, ownProps): Object { - const { conference, locked } = state['features/base/conference']; - const enabled = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true); - const { visible = enabled } = ownProps; - - return { - _localParticipantModerator: Boolean(conference && isLocalParticipantModerator(state)), - _locked: Boolean(conference && locked), - visible - }; -} - -export default translate(connect(_mapStateToProps)(RoomLockButton)); diff --git a/react/features/room-lock/components/RoomLockPrompt.native.js b/react/features/room-lock/components/RoomLockPrompt.native.js deleted file mode 100644 index 747b7eaee..000000000 --- a/react/features/room-lock/components/RoomLockPrompt.native.js +++ /dev/null @@ -1,140 +0,0 @@ -// @flow - -import React, { Component } from 'react'; -import type { Dispatch } from 'redux'; - -import { InputDialog } from '../../base/dialog'; -import { connect } from '../../base/redux'; -import { endRoomLockRequest } from '../actions'; - -/** - * The style of the {@link TextInput} rendered by {@code RoomLockPrompt}. As it - * requests the entry of a password, {@code TextInput} automatically correcting - * the entry of the password is a pain to deal with as a user. - */ -const _TEXT_INPUT_PROPS = { - autoCapitalize: 'none', - autoCorrect: false -}; - -/** - * The type of the React {@code Component} props of {@link RoomLockPrompt}. - */ -type Props = { - - /** - * The JitsiConference which requires a password. - */ - conference: Object, - - /** - * The number of digits to be used in the password. - */ - passwordNumberOfDigits: ?number, - - /** - * Redux store dispatch function. - */ - dispatch: Dispatch -}; - -/** - * Implements a React Component which prompts the user for a password to lock a - * conference/room. - */ -class RoomLockPrompt extends Component { - /** - * Initializes a new RoomLockPrompt instance. - * - * @param {Props} props - The read-only properties with which the new - * instance is to be initialized. - */ - constructor(props: Props) { - super(props); - - // Bind event handlers so they are only bound once per instance. - this._onCancel = this._onCancel.bind(this); - this._onSubmit = this._onSubmit.bind(this); - this._validateInput = this._validateInput.bind(this); - } - - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - let textInputProps = _TEXT_INPUT_PROPS; - - if (this.props.passwordNumberOfDigits) { - textInputProps = { - ...textInputProps, - keyboardType: 'number-pad', - maxLength: this.props.passwordNumberOfDigits - }; - } - - return ( - - ); - } - - _onCancel: () => boolean; - - /** - * Notifies this prompt that it has been dismissed by cancel. - * - * @private - * @returns {boolean} True to hide this dialog/prompt; otherwise, false. - */ - _onCancel() { - // An undefined password is understood to cancel the request to lock the - // conference/room. - return this._onSubmit(undefined); - } - - _onSubmit: (?string) => boolean; - - /** - * Notifies this prompt that it has been dismissed by submitting a specific - * value. - * - * @param {string|undefined} value - The submitted value. - * @private - * @returns {boolean} False because we do not want to hide this - * dialog/prompt as the hiding will be handled inside endRoomLockRequest - * after setting the password is resolved. - */ - _onSubmit(value: ?string) { - this.props.dispatch(endRoomLockRequest(this.props.conference, value)); - - return false; // Do not hide. - } - - _validateInput: (string) => boolean; - - /** - * Verifies input in case only digits are required. - * - * @param {string|undefined} value - The submitted value. - * @private - * @returns {boolean} False when the value is not valid and True otherwise. - */ - _validateInput(value: string) { - - // we want only digits, but both number-pad and numeric add ',' and '.' as symbols - if (this.props.passwordNumberOfDigits && value.length > 0 && !/^\d+$/.test(value)) { - return false; - } - - return true; - } -} - -export default connect()(RoomLockPrompt); diff --git a/react/features/room-lock/components/RoomLockPrompt.web.js b/react/features/room-lock/components/RoomLockPrompt.web.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/room-lock/components/RoomLockSwitch.js b/react/features/room-lock/components/RoomLockSwitch.js new file mode 100644 index 000000000..746522c23 --- /dev/null +++ b/react/features/room-lock/components/RoomLockSwitch.js @@ -0,0 +1,83 @@ +// @flow + +import React from 'react'; +import { Switch, Text, View } from 'react-native'; + +import { translate } from '../../base/i18n'; +import { connect } from '../../base/redux'; +import { LOCKED_REMOTELY } from '../constants'; + +import styles, { + DISABLED_THUMB_COLOR, + ENABLED_THUMB_COLOR, ENABLED_TRACK_COLOR +} from './styles'; + +/** + * The type of the React {@code Component} props of {@link RoomLockSwitch}. + */ +type Props = { + + /** + * Checks if the room is locked based on defined room lock constants. + */ + locked: string, + + /** + * Whether the switch is disabled. + */ + disabled: boolean, + + /** + * Callback to be invoked when the user toggles room lock. + */ + onToggleRoomLock: Function, + + /** + * Control for room lock. + */ + toggleRoomLock: boolean, + + /** + * Invoked to obtain translated strings. + */ + t: Function +}; + +/** + * Component meant to Add/Remove meeting password. + * + * @returns {React$Element} + */ +function RoomLockSwitch( + { + locked, + disabled, + onToggleRoomLock, + toggleRoomLock, + t + }: Props) { + + return ( + + + { + locked === LOCKED_REMOTELY + && t('passwordSetRemotely') + } + + + + ); +} + + +export default translate(connect()(RoomLockSwitch)); diff --git a/react/features/room-lock/components/index.js b/react/features/room-lock/components/index.js index 5c2f308c2..88f804a97 100644 --- a/react/features/room-lock/components/index.js +++ b/react/features/room-lock/components/index.js @@ -1,3 +1 @@ export { default as PasswordRequiredPrompt } from './PasswordRequiredPrompt'; -export { default as RoomLockButton } from './RoomLockButton'; -export { default as RoomLockPrompt } from './RoomLockPrompt'; diff --git a/react/features/room-lock/components/styles.js b/react/features/room-lock/components/styles.js new file mode 100644 index 000000000..701da7703 --- /dev/null +++ b/react/features/room-lock/components/styles.js @@ -0,0 +1,16 @@ +// @flow + +import { ColorPalette } from '../../base/styles'; + +export const ENABLED_THUMB_COLOR = ColorPalette.blueHighlight; +export const ENABLED_TRACK_COLOR = ColorPalette.blue; +export const DISABLED_THUMB_COLOR = ColorPalette.darkGrey; + +export default { + roomLockSwitchContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 16 + } +}; diff --git a/react/features/room-lock/middleware.js b/react/features/room-lock/middleware.js index d9c42ea06..b347b4d90 100644 --- a/react/features/room-lock/middleware.js +++ b/react/features/room-lock/middleware.js @@ -16,7 +16,7 @@ import { } from '../notifications'; import { _openPasswordRequiredPrompt } from './actions'; -import { PasswordRequiredPrompt, RoomLockPrompt } from './components'; +import { PasswordRequiredPrompt } from './components'; import { LOCKED_REMOTELY } from './constants'; import logger from './logger'; @@ -85,7 +85,6 @@ MiddlewareRegistry.register(store => next => action => { */ function _conferenceJoined({ dispatch }, next, action) { dispatch(hideDialog(PasswordRequiredPrompt)); - dispatch(hideDialog(RoomLockPrompt)); return next(action); } @@ -116,7 +115,6 @@ function _conferenceFailed({ dispatch }, next, action) { } } else { dispatch(hideDialog(PasswordRequiredPrompt)); - dispatch(hideDialog(RoomLockPrompt)); } return next(action); diff --git a/react/features/security/components/security-dialog/SecurityDialogButton.js b/react/features/security/components/security-dialog/SecurityDialogButton.js index 251ed21e4..a7da1520a 100644 --- a/react/features/security/components/security-dialog/SecurityDialogButton.js +++ b/react/features/security/components/security-dialog/SecurityDialogButton.js @@ -1,5 +1,7 @@ // @flow +import type { Dispatch } from 'redux'; + import { createToolbarEvent, sendAnalytics } from '../../../analytics'; import { translate } from '../../../base/i18n'; import { IconSecurityOff, IconSecurityOn } from '../../../base/icons'; @@ -16,10 +18,9 @@ type Props = AbstractButtonProps & { _locked: boolean, /** - * On click handler that opens the security dialog. + * The redux {@code dispatch} function. */ - onClick: Function - + dispatch: Dispatch }; @@ -40,7 +41,7 @@ class SecurityDialogButton extends AbstractButton { */ _handleClick() { sendAnalytics(createToolbarEvent('toggle.security', { enable: !this.props._locked })); - this.props.onClick(); + this.props.dispatch(toggleSecurityDialog()); } /** @@ -69,14 +70,4 @@ function mapStateToProps(state: Object) { }; } -/** - * Maps dispatching of some action to React component props. - * - * @param {Function} dispatch - Redux action dispatcher. - * @returns {Props} - */ -const mapDispatchToProps = { - onClick: () => toggleSecurityDialog() -}; - -export default translate(connect(mapStateToProps, mapDispatchToProps)(SecurityDialogButton)); +export default translate(connect(mapStateToProps)(SecurityDialogButton)); diff --git a/react/features/security/components/security-dialog/_.native.js b/react/features/security/components/security-dialog/_.native.js new file mode 100644 index 000000000..738c4d2b8 --- /dev/null +++ b/react/features/security/components/security-dialog/_.native.js @@ -0,0 +1 @@ +export * from './native'; diff --git a/react/features/security/components/security-dialog/_.web.js b/react/features/security/components/security-dialog/_.web.js new file mode 100644 index 000000000..b80c83af3 --- /dev/null +++ b/react/features/security/components/security-dialog/_.web.js @@ -0,0 +1 @@ +export * from './web'; diff --git a/react/features/security/components/security-dialog/index.js b/react/features/security/components/security-dialog/index.js index d67ddb726..cda61441e 100644 --- a/react/features/security/components/security-dialog/index.js +++ b/react/features/security/components/security-dialog/index.js @@ -1,4 +1 @@ -// @flow - -export { default as SecurityDialog } from './SecurityDialog'; -export { default as SecurityDialogButton } from './SecurityDialogButton'; +export * from './_'; diff --git a/react/features/security/components/security-dialog/native/SecurityDialog.js b/react/features/security/components/security-dialog/native/SecurityDialog.js new file mode 100644 index 000000000..5069f5ef0 --- /dev/null +++ b/react/features/security/components/security-dialog/native/SecurityDialog.js @@ -0,0 +1,444 @@ +// @flow + +import React, { PureComponent } from 'react'; +import { + KeyboardAvoidingView, + Platform, + Text, + TextInput, + View +} from 'react-native'; +import { connect } from 'react-redux'; +import type { Dispatch } from 'redux'; + +import { ColorSchemeRegistry } from '../../../../base/color-scheme'; +import { + FIELD_UNDERLINE, + CustomSubmitDialog +} from '../../../../base/dialog'; +import { getFeatureFlag, MEETING_PASSWORD_ENABLED } from '../../../../base/flags'; +import { translate } from '../../../../base/i18n'; +import { isLocalParticipantModerator } from '../../../../base/participants'; +import { StyleType } from '../../../../base/styles'; +import { toggleLobbyMode } from '../../../../lobby/actions.any'; +import LobbyModeSwitch + from '../../../../lobby/components/native/LobbyModeSwitch'; +import { LOCKED_LOCALLY } from '../../../../room-lock'; +import { + endRoomLockRequest, + unlockRoom +} from '../../../../room-lock/actions'; +import RoomLockSwitch from '../../../../room-lock/components/RoomLockSwitch'; + +/** + * The style of the {@link TextInput} rendered by {@code SecurityDialog}. As it + * requests the entry of a password, {@code TextInput} automatically correcting + * the entry of the password is a pain to deal with as a user. + */ +const _TEXT_INPUT_PROPS = { + autoCapitalize: 'none', + autoCorrect: false +}; + +/** + * The type of the React {@code Component} props of {@link SecurityDialog}. + */ +type Props = { + + /** + * The JitsiConference which requires a password. + */ + _conference: Object, + + /** + * The color-schemed stylesheet of the feature. + */ + _dialogStyles: StyleType, + + /** + * Whether the local user is the moderator. + */ + _isModerator: boolean, + + /** + * State of the lobby mode. + */ + _lobbyEnabled: boolean, + + /** + * Whether the lobby mode switch is available or not. + */ + _lobbyModeSwitchVisible: boolean, + + /** + * The value for how the conference is locked (or undefined if not locked) + * as defined by room-lock constants. + */ + _locked: string, + + /** + * Checks if the conference room is locked or not. + */ + _lockedConference: boolean, + + /** + * The current known password for the JitsiConference. + */ + _password: string, + + /** + * Number of digits used in the room-lock password. + */ + _passwordNumberOfDigits: number, + + /** + * Whether the room lock switch is available or not. + */ + _roomLockSwitchVisible: boolean, + + /** + * The color-schemed stylesheet of the security dialog feature. + */ + _securityDialogStyles: StyleType, + + /** + * Redux store dispatch function. + */ + dispatch: Dispatch, + + /** + * Invoked to obtain translated strings. + */ + t: Function +}; + +/** + * The type of the React {@code Component} state of {@link SecurityDialog}. + */ +type State = { + + /** + * Password added by the participant for room lock. + */ + passwordInputValue: string, + + /** + * Shows an input or a message. + */ + showElement: boolean +}; + +/** + * Component that renders the security options dialog. + * + * @returns {React$Element} + */ +class SecurityDialog extends PureComponent { + + /** + * Instantiates a new {@code SecurityDialog}. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this.state = { + passwordInputValue: '', + showElement: props._locked === LOCKED_LOCALLY || false + }; + + this._onChangeText = this._onChangeText.bind(this); + this._onSubmit = this._onSubmit.bind(this); + this._onToggleLobbyMode = this._onToggleLobbyMode.bind(this); + this._onToggleRoomLock = this._onToggleRoomLock.bind(this); + } + + /** + * Implements {@code SecurityDialog.render}. + * + * @inheritdoc + */ + render() { + return ( + + + { this._renderLobbyMode() } + { this._renderRoomLock() } + + + ); + } + + /** + * Renders lobby mode. + * + * @returns {ReactElement} + * @private + */ + _renderLobbyMode() { + const { + _lobbyEnabled, + _lobbyModeSwitchVisible, + _securityDialogStyles, + t + } = this.props; + + if (!_lobbyModeSwitchVisible) { + return null; + } + + return ( + + + { t('lobby.dialogTitle') } + + + { t('lobby.enableDialogText') } + + + + ); + } + + /** + * Renders room lock. + * + * @returns {ReactElement} + * @private + */ + _renderRoomLock() { + const { + _isModerator, + _locked, + _lockedConference, + _roomLockSwitchVisible, + _securityDialogStyles, + t + } = this.props; + const { showElement } = this.state; + + if (!_roomLockSwitchVisible) { + return null; + } + + return ( + + + { t('dialog.lockRoom') } + + + { t('security.about') } + + + { this._renderRoomLockMessage() } + + ); + } + + /** + * Renders room lock text input/message. + * + * @returns {ReactElement} + * @private + */ + _renderRoomLockMessage() { + let textInputProps = _TEXT_INPUT_PROPS; + const { + _isModerator, + _locked, + _password, + _passwordNumberOfDigits, + _securityDialogStyles, + t + } = this.props; + const { passwordInputValue, showElement } = this.state; + + if (_passwordNumberOfDigits) { + textInputProps = { + ...textInputProps, + keyboardType: 'numeric', + maxLength: _passwordNumberOfDigits + }; + } + + if (!_isModerator) { + return null; + } + + if (showElement) { + if (typeof _locked === 'undefined') { + return ( + + ); + } else if (_locked) { + if (_locked === LOCKED_LOCALLY && typeof _password !== 'undefined') { + return ( + + ); + } + } + } + } + + _onToggleLobbyMode: () => void; + + /** + * Handles the enable-disable lobby mode switch. + * + * @private + * @returns {void} + */ + _onToggleLobbyMode() { + const { _lobbyEnabled, dispatch } = this.props; + + if (_lobbyEnabled) { + dispatch(toggleLobbyMode(false)); + } else { + dispatch(toggleLobbyMode(true)); + } + } + + _onToggleRoomLock: () => void; + + /** + * Callback to be invoked when room lock button is pressed. + * + * @returns {void} + */ + _onToggleRoomLock() { + const { _isModerator, _locked, dispatch } = this.props; + const { showElement } = this.state; + + this.setState({ + showElement: !showElement + }); + + if (_locked && _isModerator) { + dispatch(unlockRoom()); + + this.setState({ + showElement: false + }); + } + } + + /** + * Verifies input in case only digits are required. + * + * @param {string} passwordInputValue - The value of the password + * text input. + * @private + * @returns {boolean} False when the value is not valid and True otherwise. + */ + _validateInputValue(passwordInputValue: string) { + const { _passwordNumberOfDigits } = this.props; + + // we want only digits, + // but both number-pad and numeric add ',' and '.' as symbols + if (_passwordNumberOfDigits + && passwordInputValue.length > 0 + && !/^\d+$/.test(passwordInputValue)) { + return false; + } + + return true; + } + + _onChangeText: string => void; + + /** + * Callback to be invoked when the text in the field changes. + * + * @param {string} passwordInputValue - The value of password input. + * @returns {void} + */ + _onChangeText(passwordInputValue) { + if (!this._validateInputValue(passwordInputValue)) { + return; + } + + this.setState({ + passwordInputValue + }); + } + + _onSubmit: () => boolean; + + /** + * Submits value typed in text input. + * + * @returns {boolean} False because we do not want to hide this + * dialog/prompt as the hiding will be handled inside endRoomLockRequest + * after setting the password is resolved. + */ + _onSubmit() { + const { + _conference, + dispatch + } = this.props; + const { passwordInputValue } = this.state; + + dispatch(endRoomLockRequest(_conference, passwordInputValue)); + + return false; + } +} + +/** + * Maps part of the Redux state to the props of this component. + * + * @param {Object} state - The Redux state. + * @returns {Props} + */ +function _mapStateToProps(state: Object): Object { + const { conference, locked, password } = state['features/base/conference']; + const { hideLobbyButton } = state['features/base/config']; + const { lobbyEnabled } = state['features/lobby']; + const { roomPasswordNumberOfDigits } = state['features/base/config']; + const lobbySupported = conference && conference.isLobbySupported(); + const visible = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true); + + return { + _conference: conference, + _dialogStyles: ColorSchemeRegistry.get(state, 'Dialog'), + _isModerator: isLocalParticipantModerator(state), + _lobbyEnabled: lobbyEnabled, + _lobbyModeSwitchVisible: + lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton, + _locked: locked, + _lockedConference: Boolean(conference && locked), + _password: password, + _passwordNumberOfDigits: roomPasswordNumberOfDigits, + _roomLockSwitchVisible: visible, + _securityDialogStyles: ColorSchemeRegistry.get(state, 'SecurityDialog') + }; +} + + +export default translate(connect(_mapStateToProps)(SecurityDialog)); diff --git a/react/features/security/components/security-dialog/native/index.js b/react/features/security/components/security-dialog/native/index.js new file mode 100644 index 000000000..b47a0d2f3 --- /dev/null +++ b/react/features/security/components/security-dialog/native/index.js @@ -0,0 +1,4 @@ +// @flow + +export { default as SecurityDialog } from './SecurityDialog'; + diff --git a/react/features/security/components/security-dialog/PasswordForm.js b/react/features/security/components/security-dialog/web/PasswordForm.js similarity index 98% rename from react/features/security/components/security-dialog/PasswordForm.js rename to react/features/security/components/security-dialog/web/PasswordForm.js index 7002e8c86..394d163ff 100644 --- a/react/features/security/components/security-dialog/PasswordForm.js +++ b/react/features/security/components/security-dialog/web/PasswordForm.js @@ -2,8 +2,8 @@ import React, { Component } from 'react'; -import { translate } from '../../../base/i18n'; -import { LOCKED_LOCALLY } from '../../../room-lock'; +import { translate } from '../../../../base/i18n'; +import { LOCKED_LOCALLY } from '../../../../room-lock'; /** * The type of the React {@code Component} props of {@link PasswordForm}. diff --git a/react/features/security/components/security-dialog/PasswordSection.js b/react/features/security/components/security-dialog/web/PasswordSection.js similarity index 98% rename from react/features/security/components/security-dialog/PasswordSection.js rename to react/features/security/components/security-dialog/web/PasswordSection.js index 9d182aa14..af7e50b50 100644 --- a/react/features/security/components/security-dialog/PasswordSection.js +++ b/react/features/security/components/security-dialog/web/PasswordSection.js @@ -3,8 +3,8 @@ import React, { useRef } from 'react'; -import { translate } from '../../../base/i18n'; -import { copyText } from '../../../base/util'; +import { translate } from '../../../../base/i18n'; +import { copyText } from '../../../../base/util'; import PasswordForm from './PasswordForm'; diff --git a/react/features/security/components/security-dialog/SecurityDialog.js b/react/features/security/components/security-dialog/web/SecurityDialog.js similarity index 89% rename from react/features/security/components/security-dialog/SecurityDialog.js rename to react/features/security/components/security-dialog/web/SecurityDialog.js index 30943f6c7..1e9dab7dc 100644 --- a/react/features/security/components/security-dialog/SecurityDialog.js +++ b/react/features/security/components/security-dialog/web/SecurityDialog.js @@ -2,13 +2,13 @@ import React, { useState, useEffect } from 'react'; -import { setPassword as setPass } from '../../../base/conference'; -import { Dialog } from '../../../base/dialog'; -import { translate } from '../../../base/i18n'; -import { isLocalParticipantModerator } from '../../../base/participants'; -import { connect } from '../../../base/redux'; -import { E2EESection } from '../../../e2ee/components'; -import { LobbySection } from '../../../lobby'; +import { setPassword as setPass } from '../../../../base/conference'; +import { Dialog } from '../../../../base/dialog'; +import { translate } from '../../../../base/i18n'; +import { isLocalParticipantModerator } from '../../../../base/participants'; +import { connect } from '../../../../base/redux'; +import { E2EESection } from '../../../../e2ee/components'; +import { LobbySection } from '../../../../lobby'; import PasswordSection from './PasswordSection'; diff --git a/react/features/security/components/security-dialog/web/index.js b/react/features/security/components/security-dialog/web/index.js new file mode 100644 index 000000000..08ec6fb20 --- /dev/null +++ b/react/features/security/components/security-dialog/web/index.js @@ -0,0 +1,3 @@ +// @flow + +export { default as SecurityDialog } from './SecurityDialog'; diff --git a/react/features/toolbox/components/native/OverflowMenu.js b/react/features/toolbox/components/native/OverflowMenu.js index 9dd1a4134..018cd81bf 100644 --- a/react/features/toolbox/components/native/OverflowMenu.js +++ b/react/features/toolbox/components/native/OverflowMenu.js @@ -11,10 +11,9 @@ import { connect } from '../../../base/redux'; import { StyleType } from '../../../base/styles'; import { SharedDocumentButton } from '../../../etherpad'; import { InviteButton } from '../../../invite'; -import { LobbyModeButton } from '../../../lobby/components/native'; import { AudioRouteButton } from '../../../mobile/audio-mode'; import { LiveStreamButton, RecordButton } from '../../../recording'; -import { RoomLockButton } from '../../../room-lock'; +import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton'; import { SharedVideoButton } from '../../../shared-video/components'; import { ClosedCaptionButton } from '../../../subtitles'; import { TileViewButton } from '../../../video-layout'; @@ -140,7 +139,7 @@ class OverflowMenu extends PureComponent { {!toolbarButtons.has('invite') && } {!toolbarButtons.has('raisehand') && } - + @@ -149,7 +148,6 @@ class OverflowMenu extends PureComponent { - diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index cf3b852b3..0bb6ca1ba 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -47,7 +47,7 @@ import { LiveStreamButton, RecordButton } from '../../../recording'; -import { SecurityDialogButton } from '../../../security'; +import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton'; import { SETTINGS_TABS, SettingsButton,