You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SecurityDialog.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. // @flow
  2. import Clipboard from '@react-native-community/clipboard';
  3. import React, { PureComponent } from 'react';
  4. import {
  5. Text,
  6. TextInput,
  7. View
  8. } from 'react-native';
  9. import { TouchableRipple } from 'react-native-paper';
  10. import type { Dispatch } from 'redux';
  11. import { ColorSchemeRegistry } from '../../../../base/color-scheme';
  12. import { FIELD_UNDERLINE } from '../../../../base/dialog';
  13. import { getFeatureFlag, MEETING_PASSWORD_ENABLED } from '../../../../base/flags';
  14. import { translate } from '../../../../base/i18n';
  15. import { IconClose } from '../../../../base/icons';
  16. import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
  17. import { isLocalParticipantModerator } from '../../../../base/participants';
  18. import { connect } from '../../../../base/redux';
  19. import { StyleType } from '../../../../base/styles';
  20. import BaseTheme from '../../../../base/ui/components/BaseTheme';
  21. import { isInBreakoutRoom } from '../../../../breakout-rooms/functions';
  22. import { toggleLobbyMode } from '../../../../lobby/actions.any';
  23. import LobbyModeSwitch
  24. from '../../../../lobby/components/native/LobbyModeSwitch';
  25. import HeaderNavigationButton
  26. from '../../../../mobile/navigation/components/HeaderNavigationButton';
  27. import { goBack }
  28. from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
  29. import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../../../room-lock';
  30. import {
  31. endRoomLockRequest,
  32. unlockRoom
  33. } from '../../../../room-lock/actions';
  34. import styles from './styles';
  35. /**
  36. * The style of the {@link TextInput} rendered by {@code SecurityDialog}. As it
  37. * requests the entry of a password, {@code TextInput} automatically correcting
  38. * the entry of the password is a pain to deal with as a user.
  39. */
  40. const _TEXT_INPUT_PROPS = {
  41. autoCapitalize: 'none',
  42. autoCorrect: false
  43. };
  44. /**
  45. * The type of the React {@code Component} props of {@link SecurityDialog}.
  46. */
  47. type Props = {
  48. /**
  49. * The JitsiConference which requires a password.
  50. */
  51. _conference: Object,
  52. /**
  53. * The color-schemed stylesheet of the feature.
  54. */
  55. _dialogStyles: StyleType,
  56. /**
  57. * Whether the local user is the moderator.
  58. */
  59. _isModerator: boolean,
  60. /**
  61. * State of the lobby mode.
  62. */
  63. _lobbyEnabled: boolean,
  64. /**
  65. * Whether the lobby mode switch is available or not.
  66. */
  67. _lobbyModeSwitchVisible: boolean,
  68. /**
  69. * The value for how the conference is locked (or undefined if not locked)
  70. * as defined by room-lock constants.
  71. */
  72. _locked: string,
  73. /**
  74. * Checks if the conference room is locked or not.
  75. */
  76. _lockedConference: boolean,
  77. /**
  78. * The current known password for the JitsiConference.
  79. */
  80. _password: string,
  81. /**
  82. * Number of digits used in the room-lock password.
  83. */
  84. _passwordNumberOfDigits: number,
  85. /**
  86. * Whether setting a room password is available or not.
  87. */
  88. _roomPasswordControls: boolean,
  89. /**
  90. * Redux store dispatch function.
  91. */
  92. dispatch: Dispatch<any>,
  93. /**
  94. * Default prop for navigation between screen components(React Navigation).
  95. */
  96. navigation: Object,
  97. /**
  98. * Invoked to obtain translated strings.
  99. */
  100. t: Function
  101. };
  102. /**
  103. * The type of the React {@code Component} state of {@link SecurityDialog}.
  104. */
  105. type State = {
  106. /**
  107. * Password added by the participant for room lock.
  108. */
  109. passwordInputValue: string,
  110. /**
  111. * Shows an input or a message.
  112. */
  113. showElement: boolean
  114. };
  115. /**
  116. * Component that renders the security options dialog.
  117. *
  118. * @returns {React$Element<any>}
  119. */
  120. class SecurityDialog extends PureComponent<Props, State> {
  121. /**
  122. * Instantiates a new {@code SecurityDialog}.
  123. *
  124. * @inheritdoc
  125. */
  126. constructor(props: Props) {
  127. super(props);
  128. this.state = {
  129. passwordInputValue: '',
  130. showElement: props._locked === LOCKED_LOCALLY || false
  131. };
  132. this._onChangeText = this._onChangeText.bind(this);
  133. this._onCancel = this._onCancel.bind(this);
  134. this._onCopy = this._onCopy.bind(this);
  135. this._onSubmit = this._onSubmit.bind(this);
  136. this._onToggleLobbyMode = this._onToggleLobbyMode.bind(this);
  137. this._onAddPassword = this._onAddPassword.bind(this);
  138. }
  139. /**
  140. * Implements React's {@link Component#componentDidMount()}. Invoked
  141. * immediately after this component is mounted.
  142. *
  143. * @inheritdoc
  144. * @returns {void}
  145. */
  146. componentDidMount() {
  147. const { navigation } = this.props;
  148. navigation.setOptions({
  149. headerLeft: () => (
  150. <HeaderNavigationButton
  151. onPress = { goBack }
  152. src = { IconClose }
  153. style = { styles.headerCloseButton } />
  154. )
  155. });
  156. }
  157. /**
  158. * Implements {@code SecurityDialog.render}.
  159. *
  160. * @inheritdoc
  161. */
  162. render() {
  163. return (
  164. <JitsiScreen style = { styles.securityDialogContainer }>
  165. { this._renderLobbyMode() }
  166. { this._renderSetRoomPassword() }
  167. </JitsiScreen>
  168. );
  169. }
  170. /**
  171. * Renders lobby mode.
  172. *
  173. * @returns {ReactElement}
  174. * @private
  175. */
  176. _renderLobbyMode() {
  177. const {
  178. _lobbyEnabled,
  179. _lobbyModeSwitchVisible,
  180. t
  181. } = this.props;
  182. if (!_lobbyModeSwitchVisible) {
  183. return null;
  184. }
  185. return (
  186. <View style = { styles.lobbyModeContainer }>
  187. <View style = { styles.lobbyModeContent } >
  188. <Text>
  189. { t('lobby.enableDialogText') }
  190. </Text>
  191. <View style = { styles.lobbyModeSection }>
  192. <Text style = { styles.lobbyModeLabel } >
  193. { t('lobby.toggleLabel') }
  194. </Text>
  195. <LobbyModeSwitch
  196. lobbyEnabled = { _lobbyEnabled }
  197. onToggleLobbyMode = { this._onToggleLobbyMode } />
  198. </View>
  199. </View>
  200. </View>
  201. );
  202. }
  203. /**
  204. * Renders setting the password.
  205. *
  206. * @returns {ReactElement}
  207. * @private
  208. */
  209. _renderSetRoomPassword() {
  210. const {
  211. _isModerator,
  212. _locked,
  213. _lockedConference,
  214. _password,
  215. _roomPasswordControls,
  216. t
  217. } = this.props;
  218. const { showElement } = this.state;
  219. let setPasswordControls;
  220. if (!_roomPasswordControls) {
  221. return null;
  222. }
  223. if (_locked && showElement) {
  224. setPasswordControls = (
  225. <>
  226. <TouchableRipple
  227. onPress = { this._onCancel }
  228. rippleColor = { BaseTheme.palette.field02 } >
  229. <Text style = { styles.passwordSetupButton }>
  230. { t('dialog.Remove') }
  231. </Text>
  232. </TouchableRipple>
  233. {
  234. _password
  235. && <TouchableRipple
  236. onPress = { this._onCopy }
  237. rippleColor = { BaseTheme.palette.field02 } >
  238. <Text style = { styles.passwordSetupButton }>
  239. { t('dialog.copy') }
  240. </Text>
  241. </TouchableRipple>
  242. }
  243. </>
  244. );
  245. } else if (!_lockedConference && showElement) {
  246. setPasswordControls = (
  247. <>
  248. <TouchableRipple
  249. onPress = { this._onCancel }
  250. rippleColor = { BaseTheme.palette.field02 } >
  251. <Text style = { styles.passwordSetupButton }>
  252. { t('dialog.Cancel') }
  253. </Text>
  254. </TouchableRipple>
  255. <TouchableRipple
  256. onPress = { this._onSubmit }
  257. rippleColor = { BaseTheme.palette.field02 } >
  258. <Text style = { styles.passwordSetupButton }>
  259. { t('dialog.add') }
  260. </Text>
  261. </TouchableRipple>
  262. </>
  263. );
  264. } else if (!_lockedConference && !showElement) {
  265. setPasswordControls = (
  266. <TouchableRipple
  267. disabled = { !_isModerator }
  268. onPress = { this._onAddPassword }
  269. rippleColor = { BaseTheme.palette.field02 } >
  270. <Text style = { styles.passwordSetupButton }>
  271. { t('info.addPassword') }
  272. </Text>
  273. </TouchableRipple>
  274. );
  275. }
  276. if (_locked === LOCKED_REMOTELY) {
  277. if (_isModerator) {
  278. setPasswordControls = (
  279. <View style = { styles.passwordSetRemotelyContainer }>
  280. <Text style = { styles.passwordSetRemotelyText }>
  281. { t('passwordSetRemotely') }
  282. </Text>
  283. <TouchableRipple
  284. onPress = { this._onCancel }
  285. rippleColor = { BaseTheme.palette.field02 } >
  286. <Text style = { styles.passwordSetupButton }>
  287. { t('dialog.Remove') }
  288. </Text>
  289. </TouchableRipple>
  290. </View>
  291. );
  292. } else {
  293. setPasswordControls = (
  294. <View style = { styles.passwordSetRemotelyContainer }>
  295. <Text style = { styles.passwordSetRemotelyTextDisabled }>
  296. { t('passwordSetRemotely') }
  297. </Text>
  298. <TouchableRipple
  299. disabled = { !_isModerator }
  300. onPress = { this._onAddPassword }
  301. rippleColor = { BaseTheme.palette.field02 } >
  302. <Text style = { styles.passwordSetupButton }>
  303. { t('info.addPassword') }
  304. </Text>
  305. </TouchableRipple>
  306. </View>
  307. );
  308. }
  309. }
  310. return (
  311. <View
  312. style = { styles.passwordContainer } >
  313. <Text>
  314. { t('security.about') }
  315. </Text>
  316. <View
  317. style = {
  318. _locked !== LOCKED_REMOTELY
  319. && styles.passwordContainerControls
  320. }>
  321. <View>
  322. { this._setRoomPasswordMessage() }
  323. </View>
  324. { setPasswordControls }
  325. </View>
  326. </View>
  327. );
  328. }
  329. /**
  330. * Renders room lock text input/message.
  331. *
  332. * @returns {ReactElement}
  333. * @private
  334. */
  335. _setRoomPasswordMessage() {
  336. let textInputProps = _TEXT_INPUT_PROPS;
  337. const {
  338. _isModerator,
  339. _locked,
  340. _password,
  341. _passwordNumberOfDigits,
  342. t
  343. } = this.props;
  344. const { passwordInputValue, showElement } = this.state;
  345. if (_passwordNumberOfDigits) {
  346. textInputProps = {
  347. ...textInputProps,
  348. keyboardType: 'numeric',
  349. maxLength: _passwordNumberOfDigits
  350. };
  351. }
  352. if (!_isModerator) {
  353. return null;
  354. }
  355. if (showElement) {
  356. if (typeof _locked === 'undefined') {
  357. return (
  358. <TextInput
  359. autoFocus = { true }
  360. onChangeText = { this._onChangeText }
  361. placeholder = { t('lobby.passwordField') }
  362. placeholderTextColor = { BaseTheme.palette.text03 }
  363. selectionColor = { BaseTheme.palette.action03Active }
  364. style = { styles.passwordInput }
  365. underlineColorAndroid = { FIELD_UNDERLINE }
  366. value = { passwordInputValue }
  367. { ...textInputProps } />
  368. );
  369. } else if (_locked) {
  370. if (_locked === LOCKED_LOCALLY && typeof _password !== 'undefined') {
  371. return (
  372. <View style = { styles.savedPasswordContainer }>
  373. <Text style = { styles.savedPasswordLabel }>
  374. { t('info.password') }
  375. </Text>
  376. <Text style = { styles.savedPassword }>
  377. { passwordInputValue }
  378. </Text>
  379. </View>
  380. );
  381. }
  382. }
  383. }
  384. }
  385. _onToggleLobbyMode: () => void;
  386. /**
  387. * Handles the enable-disable lobby mode switch.
  388. *
  389. * @private
  390. * @returns {void}
  391. */
  392. _onToggleLobbyMode() {
  393. const { _lobbyEnabled, dispatch } = this.props;
  394. if (_lobbyEnabled) {
  395. dispatch(toggleLobbyMode(false));
  396. } else {
  397. dispatch(toggleLobbyMode(true));
  398. }
  399. }
  400. _onAddPassword: () => void;
  401. /**
  402. * Callback to be invoked when add password button is pressed.
  403. *
  404. * @returns {void}
  405. */
  406. _onAddPassword() {
  407. const { showElement } = this.state;
  408. this.setState({
  409. showElement: !showElement
  410. });
  411. }
  412. /**
  413. * Verifies input in case only digits are required.
  414. *
  415. * @param {string} passwordInputValue - The value of the password
  416. * text input.
  417. * @private
  418. * @returns {boolean} False when the value is not valid and True otherwise.
  419. */
  420. _validateInputValue(passwordInputValue: string) {
  421. const { _passwordNumberOfDigits } = this.props;
  422. // we want only digits,
  423. // but both number-pad and numeric add ',' and '.' as symbols
  424. if (_passwordNumberOfDigits
  425. && passwordInputValue.length > 0
  426. && !/^\d+$/.test(passwordInputValue)) {
  427. return false;
  428. }
  429. return true;
  430. }
  431. _onChangeText: string => void;
  432. /**
  433. * Callback to be invoked when the text in the field changes.
  434. *
  435. * @param {string} passwordInputValue - The value of password input.
  436. * @returns {void}
  437. */
  438. _onChangeText(passwordInputValue) {
  439. if (!this._validateInputValue(passwordInputValue)) {
  440. return;
  441. }
  442. this.setState({
  443. passwordInputValue
  444. });
  445. }
  446. _onCancel: () => void;
  447. /**
  448. * Cancels value typed in text input.
  449. *
  450. * @returns {void}
  451. */
  452. _onCancel() {
  453. this.setState({
  454. passwordInputValue: '',
  455. showElement: false
  456. });
  457. this.props.dispatch(unlockRoom());
  458. }
  459. _onCopy: () => void;
  460. /**
  461. * Copies room password.
  462. *
  463. * @returns {void}
  464. */
  465. _onCopy() {
  466. const { passwordInputValue } = this.state;
  467. Clipboard.setString(passwordInputValue);
  468. }
  469. _onSubmit: () => void;
  470. /**
  471. * Submits value typed in text input.
  472. *
  473. * @returns {void}
  474. */
  475. _onSubmit() {
  476. const {
  477. _conference,
  478. dispatch
  479. } = this.props;
  480. const { passwordInputValue } = this.state;
  481. dispatch(endRoomLockRequest(_conference, passwordInputValue));
  482. }
  483. }
  484. /**
  485. * Maps part of the Redux state to the props of this component.
  486. *
  487. * @param {Object} state - The Redux state.
  488. * @returns {Props}
  489. */
  490. function _mapStateToProps(state: Object): Object {
  491. const { conference, locked, password } = state['features/base/conference'];
  492. const { hideLobbyButton } = state['features/base/config'];
  493. const { lobbyEnabled } = state['features/lobby'];
  494. const { roomPasswordNumberOfDigits } = state['features/base/config'];
  495. const lobbySupported = conference && conference.isLobbySupported();
  496. const visible = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true);
  497. return {
  498. _conference: conference,
  499. _dialogStyles: ColorSchemeRegistry.get(state, 'Dialog'),
  500. _isModerator: isLocalParticipantModerator(state),
  501. _lobbyEnabled: lobbyEnabled,
  502. _lobbyModeSwitchVisible:
  503. lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton && !isInBreakoutRoom(state),
  504. _locked: locked,
  505. _lockedConference: Boolean(conference && locked),
  506. _password: password,
  507. _passwordNumberOfDigits: roomPasswordNumberOfDigits,
  508. _roomPasswordControls: visible
  509. };
  510. }
  511. export default translate(connect(_mapStateToProps)(SecurityDialog));