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.

InfoDialog.web.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. /* @flow */
  2. import React, { Component } from 'react';
  3. import { connect } from 'react-redux';
  4. import { setPassword } from '../../../base/conference';
  5. import { getInviteURL } from '../../../base/connection';
  6. import { translate } from '../../../base/i18n';
  7. import { isLocalParticipantModerator } from '../../../base/participants';
  8. import { _getDefaultPhoneNumber, getDialInfoPageURL } from '../../functions';
  9. import DialInNumber from './DialInNumber';
  10. import PasswordForm from './PasswordForm';
  11. const logger = require('jitsi-meet-logger').getLogger(__filename);
  12. /**
  13. * The type of the React {@code Component} props of {@link InfoDialog}.
  14. */
  15. type Props = {
  16. /**
  17. * Whether or not the current user can modify the current password.
  18. */
  19. _canEditPassword: boolean,
  20. /**
  21. * The JitsiConference for which to display a lock state and change the
  22. * password.
  23. */
  24. _conference: Object,
  25. /**
  26. * The name of the current conference. Used as part of inviting users.
  27. */
  28. _conferenceName: string,
  29. /**
  30. * The current url of the conference to be copied onto the clipboard.
  31. */
  32. _inviteURL: string,
  33. /**
  34. * The current location url of the conference.
  35. */
  36. _locationURL: Object,
  37. /**
  38. * The value for how the conference is locked (or undefined if not locked)
  39. * as defined by room-lock constants.
  40. */
  41. _locked: string,
  42. /**
  43. * The current known password for the JitsiConference.
  44. */
  45. _password: string,
  46. /**
  47. * The object representing the dialIn feature.
  48. */
  49. dialIn: Object,
  50. /**
  51. * Invoked to open a dialog for adding participants to the conference.
  52. */
  53. dispatch: Dispatch<*>,
  54. /**
  55. * The current known URL for a live stream in progress.
  56. */
  57. liveStreamViewURL: string,
  58. /**
  59. * Callback invoked when the dialog should be closed.
  60. */
  61. onClose: Function,
  62. /**
  63. * Callback invoked when a mouse-related event has been detected.
  64. */
  65. onMouseOver: Function,
  66. /**
  67. * Invoked to obtain translated strings.
  68. */
  69. t: Function
  70. };
  71. /**
  72. * The type of the React {@code Component} state of {@link InfoDialog}.
  73. */
  74. type State = {
  75. /**
  76. * Whether or not to show the password in editing mode.
  77. */
  78. passwordEditEnabled: boolean,
  79. /**
  80. * The conference dial-in number to display.
  81. */
  82. phoneNumber: ?string
  83. };
  84. /**
  85. * A React Component with the contents for a dialog that shows information about
  86. * the current conference.
  87. *
  88. * @extends Component
  89. */
  90. class InfoDialog extends Component<Props, State> {
  91. _copyElement: ?Object;
  92. /**
  93. * {@code InfoDialog} component's local state.
  94. *
  95. * @type {Object}
  96. * @property {boolean} passwordEditEnabled - Whether or not to show the
  97. * {@code PasswordForm} in its editing state.
  98. * @property {string} phoneNumber - The number to display for dialing into
  99. * the conference.
  100. */
  101. state = {
  102. passwordEditEnabled: false,
  103. phoneNumber: undefined
  104. };
  105. /**
  106. * Initializes new {@code InfoDialog} instance.
  107. *
  108. * @param {Object} props - The read-only properties with which the new
  109. * instance is to be initialized.
  110. */
  111. constructor(props: Props) {
  112. super(props);
  113. const { defaultCountry, numbers } = props.dialIn;
  114. if (numbers) {
  115. this.state.phoneNumber
  116. = _getDefaultPhoneNumber(numbers, defaultCountry);
  117. }
  118. /**
  119. * The internal reference to the DOM/HTML element backing the React
  120. * {@code Component} text area. It is necessary for the implementation
  121. * of copying to the clipboard.
  122. *
  123. * @private
  124. * @type {HTMLTextAreaElement}
  125. */
  126. this._copyElement = null;
  127. // Bind event handlers so they are only bound once for every instance.
  128. this._onClickURLText = this._onClickURLText.bind(this);
  129. this._onCopyInviteURL = this._onCopyInviteURL.bind(this);
  130. this._onPasswordRemove = this._onPasswordRemove.bind(this);
  131. this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
  132. this._onTogglePasswordEditState
  133. = this._onTogglePasswordEditState.bind(this);
  134. this._setCopyElement = this._setCopyElement.bind(this);
  135. }
  136. /**
  137. * Implements React's {@link Component#componentWillReceiveProps()}. Invoked
  138. * before this mounted component receives new props.
  139. *
  140. * @inheritdoc
  141. * @param {Props} nextProps - New props component will receive.
  142. */
  143. componentWillReceiveProps(nextProps) {
  144. if (!this.props._password && nextProps._password) {
  145. this.setState({ passwordEditEnabled: false });
  146. }
  147. if (!this.state.phoneNumber && nextProps.dialIn.numbers) {
  148. const { defaultCountry, numbers } = nextProps.dialIn;
  149. this.setState({
  150. phoneNumber:
  151. _getDefaultPhoneNumber(numbers, defaultCountry)
  152. });
  153. }
  154. }
  155. /**
  156. * Implements React's {@link Component#render()}.
  157. *
  158. * @inheritdoc
  159. * @returns {ReactElement}
  160. */
  161. render() {
  162. const { liveStreamViewURL, onMouseOver, t } = this.props;
  163. return (
  164. <div
  165. className = 'info-dialog'
  166. onMouseOver = { onMouseOver } >
  167. <div className = 'info-dialog-column'>
  168. <h4 className = 'info-dialog-icon'>
  169. <i className = 'icon-info' />
  170. </h4>
  171. </div>
  172. <div className = 'info-dialog-column'>
  173. <div className = 'info-dialog-title'>
  174. { t('info.title') }
  175. </div>
  176. <div className = 'info-dialog-conference-url'>
  177. <span className = 'info-label'>
  178. { t('info.conferenceURL') }
  179. </span>
  180. <span className = 'spacer'>&nbsp;</span>
  181. <span className = 'info-value'>
  182. <a
  183. className = 'info-dialog-url-text'
  184. href = { this.props._inviteURL }
  185. onClick = { this._onClickURLText } >
  186. { this._getURLToDisplay() }
  187. </a>
  188. </span>
  189. </div>
  190. <div className = 'info-dialog-dial-in'>
  191. { this._renderDialInDisplay() }
  192. </div>
  193. { liveStreamViewURL && this._renderLiveStreamURL() }
  194. <div className = 'info-dialog-password'>
  195. <PasswordForm
  196. editEnabled = { this.state.passwordEditEnabled }
  197. locked = { this.props._locked }
  198. onSubmit = { this._onPasswordSubmit }
  199. password = { this.props._password } />
  200. </div>
  201. <div className = 'info-dialog-action-links'>
  202. <div className = 'info-dialog-action-link'>
  203. <a
  204. className = 'info-copy'
  205. onClick = { this._onCopyInviteURL }>
  206. { t('dialog.copy') }
  207. </a>
  208. </div>
  209. { this._renderPasswordAction() }
  210. </div>
  211. </div>
  212. <textarea
  213. className = 'info-dialog-copy-element'
  214. readOnly = { true }
  215. ref = { this._setCopyElement }
  216. tabIndex = '-1'
  217. value = { this._getTextToCopy() } />
  218. </div>
  219. );
  220. }
  221. /**
  222. * Generates the URL for the static dial in info page.
  223. *
  224. * @private
  225. * @returns {string}
  226. */
  227. _getDialInfoPageURL() {
  228. return getDialInfoPageURL(
  229. encodeURIComponent(this.props._conferenceName),
  230. this.props._locationURL);
  231. }
  232. /**
  233. * Creates a message describing how to dial in to the conference.
  234. *
  235. * @private
  236. * @returns {string}
  237. */
  238. _getTextToCopy() {
  239. const { liveStreamViewURL, t } = this.props;
  240. let invite = t('info.inviteURL', {
  241. url: this.props._inviteURL
  242. });
  243. if (liveStreamViewURL) {
  244. const liveStream = t('info.inviteLiveStream', {
  245. url: liveStreamViewURL
  246. });
  247. invite = `${invite}\n${liveStream}`;
  248. }
  249. if (this._shouldDisplayDialIn()) {
  250. const dial = t('info.invitePhone', {
  251. number: this.state.phoneNumber,
  252. conferenceID: this.props.dialIn.conferenceID
  253. });
  254. const moreNumbers = t('info.invitePhoneAlternatives', {
  255. url: this._getDialInfoPageURL()
  256. });
  257. invite = `${invite}\n${dial}\n${moreNumbers}`;
  258. }
  259. return invite;
  260. }
  261. /**
  262. * Modifies the inviteURL for display in the modal.
  263. *
  264. * @private
  265. * @returns {string}
  266. */
  267. _getURLToDisplay() {
  268. return this.props._inviteURL.replace(/^https?:\/\//i, '');
  269. }
  270. _onClickURLText: (Object) => void;
  271. /**
  272. * Callback invoked when a displayed URL link is clicked to prevent actual
  273. * navigation from happening. The URL links have an href to display the
  274. * action "Copy Link Address" in the context menu but otherwise it should
  275. * not behave like links.
  276. *
  277. * @param {Object} event - The click event from clicking on the link.
  278. * @private
  279. * @returns {void}
  280. */
  281. _onClickURLText(event) {
  282. event.preventDefault();
  283. }
  284. _onCopyInviteURL: () => void;
  285. /**
  286. * Callback invoked to copy the contents of {@code this._copyElement} to the
  287. * clipboard.
  288. *
  289. * @private
  290. * @returns {void}
  291. */
  292. _onCopyInviteURL() {
  293. try {
  294. if (!this._copyElement) {
  295. throw new Error('No element to copy from.');
  296. }
  297. this._copyElement && this._copyElement.select();
  298. document.execCommand('copy');
  299. this._copyElement && this._copyElement.blur();
  300. } catch (err) {
  301. logger.error('error when copying the text', err);
  302. }
  303. }
  304. _onPasswordRemove: () => void;
  305. /**
  306. * Callback invoked to unlock the current JitsiConference.
  307. *
  308. * @private
  309. * @returns {void}
  310. */
  311. _onPasswordRemove() {
  312. this._onPasswordSubmit('');
  313. }
  314. _onPasswordSubmit: (string) => void;
  315. /**
  316. * Callback invoked to set a password on the current JitsiConference.
  317. *
  318. * @param {string} enteredPassword - The new password to be used to lock the
  319. * current JitsiConference.
  320. * @private
  321. * @returns {void}
  322. */
  323. _onPasswordSubmit(enteredPassword) {
  324. const { _conference } = this.props;
  325. this.props.dispatch(setPassword(
  326. _conference,
  327. _conference.lock,
  328. enteredPassword
  329. ));
  330. }
  331. _onTogglePasswordEditState: () => void;
  332. /**
  333. * Toggles whether or not the password should currently be shown as being
  334. * edited locally.
  335. *
  336. * @private
  337. * @returns {void}
  338. */
  339. _onTogglePasswordEditState() {
  340. this.setState({
  341. passwordEditEnabled: !this.state.passwordEditEnabled
  342. });
  343. }
  344. /**
  345. * Returns a ReactElement for showing how to dial into the conference, if
  346. * dialing in is available.
  347. *
  348. * @private
  349. * @returns {null|ReactElement}
  350. */
  351. _renderDialInDisplay() {
  352. if (!this._shouldDisplayDialIn()) {
  353. return null;
  354. }
  355. return (
  356. <div>
  357. <DialInNumber
  358. conferenceID = { this.props.dialIn.conferenceID }
  359. phoneNumber = { this.state.phoneNumber } />
  360. <a
  361. className = 'more-numbers'
  362. href = { this._getDialInfoPageURL() }
  363. rel = 'noopener noreferrer'
  364. target = '_blank'>
  365. { this.props.t('info.moreNumbers') }
  366. </a>
  367. </div>
  368. );
  369. }
  370. /**
  371. * Returns a ReactElement for interacting with the password field.
  372. *
  373. * @private
  374. * @returns {null|ReactElement}
  375. */
  376. _renderPasswordAction() {
  377. const { t } = this.props;
  378. let className, onClick, textKey;
  379. if (!this.props._canEditPassword) {
  380. // intentionally left blank to prevent rendering anything
  381. } else if (this.state.passwordEditEnabled) {
  382. className = 'cancel-password';
  383. onClick = this._onTogglePasswordEditState;
  384. textKey = 'info.cancelPassword';
  385. } else if (this.props._locked) {
  386. className = 'remove-password';
  387. onClick = this._onPasswordRemove;
  388. textKey = 'dialog.removePassword';
  389. } else {
  390. className = 'add-password';
  391. onClick = this._onTogglePasswordEditState;
  392. textKey = 'info.addPassword';
  393. }
  394. return className && onClick && textKey
  395. ? <div className = 'info-dialog-action-link'>
  396. <a
  397. className = { className }
  398. onClick = { onClick }>
  399. { t(textKey) }
  400. </a>
  401. </div>
  402. : null;
  403. }
  404. /**
  405. * Returns a ReactElement for display a link to the current url of a
  406. * live stream in progress.
  407. *
  408. * @private
  409. * @returns {null|ReactElement}
  410. */
  411. _renderLiveStreamURL() {
  412. const { liveStreamViewURL, t } = this.props;
  413. return (
  414. <div className = 'info-dialog-live-stream-url'>
  415. <span className = 'info-label'>
  416. { t('info.liveStreamURL') }
  417. </span>
  418. <span className = 'spacer'>&nbsp;</span>
  419. <span className = 'info-value'>
  420. <a
  421. className = 'info-dialog-url-text'
  422. href = { liveStreamViewURL }
  423. onClick = { this._onClickURLText } >
  424. { liveStreamViewURL }
  425. </a>
  426. </span>
  427. </div>
  428. );
  429. }
  430. /**
  431. * Returns whether or not dial-in related UI should be displayed.
  432. *
  433. * @private
  434. * @returns {boolean}
  435. */
  436. _shouldDisplayDialIn() {
  437. const { conferenceID, numbers, numbersEnabled } = this.props.dialIn;
  438. const { phoneNumber } = this.state;
  439. return Boolean(
  440. conferenceID
  441. && numbers
  442. && numbersEnabled
  443. && phoneNumber);
  444. }
  445. _setCopyElement: () => void;
  446. /**
  447. * Sets the internal reference to the DOM/HTML element backing the React
  448. * {@code Component} input.
  449. *
  450. * @param {HTMLInputElement} element - The DOM/HTML element for this
  451. * {@code Component}'s input.
  452. * @private
  453. * @returns {void}
  454. */
  455. _setCopyElement(element: Object) {
  456. this._copyElement = element;
  457. }
  458. }
  459. /**
  460. * Maps (parts of) the Redux state to the associated props for the
  461. * {@code InfoDialog} component.
  462. *
  463. * @param {Object} state - The Redux state.
  464. * @private
  465. * @returns {{
  466. * _canEditPassword: boolean,
  467. * _conference: Object,
  468. * _conferenceName: string,
  469. * _inviteURL: string,
  470. * _locationURL: string,
  471. * _locked: string,
  472. * _password: string
  473. * }}
  474. */
  475. function _mapStateToProps(state) {
  476. const {
  477. conference,
  478. locked,
  479. password,
  480. room
  481. } = state['features/base/conference'];
  482. return {
  483. _canEditPassword: isLocalParticipantModerator(state),
  484. _conference: conference,
  485. _conferenceName: room,
  486. _inviteURL: getInviteURL(state),
  487. _locationURL: state['features/base/connection'].locationURL,
  488. _locked: locked,
  489. _password: password
  490. };
  491. }
  492. export default translate(connect(_mapStateToProps)(InfoDialog));