@ -0,0 +1,252 @@ |
||||
.invite-more { |
||||
&-container { |
||||
color: #fff; |
||||
font-weight: 600; |
||||
position: absolute; |
||||
width: 100%; |
||||
text-align: center; |
||||
z-index: $zindex2; |
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); |
||||
|
||||
&.elevated { |
||||
z-index: $filmstripVideosZ + 1; |
||||
} |
||||
} |
||||
|
||||
&-header { |
||||
font-size: 19px; |
||||
line-height: 28px; |
||||
margin: 24px 0 16px 0; |
||||
} |
||||
|
||||
&-button { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin: auto; |
||||
padding: 8px 16px; |
||||
width: 152px; |
||||
height: 24px; |
||||
background: #0376DA; |
||||
border-radius: 3px; |
||||
font-size: 14px; |
||||
line-height: 24px; |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
background: #278ADF; |
||||
} |
||||
|
||||
&-text { |
||||
font-size: 15px; |
||||
line-height: 24px; |
||||
} |
||||
} |
||||
&-dialog { |
||||
color: #fff; |
||||
font-size: 15px; |
||||
line-height: 24px; |
||||
|
||||
& > span { |
||||
font-weight: 600; |
||||
} |
||||
|
||||
&.header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
margin: 16px 16px 24px; |
||||
width: calc(100% - 32px); |
||||
color: #fff; |
||||
font-weight: 600; |
||||
font-size: 24px; |
||||
line-height: 32px; |
||||
|
||||
& > div > svg { |
||||
cursor: pointer; |
||||
fill: #A4B8D1; |
||||
} |
||||
} |
||||
|
||||
&.copy-link { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 8px 8px 8px 16px; |
||||
margin-top: 8px; |
||||
width: calc(100% - 24px); |
||||
height: 24px; |
||||
|
||||
background: #0376DA; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
background: #278ADF; |
||||
font-weight: 600; |
||||
} |
||||
|
||||
&-text { |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
max-width: 292px; |
||||
|
||||
&.selected { |
||||
font-weight: 600; |
||||
} |
||||
} |
||||
|
||||
&.clicked { |
||||
background: #31B76A; |
||||
} |
||||
|
||||
& > div > svg > path { |
||||
fill: #fff; |
||||
} |
||||
} |
||||
|
||||
&.separator { |
||||
margin: 24px 0 24px -20px; |
||||
padding: 0 20px; |
||||
width: 100%; |
||||
height: 1px; |
||||
background: #5E6D7A; |
||||
} |
||||
|
||||
&.email-container { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 8px 8px 8px 16px; |
||||
margin-top: 24px; |
||||
width: calc(100% - 26px); |
||||
height: 22px; |
||||
|
||||
background: #2A3A4B; |
||||
border: 1px solid #5E6D7A; |
||||
border-radius: 3px; |
||||
cursor: pointer; |
||||
|
||||
&.active { |
||||
border-radius: 3px 3px 0 0; |
||||
} |
||||
} |
||||
|
||||
&.icon-container { |
||||
display: none; |
||||
|
||||
&.active { |
||||
display: flex; |
||||
width: calc(100% - 26px); |
||||
padding: 8px 8px 8px 16px; |
||||
|
||||
background: #2A3A4B; |
||||
border: 1px solid #5E6D7A; |
||||
border-top: none; |
||||
border-radius: 0 0 3px 3px; |
||||
|
||||
& > * { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
height: 40px; |
||||
width: 40px; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&:hover > div:hover { |
||||
background-color: rgba(255, 255, 255, 0.2); |
||||
} |
||||
|
||||
& > :not(:last-child) { |
||||
margin-right: 16px; |
||||
} |
||||
|
||||
.copy-invite-icon > div > svg > path { |
||||
fill: #A4B8D1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&.dial-in-display { |
||||
.info-label { |
||||
color: #A4B8D1; |
||||
} |
||||
|
||||
.dial-in-copy { |
||||
display: inline-block; |
||||
vertical-align: middle; |
||||
margin-left: 21px; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
|
||||
&.invite-buttons { |
||||
width: 100%; |
||||
text-align: right; |
||||
margin-top: 8px; |
||||
|
||||
& > a { |
||||
display: inline-block; |
||||
height: 24px; |
||||
width: 48px; |
||||
border-radius: 3px; |
||||
text-align: center; |
||||
text-decoration: none; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&-cancel { |
||||
margin-right: 16px; |
||||
padding: 7px 15px; |
||||
background: #2A3A4B; |
||||
border: 1px solid #5E6D7A; |
||||
} |
||||
|
||||
&-add { |
||||
padding: 8px 16px; |
||||
background: #0376DA; |
||||
} |
||||
} |
||||
|
||||
&.stream { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 8px 8px 8px 16px; |
||||
margin-top: 8px; |
||||
width: calc(100% - 26px); |
||||
height: 22px; |
||||
|
||||
background: #2A3A4B; |
||||
border: 1px solid #5E6D7A; |
||||
border-radius: 3px; |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
font-weight: 600; |
||||
} |
||||
|
||||
&-text { |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
max-width: 292px; |
||||
|
||||
&.selected { |
||||
font-weight: 600; |
||||
} |
||||
} |
||||
|
||||
&.clicked { |
||||
background: #31B76A; |
||||
border: 1px solid #31B76A; |
||||
} |
||||
|
||||
& > div > svg > path { |
||||
fill: #fff; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
.security { |
||||
&-dialog { |
||||
color: #fff; |
||||
font-size: 15px; |
||||
line-height: 24px; |
||||
|
||||
&.password { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
|
||||
&-actions { |
||||
a { |
||||
cursor: pointer; |
||||
text-decoration: none; |
||||
font-size: 14px; |
||||
color: #6FB1EA; |
||||
} |
||||
|
||||
& > a + a { |
||||
margin-left: 24px; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.new-toolbox .toolbox-content .toolbox-icon.security-toolbar-button, |
||||
.new-toolbox .toolbox-content .toolbox-icon.toggled.security-toolbar-button { |
||||
background: rgba(241, 173, 51, 0.7); |
||||
border: 1px solid rgba(255, 255, 255, 0.4); |
||||
|
||||
&:hover { |
||||
background: rgba(241, 173, 51, 0.7); |
||||
border: 1px solid rgba(255, 255, 255, 0.4); |
||||
} |
||||
} |
After Width: | Height: | Size: 488 B |
After Width: | Height: | Size: 332 B |
After Width: | Height: | Size: 605 B |
Before Width: | Height: | Size: 240 B |
After Width: | Height: | Size: 720 B |
After Width: | Height: | Size: 258 B |
After Width: | Height: | Size: 757 B |
After Width: | Height: | Size: 836 B |
After Width: | Height: | Size: 215 B |
@ -0,0 +1,94 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import { Icon, IconInviteMore } from '../../../base/icons'; |
||||
import { getParticipantCount } from '../../../base/participants'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { beginAddPeople } from '../../../invite'; |
||||
import { isToolboxVisible } from '../../../toolbox'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Whether tile view is enabled. |
||||
*/ |
||||
_tileViewEnabled: Boolean, |
||||
|
||||
/** |
||||
* Whether to show the option to invite more people |
||||
* instead of the subject. |
||||
*/ |
||||
_visible: boolean, |
||||
|
||||
/** |
||||
* Handler to open the invite dialog. |
||||
*/ |
||||
onClick: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
} |
||||
|
||||
/** |
||||
* Represents a replacement for the subject, prompting the |
||||
* sole participant to invite more participants. |
||||
* |
||||
* @param {Object} props - The props of the component. |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function InviteMore({ |
||||
_tileViewEnabled, |
||||
_visible, |
||||
onClick, |
||||
t |
||||
}: Props) { |
||||
return ( |
||||
_visible |
||||
? <div className = { `invite-more-container${_tileViewEnabled ? ' elevated' : ''}` }> |
||||
<div className = 'invite-more-header'> |
||||
{t('addPeople.inviteMoreHeader')} |
||||
</div> |
||||
<div |
||||
className = 'invite-more-button' |
||||
onClick = { onClick }> |
||||
<Icon src = { IconInviteMore } /> |
||||
<div className = 'invite-more-text'> |
||||
{t('addPeople.inviteMorePrompt')} |
||||
</div> |
||||
</div> |
||||
</div> : null |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated |
||||
* {@code Subject}'s props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function mapStateToProps(state) { |
||||
const participantCount = getParticipantCount(state); |
||||
|
||||
return { |
||||
_tileViewEnabled: state['features/video-layout'].tileViewEnabled, |
||||
_visible: isToolboxVisible(state) && participantCount === 1 |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Maps dispatching of some action to React component props. |
||||
* |
||||
* @param {Function} dispatch - Redux action dispatcher. |
||||
* @returns {Props} |
||||
*/ |
||||
const mapDispatchToProps = { |
||||
onClick: () => beginAddPeople() |
||||
}; |
||||
|
||||
export default translate(connect(mapStateToProps, mapDispatchToProps)(InviteMore)); |
@ -1,478 +1,214 @@ |
||||
// @flow
|
||||
|
||||
import InlineMessage from '@atlaskit/inline-message'; |
||||
import React from 'react'; |
||||
import type { Dispatch } from 'redux'; |
||||
import React, { useState, useEffect } from 'react'; |
||||
|
||||
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics'; |
||||
import { Avatar } from '../../../../base/avatar'; |
||||
import { Dialog, hideDialog } from '../../../../base/dialog'; |
||||
import { translate, translateToHTML } from '../../../../base/i18n'; |
||||
import { Icon, IconPhone } from '../../../../base/icons'; |
||||
import { getRoomName } from '../../../../base/conference'; |
||||
import { getInviteURL } from '../../../../base/connection'; |
||||
import { Dialog } from '../../../../base/dialog'; |
||||
import { translate } from '../../../../base/i18n'; |
||||
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet'; |
||||
import { getLocalParticipant } from '../../../../base/participants'; |
||||
import { MultiSelectAutocomplete } from '../../../../base/react'; |
||||
import { connect } from '../../../../base/redux'; |
||||
import { getActiveSession } from '../../../../recording'; |
||||
|
||||
import AbstractAddPeopleDialog, { |
||||
type Props as AbstractProps, |
||||
type State, |
||||
_mapStateToProps as _abstractMapStateToProps |
||||
} from '../AbstractAddPeopleDialog'; |
||||
import { updateDialInNumbers } from '../../../actions'; |
||||
import { _getDefaultPhoneNumber, getInviteText, isAddPeopleEnabled, isDialOutEnabled } from '../../../functions'; |
||||
|
||||
import CopyMeetingLinkSection from './CopyMeetingLinkSection'; |
||||
import DialInSection from './DialInSection'; |
||||
import Header from './Header'; |
||||
import InviteByEmailSection from './InviteByEmailSection'; |
||||
import InviteContactsSection from './InviteContactsSection'; |
||||
import LiveStreamSection from './LiveStreamSection'; |
||||
|
||||
declare var interfaceConfig: Object; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link AddPeopleDialog}. |
||||
*/ |
||||
type Props = AbstractProps & { |
||||
type Props = { |
||||
|
||||
/** |
||||
* The {@link JitsiMeetConference} which will be used to invite "room" |
||||
* participants through the SIP Jibri (Video SIP gateway). |
||||
* The name of the current conference. Used as part of inviting users. |
||||
*/ |
||||
_conference: Object, |
||||
_conferenceName: string, |
||||
|
||||
/** |
||||
* Whether to show a footer text after the search results as a last element. |
||||
* The object representing the dialIn feature. |
||||
*/ |
||||
_footerTextEnabled: boolean, |
||||
_dialIn: Object, |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
* Whether or not invite should be hidden. |
||||
*/ |
||||
dispatch: Dispatch<any>, |
||||
_hideInviteContacts: boolean, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function, |
||||
}; |
||||
|
||||
/** |
||||
* The dialog that allows to invite people to the call. |
||||
* The current url of the conference to be copied onto the clipboard. |
||||
*/ |
||||
class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> { |
||||
_multiselect = null; |
||||
|
||||
_resourceClient: Object; |
||||
|
||||
state = { |
||||
addToCallError: false, |
||||
addToCallInProgress: false, |
||||
inviteItems: [] |
||||
}; |
||||
_inviteUrl: string, |
||||
|
||||
/** |
||||
* Initializes a new {@code AddPeopleDialog} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
* The current known URL for a live stream in progress. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onItemSelected = this._onItemSelected.bind(this); |
||||
this._onSelectionChange = this._onSelectionChange.bind(this); |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
this._parseQueryResults = this._parseQueryResults.bind(this); |
||||
this._setMultiSelectElement = this._setMultiSelectElement.bind(this); |
||||
|
||||
this._resourceClient = { |
||||
makeQuery: this._query, |
||||
parseResults: this._parseQueryResults |
||||
}; |
||||
} |
||||
_liveStreamViewURL: string, |
||||
|
||||
/** |
||||
* Sends an analytics event to record the dialog has been shown. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
* The redux representation of the local participant. |
||||
*/ |
||||
componentDidMount() { |
||||
sendAnalytics(createInviteDialogEvent( |
||||
'invite.dialog.opened', 'dialog')); |
||||
} |
||||
_localParticipantName: ?string, |
||||
|
||||
/** |
||||
* React Component method that executes once component is updated. |
||||
* |
||||
* @param {Object} prevProps - The state object before the update. |
||||
* @param {Object} prevState - The state object before the update. |
||||
* @returns {void} |
||||
*/ |
||||
componentDidUpdate(prevProps, prevState) { |
||||
/** |
||||
* Clears selected items from the multi select component on successful |
||||
* invite. |
||||
* The current location url of the conference. |
||||
*/ |
||||
if (prevState.addToCallError |
||||
&& !this.state.addToCallInProgress |
||||
&& !this.state.addToCallError |
||||
&& this._multiselect) { |
||||
this._multiselect.setSelectedItems([]); |
||||
} |
||||
} |
||||
_locationUrl: Object, |
||||
|
||||
/** |
||||
* Sends an analytics event to record the dialog has been closed. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
componentWillUnmount() { |
||||
sendAnalytics(createInviteDialogEvent( |
||||
'invite.dialog.closed', 'dialog')); |
||||
} |
||||
t: Function, |
||||
|
||||
/** |
||||
* Renders the content of this component. |
||||
* |
||||
* @returns {ReactElement} |
||||
* Method to update the dial in numbers. |
||||
*/ |
||||
render() { |
||||
const { |
||||
_addPeopleEnabled, |
||||
_dialOutEnabled, |
||||
_footerTextEnabled, |
||||
t |
||||
} = this.props; |
||||
let isMultiSelectDisabled = this.state.addToCallInProgress || false; |
||||
let placeholder; |
||||
let loadingMessage; |
||||
let noMatches; |
||||
let footerText; |
||||
|
||||
if (_addPeopleEnabled && _dialOutEnabled) { |
||||
loadingMessage = 'addPeople.loading'; |
||||
noMatches = 'addPeople.noResults'; |
||||
placeholder = 'addPeople.searchPeopleAndNumbers'; |
||||
} else if (_addPeopleEnabled) { |
||||
loadingMessage = 'addPeople.loadingPeople'; |
||||
noMatches = 'addPeople.noResults'; |
||||
placeholder = 'addPeople.searchPeople'; |
||||
} else if (_dialOutEnabled) { |
||||
loadingMessage = 'addPeople.loadingNumber'; |
||||
noMatches = 'addPeople.noValidNumbers'; |
||||
placeholder = 'addPeople.searchNumbers'; |
||||
} else { |
||||
isMultiSelectDisabled = true; |
||||
noMatches = 'addPeople.noResults'; |
||||
placeholder = 'addPeople.disabled'; |
||||
} |
||||
|
||||
if (_footerTextEnabled) { |
||||
footerText = { |
||||
content: <div className = 'footer-text-wrap'> |
||||
<div> |
||||
<span className = 'footer-telephone-icon'> |
||||
<Icon src = { IconPhone } /> |
||||
</span> |
||||
</div> |
||||
{ translateToHTML(t, 'addPeople.footerText') } |
||||
</div> |
||||
}; |
||||
} |
||||
|
||||
return ( |
||||
<Dialog |
||||
okDisabled = { this._isAddDisabled() } |
||||
okKey = 'addPeople.add' |
||||
onSubmit = { this._onSubmit } |
||||
titleKey = 'addPeople.title' |
||||
width = 'medium'> |
||||
<div className = 'add-people-form-wrap'> |
||||
{ this._renderErrorMessage() } |
||||
<MultiSelectAutocomplete |
||||
footer = { footerText } |
||||
isDisabled = { isMultiSelectDisabled } |
||||
loadingMessage = { t(loadingMessage) } |
||||
noMatchesFound = { t(noMatches) } |
||||
onItemSelected = { this._onItemSelected } |
||||
onSelectionChange = { this._onSelectionChange } |
||||
placeholder = { t(placeholder) } |
||||
ref = { this._setMultiSelectElement } |
||||
resourceClient = { this._resourceClient } |
||||
shouldFitContainer = { true } |
||||
shouldFocus = { true } /> |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
_invite: Array<Object> => Promise<*> |
||||
|
||||
_isAddDisabled: () => boolean; |
||||
|
||||
_onItemSelected: (Object) => Object; |
||||
updateNumbers: Function |
||||
}; |
||||
|
||||
/** |
||||
* Callback invoked when a selection has been made but before it has been |
||||
* set as selected. |
||||
/** |
||||
* Invite More component. |
||||
* |
||||
* @param {Object} item - The item that has just been selected. |
||||
* @private |
||||
* @returns {Object} The item to display as selected in the input. |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
_onItemSelected(item) { |
||||
if (item.item.type === 'phone') { |
||||
item.content = item.item.number; |
||||
} |
||||
|
||||
return item; |
||||
} |
||||
|
||||
_onSelectionChange: (Map<*, *>) => void; |
||||
function AddPeopleDialog({ |
||||
_conferenceName, |
||||
_dialIn, |
||||
_hideInviteContacts, |
||||
_inviteUrl, |
||||
_liveStreamViewURL, |
||||
_localParticipantName, |
||||
_locationUrl, |
||||
t, |
||||
updateNumbers }: Props) { |
||||
const [ phoneNumber, setPhoneNumber ] = useState(undefined); |
||||
|
||||
/** |
||||
* Handles a selection change. |
||||
* |
||||
* @param {Map} selectedItems - The list of selected items. |
||||
* @private |
||||
* @returns {void} |
||||
* Updates the dial-in numbers. |
||||
*/ |
||||
_onSelectionChange(selectedItems) { |
||||
this.setState({ |
||||
inviteItems: selectedItems |
||||
}); |
||||
useEffect(() => { |
||||
if (!_dialIn.numbers) { |
||||
updateNumbers(); |
||||
} |
||||
|
||||
_onSubmit: () => void; |
||||
}, []); |
||||
|
||||
/** |
||||
* Submits the selection for inviting. |
||||
* Sends analytics events when the dialog opens/closes. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSubmit() { |
||||
const { inviteItems } = this.state; |
||||
const invitees = inviteItems.map(({ item }) => item); |
||||
|
||||
this._invite(invitees) |
||||
.then(invitesLeftToSend => { |
||||
if (invitesLeftToSend.length) { |
||||
const unsentInviteIDs |
||||
= invitesLeftToSend.map(invitee => |
||||
invitee.id || invitee.user_id || invitee.number); |
||||
const itemsToSelect |
||||
= inviteItems.filter(({ item }) => |
||||
unsentInviteIDs.includes(item.id || item.user_id || item.number)); |
||||
|
||||
if (this._multiselect) { |
||||
this._multiselect.setSelectedItems(itemsToSelect); |
||||
} |
||||
} else { |
||||
this.props.dispatch(hideDialog()); |
||||
} |
||||
}); |
||||
} |
||||
useEffect(() => { |
||||
sendAnalytics(createInviteDialogEvent( |
||||
'invite.dialog.opened', 'dialog')); |
||||
|
||||
_parseQueryResults: (?Array<Object>) => Array<Object>; |
||||
return () => { |
||||
sendAnalytics(createInviteDialogEvent( |
||||
'invite.dialog.closed', 'dialog')); |
||||
}; |
||||
}, []); |
||||
|
||||
/** |
||||
* Returns the avatar component for a user. |
||||
* Updates the phone number in the state once the dial-in numbers are fetched. |
||||
* |
||||
* @param {Object} user - The user. |
||||
* @param {string} className - The CSS class for the avatar component. |
||||
* @private |
||||
* @returns {ReactElement} |
||||
* @returns {void} |
||||
*/ |
||||
_getAvatar(user, className = 'avatar-small') { |
||||
return (<Avatar |
||||
className = { className } |
||||
status = { user.status } |
||||
url = { user.avatar } />); |
||||
useEffect(() => { |
||||
if (!phoneNumber && _dialIn && _dialIn.numbers) { |
||||
setPhoneNumber(_getDefaultPhoneNumber(_dialIn.numbers)); |
||||
} |
||||
}, [ _dialIn ]); |
||||
|
||||
/** |
||||
* Processes results from requesting available numbers and people by munging |
||||
* each result into a format {@code MultiSelectAutocomplete} can use for |
||||
* display. |
||||
* |
||||
* @param {Array} response - The response object from the server for the |
||||
* query. |
||||
* @private |
||||
* @returns {Object[]} Configuration objects for items to display in the |
||||
* search autocomplete. |
||||
*/ |
||||
_parseQueryResults(response = []) { |
||||
const { t, _dialOutEnabled } = this.props; |
||||
const users = response.filter(item => item.type !== 'phone'); |
||||
const userDisplayItems = []; |
||||
|
||||
users.forEach(user => { |
||||
const { name, phone } = user; |
||||
const tagAvatar = this._getAvatar(user, 'avatar-xsmall'); |
||||
const elemAvatar = this._getAvatar(user); |
||||
|
||||
userDisplayItems.push({ |
||||
content: name, |
||||
elemBefore: elemAvatar, |
||||
item: user, |
||||
tag: { |
||||
elemBefore: tagAvatar |
||||
}, |
||||
value: user.id || user.user_id |
||||
}); |
||||
|
||||
if (phone && _dialOutEnabled) { |
||||
userDisplayItems.push({ |
||||
filterValues: [ name, phone ], |
||||
content: `${phone} (${name})`, |
||||
elemBefore: elemAvatar, |
||||
item: { |
||||
type: 'phone', |
||||
number: phone |
||||
}, |
||||
tag: { |
||||
elemBefore: tagAvatar |
||||
}, |
||||
value: phone |
||||
const invite = getInviteText({ |
||||
_conferenceName, |
||||
_localParticipantName, |
||||
_inviteUrl, |
||||
_locationUrl, |
||||
_dialIn, |
||||
_liveStreamViewURL, |
||||
phoneNumber, |
||||
t |
||||
}); |
||||
} |
||||
const inviteSubject = t('addPeople.inviteMoreMailSubject', { |
||||
appName: interfaceConfig.APP_NAME |
||||
}); |
||||
|
||||
const numbers = response.filter(item => item.type === 'phone'); |
||||
const telephoneIcon = this._renderTelephoneIcon(); |
||||
|
||||
const numberDisplayItems = numbers.map(number => { |
||||
const numberNotAllowedMessage |
||||
= number.allowed ? '' : t('addPeople.countryNotSupported'); |
||||
const countryCodeReminder = number.showCountryCodeReminder |
||||
? t('addPeople.countryReminder') : ''; |
||||
const description |
||||
= `${numberNotAllowedMessage} ${countryCodeReminder}`.trim(); |
||||
|
||||
return { |
||||
filterValues: [ |
||||
number.originalEntry, |
||||
number.number |
||||
], |
||||
content: t('addPeople.telephone', { number: number.number }), |
||||
description, |
||||
isDisabled: !number.allowed, |
||||
elemBefore: telephoneIcon, |
||||
item: number, |
||||
tag: { |
||||
elemBefore: telephoneIcon |
||||
}, |
||||
value: number.number |
||||
}; |
||||
}); |
||||
|
||||
return [ |
||||
...userDisplayItems, |
||||
...numberDisplayItems |
||||
]; |
||||
} |
||||
|
||||
_query: (string) => Promise<Array<Object>>; |
||||
|
||||
/** |
||||
* Renders the error message if the add doesn't succeed. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement|null} |
||||
*/ |
||||
_renderErrorMessage() { |
||||
if (!this.state.addToCallError) { |
||||
return null; |
||||
} |
||||
|
||||
const { t } = this.props; |
||||
const supportString = t('inlineDialogFailure.supportMsg'); |
||||
const supportLink = interfaceConfig.SUPPORT_URL; |
||||
const supportLinkContent |
||||
= ( |
||||
<span> |
||||
<span> |
||||
{ supportString.padEnd(supportString.length + 1) } |
||||
</span> |
||||
<span> |
||||
<a |
||||
href = { supportLink } |
||||
rel = 'noopener noreferrer' |
||||
target = '_blank'> |
||||
{ t('inlineDialogFailure.support') } |
||||
</a> |
||||
</span> |
||||
<span>.</span> |
||||
</span> |
||||
); |
||||
|
||||
return ( |
||||
<div className = 'modal-dialog-form-error'> |
||||
<InlineMessage |
||||
title = { t('addPeople.failedToAdd') } |
||||
type = 'error'> |
||||
{ supportLinkContent } |
||||
</InlineMessage> |
||||
</div> |
||||
); |
||||
<Dialog |
||||
cancelKey = { 'dialog.close' } |
||||
customHeader = { Header } |
||||
hideCancelButton = { true } |
||||
submitDisabled = { true } |
||||
titleKey = 'addPeople.inviteMorePrompt' |
||||
width = { 'small' }> |
||||
<div className = 'invite-more-dialog'> |
||||
{ !_hideInviteContacts && <InviteContactsSection /> } |
||||
<CopyMeetingLinkSection url = { _inviteUrl } /> |
||||
<InviteByEmailSection |
||||
inviteSubject = { inviteSubject } |
||||
inviteText = { invite } /> |
||||
{ |
||||
_liveStreamViewURL |
||||
&& <LiveStreamSection liveStreamViewURL = { _liveStreamViewURL } /> |
||||
} |
||||
{ |
||||
_dialIn.numbers |
||||
&& <DialInSection |
||||
conferenceName = { _conferenceName } |
||||
dialIn = { _dialIn } |
||||
locationUrl = { _locationUrl } |
||||
phoneNumber = { phoneNumber } /> |
||||
} |
||||
|
||||
/** |
||||
* Renders a telephone icon. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_renderTelephoneIcon() { |
||||
return ( |
||||
<span className = 'add-telephone-icon'> |
||||
<Icon src = { IconPhone } /> |
||||
</span> |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
_setMultiSelectElement: (React$ElementRef<*> | null) => void; |
||||
|
||||
/** |
||||
* Sets the instance variable for the multi select component |
||||
* element so it can be accessed directly. |
||||
* |
||||
* @param {Object} element - The DOM element for the component's dialog. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_setMultiSelectElement(element) { |
||||
this._multiselect = element; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated |
||||
* {@code AddPeopleDialog}'s props. |
||||
* Maps (parts of) the Redux state to the associated props for the |
||||
* {@code AddPeopleDialog} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _dialOutAuthUrl: string, |
||||
* _jwt: string, |
||||
* _peopleSearchQueryTypes: Array<string>, |
||||
* _peopleSearchUrl: string |
||||
* }} |
||||
* @returns {Props} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { |
||||
enableFeaturesBasedOnToken |
||||
} = state['features/base/config']; |
||||
let footerTextEnabled = false; |
||||
|
||||
if (enableFeaturesBasedOnToken) { |
||||
const { features = {} } = getLocalParticipant(state); |
||||
|
||||
if (String(features['outbound-call']) !== 'true') { |
||||
footerTextEnabled = true; |
||||
} |
||||
} |
||||
function mapStateToProps(state) { |
||||
const localParticipant = getLocalParticipant(state); |
||||
const currentLiveStreamingSession |
||||
= getActiveSession(state, JitsiRecordingConstants.mode.STREAM); |
||||
const { iAmRecorder } = state['features/base/config']; |
||||
const addPeopleEnabled = isAddPeopleEnabled(state); |
||||
const dialOutEnabled = isDialOutEnabled(state); |
||||
|
||||
return { |
||||
..._abstractMapStateToProps(state), |
||||
_footerTextEnabled: footerTextEnabled |
||||
_conferenceName: getRoomName(state), |
||||
_dialIn: state['features/invite'], |
||||
_hideInviteContacts: |
||||
iAmRecorder || (!addPeopleEnabled && !dialOutEnabled), |
||||
_inviteUrl: getInviteURL(state), |
||||
_liveStreamViewURL: |
||||
currentLiveStreamingSession |
||||
&& currentLiveStreamingSession.liveStreamViewURL, |
||||
_localParticipantName: localParticipant?.name, |
||||
_locationUrl: state['features/base/connection'].locationURL |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(AddPeopleDialog)); |
||||
/** |
||||
* Maps dispatching of some action to React component props. |
||||
* |
||||
* @param {Function} dispatch - Redux action dispatcher. |
||||
* @returns {Props} |
||||
*/ |
||||
const mapDispatchToProps = { |
||||
updateNumbers: () => updateDialInNumbers() |
||||
}; |
||||
|
||||
export default translate( |
||||
connect(mapStateToProps, mapDispatchToProps)(AddPeopleDialog) |
||||
); |
||||
|
@ -0,0 +1,111 @@ |
||||
// @flow
|
||||
|
||||
import React, { useState } from 'react'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
import { Icon, IconCheck, IconCopy } from '../../../../base/icons'; |
||||
|
||||
import { copyText } from './utils'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* The URL of the conference. |
||||
*/ |
||||
url: string |
||||
}; |
||||
|
||||
/** |
||||
* Component meant to enable users to copy the conference URL. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function CopyMeetingLinkSection({ t, url }: Props) { |
||||
const [ isClicked, setIsClicked ] = useState(false); |
||||
const [ isHovered, setIsHovered ] = useState(false); |
||||
|
||||
/** |
||||
* Click handler for the element. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onClick() { |
||||
setIsHovered(false); |
||||
if (copyText(url)) { |
||||
setIsClicked(true); |
||||
|
||||
setTimeout(() => { |
||||
setIsClicked(false); |
||||
}, 2500); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Hover handler for the element. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onHoverIn() { |
||||
if (!isClicked) { |
||||
setIsHovered(true); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Hover handler for the element. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onHoverOut() { |
||||
setIsHovered(false); |
||||
} |
||||
|
||||
/** |
||||
* Renders the content of the link based on the state. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function renderLinkContent() { |
||||
if (isClicked) { |
||||
return ( |
||||
<> |
||||
<div className = 'invite-more-dialog copy-link-text selected'> |
||||
{t('addPeople.linkCopied')} |
||||
</div> |
||||
<Icon src = { IconCheck } /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
const displayUrl = decodeURI(url.replace(/^https?:\/\//i, '')); |
||||
|
||||
return ( |
||||
<> |
||||
<div className = 'invite-more-dialog invite-more-dialog-conference-url copy-link-text'> |
||||
{isHovered ? t('addPeople.copyLink') : displayUrl} |
||||
</div> |
||||
<Icon src = { IconCopy } /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<span>{t('addPeople.shareLink')}</span> |
||||
<div |
||||
className = { `invite-more-dialog copy-link${isClicked ? ' clicked' : ''}` } |
||||
onClick = { onClick } |
||||
onMouseOut = { onHoverOut } |
||||
onMouseOver = { onHoverIn }> |
||||
{ renderLinkContent() } |
||||
</div> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default translate(CopyMeetingLinkSection); |
@ -0,0 +1,76 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
|
||||
import { getDialInfoPageURL } from '../../../functions'; |
||||
|
||||
import DialInNumber from './DialInNumber'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The name of the current conference. Used as part of inviting users. |
||||
*/ |
||||
conferenceName: string, |
||||
|
||||
/** |
||||
* The object representing the dialIn feature. |
||||
*/ |
||||
dialIn: Object, |
||||
|
||||
/** |
||||
* The current location url of the conference. |
||||
*/ |
||||
locationUrl: Object, |
||||
|
||||
/** |
||||
* The phone number to dial to begin the process of dialing into a |
||||
* conference. |
||||
*/ |
||||
phoneNumber: string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
|
||||
}; |
||||
|
||||
/** |
||||
* Returns a ReactElement for showing how to dial into the conference, if |
||||
* dialing in is available. |
||||
* |
||||
* @private |
||||
* @returns {null|ReactElement} |
||||
*/ |
||||
function DialInSection({ |
||||
conferenceName, |
||||
dialIn, |
||||
locationUrl, |
||||
phoneNumber, |
||||
t |
||||
}: Props) { |
||||
return ( |
||||
<div className = 'invite-more-dialog dial-in-display'> |
||||
<DialInNumber |
||||
conferenceID = { dialIn.conferenceID } |
||||
phoneNumber = { phoneNumber } /> |
||||
<a |
||||
className = 'more-numbers' |
||||
href = { |
||||
getDialInfoPageURL( |
||||
conferenceName, |
||||
locationUrl |
||||
) |
||||
} |
||||
rel = 'noopener noreferrer' |
||||
target = '_blank'> |
||||
{ t('info.moreNumbers') } |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export default translate(DialInSection); |
@ -0,0 +1,38 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
import { Icon, IconClose } from '../../../../base/icons'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The {@link ModalDialog} closing function. |
||||
*/ |
||||
onClose: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Custom header of the {@code AddPeopleDialog}. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function Header({ onClose, t }: Props) { |
||||
return ( |
||||
<div |
||||
className = 'invite-more-dialog header'> |
||||
{ t('addPeople.inviteMorePrompt') } |
||||
<Icon |
||||
onClick = { onClose } |
||||
src = { IconClose } /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export default translate(Header); |
@ -0,0 +1,156 @@ |
||||
// @flow
|
||||
|
||||
import React, { useState } from 'react'; |
||||
import Tooltip from '@atlaskit/tooltip'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
import { |
||||
Icon, |
||||
IconArrowDownSmall, |
||||
IconCopy, |
||||
IconEmail, |
||||
IconGoogle, |
||||
IconOutlook, |
||||
IconYahoo |
||||
} from '../../../../base/icons'; |
||||
import { openURLInBrowser } from '../../../../base/util'; |
||||
|
||||
import { copyText } from './utils'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The encoded invitation subject. |
||||
*/ |
||||
inviteSubject: string, |
||||
|
||||
/** |
||||
* The encoded invitation text to be sent. |
||||
*/ |
||||
inviteText: string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function, |
||||
}; |
||||
|
||||
/** |
||||
* Component that renders email invite options. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) { |
||||
const [ isActive, setIsActive ] = useState(false); |
||||
const encodedInviteSubject = encodeURIComponent(inviteSubject); |
||||
const encodedInviteText = encodeURIComponent(inviteText); |
||||
|
||||
/** |
||||
* Copies the conference invitation to the clipboard. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function _onCopyText() { |
||||
copyText(inviteText); |
||||
} |
||||
|
||||
/** |
||||
* Opens an email provider containing the conference invite. |
||||
* |
||||
* @param {string} url - The url to be opened. |
||||
* @returns {Function} |
||||
*/ |
||||
function _onSelectProvider(url) { |
||||
return function() { |
||||
openURLInBrowser(url, true); |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Toggles the email invite drawer. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function _onToggleActiveState() { |
||||
setIsActive(!isActive); |
||||
} |
||||
|
||||
/** |
||||
* Renders clickable elements that each open an email client |
||||
* containing a conference invite. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function renderEmailIcons() { |
||||
const PROVIDER_MAPPING = [ |
||||
{ |
||||
icon: IconEmail, |
||||
tooltipKey: 'addPeople.defaultEmail', |
||||
url: `mailto:?subject=${encodedInviteSubject}&body=${encodedInviteText}` |
||||
}, |
||||
{ |
||||
icon: IconGoogle, |
||||
tooltipKey: 'addPeople.googleEmail', |
||||
url: `https://mail.google.com/mail/?view=cm&fs=1&su=${encodedInviteSubject}&body=${encodedInviteText}` |
||||
}, |
||||
{ |
||||
icon: IconOutlook, |
||||
tooltipKey: 'addPeople.outlookEmail', |
||||
// eslint-disable-next-line max-len
|
||||
url: `https://outlook.office.com/mail/deeplink/compose?subject=${encodedInviteSubject}&body=${encodedInviteText}` |
||||
}, |
||||
{ |
||||
icon: IconYahoo, |
||||
tooltipKey: 'addPeople.yahooEmail', |
||||
url: `https://compose.mail.yahoo.com/?To=&Subj=${encodedInviteSubject}&Body=${encodedInviteText}` |
||||
} |
||||
]; |
||||
|
||||
return ( |
||||
<> |
||||
{ |
||||
PROVIDER_MAPPING.map(({ icon, tooltipKey, url }, idx) => ( |
||||
<Tooltip |
||||
content = { t(tooltipKey) } |
||||
key = { idx } |
||||
position = 'top'> |
||||
<div |
||||
onClick = { _onSelectProvider(url) }> |
||||
<Icon src = { icon } /> |
||||
</div> |
||||
</Tooltip> |
||||
)) |
||||
} |
||||
</> |
||||
); |
||||
|
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<div> |
||||
<div |
||||
className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` } |
||||
onClick = { _onToggleActiveState }> |
||||
<span>{t('addPeople.shareInvite')}</span> |
||||
<Icon src = { IconArrowDownSmall } /> |
||||
</div> |
||||
<div className = { `invite-more-dialog icon-container${isActive ? ' active' : ''}` }> |
||||
<Tooltip |
||||
content = { t('addPeople.copyInvite') } |
||||
position = 'top'> |
||||
<div |
||||
className = 'copy-invite-icon' |
||||
onClick = { _onCopyText }> |
||||
<Icon src = { IconCopy } /> |
||||
</div> |
||||
</Tooltip> |
||||
{renderEmailIcons()} |
||||
</div> |
||||
</div> |
||||
<div className = 'invite-more-dialog separator' /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default translate(InviteByEmailSection); |
@ -0,0 +1,501 @@ |
||||
// @flow
|
||||
|
||||
import InlineMessage from '@atlaskit/inline-message'; |
||||
import React from 'react'; |
||||
import type { Dispatch } from 'redux'; |
||||
|
||||
import { Avatar } from '../../../../base/avatar'; |
||||
import { translate, translateToHTML } from '../../../../base/i18n'; |
||||
import { Icon, IconPhone } from '../../../../base/icons'; |
||||
import { getLocalParticipant } from '../../../../base/participants'; |
||||
import { MultiSelectAutocomplete } from '../../../../base/react'; |
||||
import { connect } from '../../../../base/redux'; |
||||
|
||||
import AbstractAddPeopleDialog, { |
||||
type Props as AbstractProps, |
||||
type State, |
||||
_mapStateToProps as _abstractMapStateToProps |
||||
} from '../AbstractAddPeopleDialog'; |
||||
|
||||
declare var interfaceConfig: Object; |
||||
|
||||
type Props = AbstractProps & { |
||||
|
||||
/** |
||||
* The {@link JitsiMeetConference} which will be used to invite "room" participants. |
||||
*/ |
||||
_conference: Object, |
||||
|
||||
/** |
||||
* Whether to show a footer text after the search results as a last element. |
||||
*/ |
||||
_footerTextEnabled: boolean, |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Dispatch<any>, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function, |
||||
}; |
||||
|
||||
/** |
||||
* Form that enables inviting others to the call. |
||||
*/ |
||||
class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> { |
||||
_multiselect = null; |
||||
|
||||
_resourceClient: Object; |
||||
|
||||
state = { |
||||
addToCallError: false, |
||||
addToCallInProgress: false, |
||||
inviteItems: [] |
||||
}; |
||||
|
||||
/** |
||||
* Initializes a new {@code AddPeopleDialog} instance. |
||||
* |
||||
* @param {Object} 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._onClearItems = this._onClearItems.bind(this); |
||||
this._onItemSelected = this._onItemSelected.bind(this); |
||||
this._onSelectionChange = this._onSelectionChange.bind(this); |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
this._parseQueryResults = this._parseQueryResults.bind(this); |
||||
this._setMultiSelectElement = this._setMultiSelectElement.bind(this); |
||||
this._renderFooterText = this._renderFooterText.bind(this); |
||||
|
||||
this._resourceClient = { |
||||
makeQuery: this._query, |
||||
parseResults: this._parseQueryResults |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* React Component method that executes once component is updated. |
||||
* |
||||
* @param {Object} prevProps - The state object before the update. |
||||
* @param {Object} prevState - The state object before the update. |
||||
* @returns {void} |
||||
*/ |
||||
componentDidUpdate(prevProps, prevState) { |
||||
/** |
||||
* Clears selected items from the multi select component on successful |
||||
* invite. |
||||
*/ |
||||
if (prevState.addToCallError |
||||
&& !this.state.addToCallInProgress |
||||
&& !this.state.addToCallError |
||||
&& this._multiselect) { |
||||
this._multiselect.setSelectedItems([]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Renders the content of this component. |
||||
* |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
_addPeopleEnabled, |
||||
_dialOutEnabled, |
||||
t |
||||
} = this.props; |
||||
const footerText = this._renderFooterText(); |
||||
let isMultiSelectDisabled = this.state.addToCallInProgress; |
||||
let placeholder; |
||||
let loadingMessage; |
||||
let noMatches; |
||||
|
||||
if (_addPeopleEnabled && _dialOutEnabled) { |
||||
loadingMessage = 'addPeople.loading'; |
||||
noMatches = 'addPeople.noResults'; |
||||
placeholder = 'addPeople.searchPeopleAndNumbers'; |
||||
} else if (_addPeopleEnabled) { |
||||
loadingMessage = 'addPeople.loadingPeople'; |
||||
noMatches = 'addPeople.noResults'; |
||||
placeholder = 'addPeople.searchPeople'; |
||||
} else if (_dialOutEnabled) { |
||||
loadingMessage = 'addPeople.loadingNumber'; |
||||
noMatches = 'addPeople.noValidNumbers'; |
||||
placeholder = 'addPeople.searchNumbers'; |
||||
} else { |
||||
isMultiSelectDisabled = true; |
||||
noMatches = 'addPeople.noResults'; |
||||
placeholder = 'addPeople.disabled'; |
||||
} |
||||
|
||||
return ( |
||||
<div className = 'add-people-form-wrap'> |
||||
{ this._renderErrorMessage() } |
||||
<MultiSelectAutocomplete |
||||
footer = { footerText } |
||||
isDisabled = { isMultiSelectDisabled } |
||||
loadingMessage = { t(loadingMessage) } |
||||
noMatchesFound = { t(noMatches) } |
||||
onItemSelected = { this._onItemSelected } |
||||
onSelectionChange = { this._onSelectionChange } |
||||
placeholder = { t(placeholder) } |
||||
ref = { this._setMultiSelectElement } |
||||
resourceClient = { this._resourceClient } |
||||
shouldFitContainer = { true } |
||||
shouldFocus = { true } /> |
||||
{ this._renderFormActions() } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
_invite: Array<Object> => Promise<*> |
||||
|
||||
_isAddDisabled: () => boolean; |
||||
|
||||
_onItemSelected: (Object) => Object; |
||||
|
||||
/** |
||||
* Callback invoked when a selection has been made but before it has been |
||||
* set as selected. |
||||
* |
||||
* @param {Object} item - The item that has just been selected. |
||||
* @private |
||||
* @returns {Object} The item to display as selected in the input. |
||||
*/ |
||||
_onItemSelected(item) { |
||||
if (item.item.type === 'phone') { |
||||
item.content = item.item.number; |
||||
} |
||||
|
||||
return item; |
||||
} |
||||
|
||||
_onSelectionChange: (Map<*, *>) => void; |
||||
|
||||
/** |
||||
* Handles a selection change. |
||||
* |
||||
* @param {Array} selectedItems - The list of selected items. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSelectionChange(selectedItems) { |
||||
this.setState({ |
||||
inviteItems: selectedItems |
||||
}); |
||||
} |
||||
|
||||
_onSubmit: () => void; |
||||
|
||||
/** |
||||
* Submits the selection for inviting. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSubmit() { |
||||
const { inviteItems } = this.state; |
||||
const invitees = inviteItems.map(({ item }) => item); |
||||
|
||||
this._invite(invitees) |
||||
.then(invitesLeftToSend => { |
||||
if (invitesLeftToSend.length) { |
||||
const unsentInviteIDs |
||||
= invitesLeftToSend.map(invitee => |
||||
invitee.id || invitee.user_id || invitee.number); |
||||
const itemsToSelect |
||||
= inviteItems.filter(({ item }) => |
||||
unsentInviteIDs.includes(item.id || item.user_id || item.number)); |
||||
|
||||
if (this._multiselect) { |
||||
this._multiselect.setSelectedItems(itemsToSelect); |
||||
} |
||||
} else { |
||||
// Do nothing.
|
||||
} |
||||
}); |
||||
} |
||||
|
||||
_parseQueryResults: (?Array<Object>) => Array<Object>; |
||||
|
||||
/** |
||||
* Returns the avatar component for a user. |
||||
* |
||||
* @param {Object} user - The user. |
||||
* @param {string} className - The CSS class for the avatar component. |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_getAvatar(user, className = 'avatar-small') { |
||||
return ( |
||||
<Avatar |
||||
className = { className } |
||||
status = { user.status } |
||||
url = { user.avatar } /> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Processes results from requesting available numbers and people by munging |
||||
* each result into a format {@code MultiSelectAutocomplete} can use for |
||||
* display. |
||||
* |
||||
* @param {Array} response - The response object from the server for the |
||||
* query. |
||||
* @private |
||||
* @returns {Object[]} Configuration objects for items to display in the |
||||
* search autocomplete. |
||||
*/ |
||||
_parseQueryResults(response = []) { |
||||
const { t, _dialOutEnabled } = this.props; |
||||
const users = response.filter(item => item.type !== 'phone'); |
||||
const userDisplayItems = []; |
||||
|
||||
for (const user of users) { |
||||
const { name, phone } = user; |
||||
const tagAvatar = this._getAvatar(user, 'avatar-xsmall'); |
||||
const elemAvatar = this._getAvatar(user); |
||||
|
||||
userDisplayItems.push({ |
||||
content: name, |
||||
elemBefore: elemAvatar, |
||||
item: user, |
||||
tag: { |
||||
elemBefore: tagAvatar |
||||
}, |
||||
value: user.id || user.user_id |
||||
}); |
||||
|
||||
if (phone && _dialOutEnabled) { |
||||
userDisplayItems.push({ |
||||
filterValues: [ name, phone ], |
||||
content: `${phone} (${name})`, |
||||
elemBefore: elemAvatar, |
||||
item: { |
||||
type: 'phone', |
||||
number: phone |
||||
}, |
||||
tag: { |
||||
elemBefore: tagAvatar |
||||
}, |
||||
value: phone |
||||
}); |
||||
} |
||||
} |
||||
|
||||
const numbers = response.filter(item => item.type === 'phone'); |
||||
const telephoneIcon = this._renderTelephoneIcon(); |
||||
|
||||
const numberDisplayItems = numbers.map(number => { |
||||
const numberNotAllowedMessage |
||||
= number.allowed ? '' : t('addPeople.countryNotSupported'); |
||||
const countryCodeReminder = number.showCountryCodeReminder |
||||
? t('addPeople.countryReminder') : ''; |
||||
const description |
||||
= `${numberNotAllowedMessage} ${countryCodeReminder}`.trim(); |
||||
|
||||
return { |
||||
filterValues: [ |
||||
number.originalEntry, |
||||
number.number |
||||
], |
||||
content: t('addPeople.telephone', { number: number.number }), |
||||
description, |
||||
isDisabled: !number.allowed, |
||||
elemBefore: telephoneIcon, |
||||
item: number, |
||||
tag: { |
||||
elemBefore: telephoneIcon |
||||
}, |
||||
value: number.number |
||||
}; |
||||
}); |
||||
|
||||
return [ |
||||
...userDisplayItems, |
||||
...numberDisplayItems |
||||
]; |
||||
} |
||||
|
||||
_query: (string) => Promise<Array<Object>>; |
||||
|
||||
_renderFooterText: () => Object; |
||||
|
||||
/** |
||||
* Sets up the rendering of the footer text, if enabled. |
||||
* |
||||
* @returns {Object | undefined} |
||||
*/ |
||||
_renderFooterText() { |
||||
const { _footerTextEnabled, t } = this.props; |
||||
let footerText; |
||||
|
||||
if (_footerTextEnabled) { |
||||
footerText = { |
||||
content: <div className = 'footer-text-wrap'> |
||||
<div> |
||||
<span className = 'footer-telephone-icon'> |
||||
<Icon src = { IconPhone } /> |
||||
</span> |
||||
</div> |
||||
{ translateToHTML(t, 'addPeople.footerText') } |
||||
</div> |
||||
}; |
||||
} |
||||
|
||||
return footerText; |
||||
} |
||||
|
||||
_onClearItems: () => void; |
||||
|
||||
/** |
||||
* Clears the selected items from state and form. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onClearItems() { |
||||
if (this._multiselect) { |
||||
this._multiselect.setSelectedItems([]); |
||||
} |
||||
this.setState({ inviteItems: [] }); |
||||
} |
||||
|
||||
/** |
||||
* Renders the add/cancel actions for the form. |
||||
* |
||||
* @returns {ReactElement|null} |
||||
*/ |
||||
_renderFormActions() { |
||||
const { inviteItems } = this.state; |
||||
const { t } = this.props; |
||||
|
||||
if (!inviteItems.length) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div className = 'invite-more-dialog invite-buttons'> |
||||
<a |
||||
className = 'invite-more-dialog invite-buttons-cancel' |
||||
onClick = { this._onClearItems }> |
||||
{t('dialog.Cancel')} |
||||
</a> |
||||
<a |
||||
className = 'invite-more-dialog invite-buttons-add' |
||||
onClick = { this._onSubmit }> |
||||
{t('addPeople.add')} |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the error message if the add doesn't succeed. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement|null} |
||||
*/ |
||||
_renderErrorMessage() { |
||||
if (!this.state.addToCallError) { |
||||
return null; |
||||
} |
||||
|
||||
const { t } = this.props; |
||||
const supportString = t('inlineDialogFailure.supportMsg'); |
||||
const supportLink = interfaceConfig.SUPPORT_URL; |
||||
|
||||
if (!supportLink) { |
||||
return null; |
||||
} |
||||
|
||||
const supportLinkContent = ( |
||||
<span> |
||||
<span> |
||||
{ supportString.padEnd(supportString.length + 1) } |
||||
</span> |
||||
<span> |
||||
<a |
||||
href = { supportLink } |
||||
rel = 'noopener noreferrer' |
||||
target = '_blank'> |
||||
{ t('inlineDialogFailure.support') } |
||||
</a> |
||||
</span> |
||||
<span>.</span> |
||||
</span> |
||||
); |
||||
|
||||
return ( |
||||
<div className = 'modal-dialog-form-error'> |
||||
<InlineMessage |
||||
title = { t('addPeople.failedToAdd') } |
||||
type = 'error'> |
||||
{ supportLinkContent } |
||||
</InlineMessage> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders a telephone icon. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_renderTelephoneIcon() { |
||||
return ( |
||||
<span className = 'add-telephone-icon'> |
||||
<Icon src = { IconPhone } /> |
||||
</span> |
||||
); |
||||
} |
||||
|
||||
_setMultiSelectElement: (React$ElementRef<*> | null) => void; |
||||
|
||||
/** |
||||
* Sets the instance variable for the multi select component |
||||
* element so it can be accessed directly. |
||||
* |
||||
* @param {Object} element - The DOM element for the component's dialog. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_setMultiSelectElement(element) { |
||||
this._multiselect = element; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated |
||||
* {@code AddPeopleDialog}'s props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { enableFeaturesBasedOnToken } = state['features/base/config']; |
||||
let footerTextEnabled = false; |
||||
|
||||
if (enableFeaturesBasedOnToken) { |
||||
const { features = {} } = getLocalParticipant(state); |
||||
|
||||
if (String(features['outbound-call']) !== 'true') { |
||||
footerTextEnabled = true; |
||||
} |
||||
} |
||||
|
||||
return { |
||||
..._abstractMapStateToProps(state), |
||||
_footerTextEnabled: footerTextEnabled |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(InviteContactsForm)); |
@ -0,0 +1,32 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
|
||||
import InviteContactsForm from './InviteContactsForm'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Component that represents the invitation section of the {@code AddPeopleDialog}. |
||||
* |
||||
* @returns {ReactElement$<any>} |
||||
*/ |
||||
function InviteContactsSection({ t }: Props) { |
||||
return ( |
||||
<> |
||||
<span>{t('addPeople.addContacts')}</span> |
||||
<InviteContactsForm /> |
||||
<div className = 'invite-more-dialog separator' /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default translate(InviteContactsSection); |
@ -0,0 +1,111 @@ |
||||
// @flow
|
||||
|
||||
import React, { useState } from 'react'; |
||||
|
||||
import { translate } from '../../../../base/i18n'; |
||||
import { Icon, IconCheck, IconCopy } from '../../../../base/icons'; |
||||
|
||||
import { copyText } from './utils'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The current known URL for a live stream in progress. |
||||
*/ |
||||
liveStreamViewURL: string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
} |
||||
|
||||
/** |
||||
* Section of the {@code AddPeopleDialog} that renders the |
||||
* live streaming url, allowing a copy action. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function LiveStreamSection({ liveStreamViewURL, t }: Props) { |
||||
const [ isClicked, setIsClicked ] = useState(false); |
||||
const [ isHovered, setIsHovered ] = useState(false); |
||||
|
||||
/** |
||||
* Click handler for the element. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onClick() { |
||||
setIsHovered(false); |
||||
if (copyText(liveStreamViewURL)) { |
||||
setIsClicked(true); |
||||
|
||||
setTimeout(() => { |
||||
setIsClicked(false); |
||||
}, 2500); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Hover handler for the element. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onHoverIn() { |
||||
if (!isClicked) { |
||||
setIsHovered(true); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Hover handler for the element. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onHoverOut() { |
||||
setIsHovered(false); |
||||
} |
||||
|
||||
/** |
||||
* Renders the content of the link based on the state. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function renderLinkContent() { |
||||
if (isClicked) { |
||||
return ( |
||||
<> |
||||
<div className = 'invite-more-dialog stream-text selected'> |
||||
{t('addPeople.linkCopied')} |
||||
</div> |
||||
<Icon src = { IconCheck } /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<div className = 'invite-more-dialog stream-text'> |
||||
{isHovered ? t('addPeople.copyStream') : liveStreamViewURL} |
||||
</div> |
||||
<Icon src = { IconCopy } /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<span>{t('addPeople.shareStream')}</span> |
||||
<div |
||||
className = { `invite-more-dialog stream${isClicked ? ' clicked' : ''}` } |
||||
onClick = { onClick } |
||||
onMouseOut = { onHoverOut } |
||||
onMouseOver = { onHoverIn }> |
||||
{ renderLinkContent() } |
||||
</div> |
||||
<div className = 'invite-more-dialog separator' /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default translate(LiveStreamSection); |
@ -1,3 +1,4 @@ |
||||
// @flow
|
||||
|
||||
export { default as AddPeopleDialog } from './AddPeopleDialog'; |
||||
export * from './utils'; |
||||
|
@ -0,0 +1,23 @@ |
||||
// @flow
|
||||
|
||||
/** |
||||
* Tries to copy a given text to the clipboard. |
||||
* |
||||
* @param {string} textToCopy - Text to be copied. |
||||
* @returns {boolean} |
||||
*/ |
||||
export function copyText(textToCopy: string) { |
||||
const fakeTextArea = document.createElement('textarea'); |
||||
|
||||
// $FlowFixMe
|
||||
document.body.appendChild(fakeTextArea); |
||||
fakeTextArea.value = textToCopy; |
||||
fakeTextArea.select(); |
||||
|
||||
const result = document.execCommand('copy'); |
||||
|
||||
// $FlowFixMe
|
||||
document.body.removeChild(fakeTextArea); |
||||
|
||||
return result; |
||||
} |
@ -1,3 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export * from './web'; |
@ -1,644 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
import type { Dispatch } from 'redux'; |
||||
|
||||
import { setPassword } from '../../../../base/conference'; |
||||
import { getInviteURL } from '../../../../base/connection'; |
||||
import { Dialog } from '../../../../base/dialog'; |
||||
import { translate } from '../../../../base/i18n'; |
||||
import { Icon, IconInfo, IconCopy } from '../../../../base/icons'; |
||||
import { connect } from '../../../../base/redux'; |
||||
import { |
||||
isLocalParticipantModerator, |
||||
getLocalParticipant |
||||
} from '../../../../base/participants'; |
||||
|
||||
import { |
||||
_decodeRoomURI, |
||||
_getDefaultPhoneNumber, |
||||
getDialInfoPageURL, |
||||
shouldDisplayDialIn |
||||
} from '../../../functions'; |
||||
import logger from '../../../logger'; |
||||
import DialInNumber from './DialInNumber'; |
||||
import PasswordForm from './PasswordForm'; |
||||
|
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link InfoDialog}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Whether or not the current user can modify the current password. |
||||
*/ |
||||
_canEditPassword: boolean, |
||||
|
||||
/** |
||||
* The JitsiConference for which to display a lock state and change the |
||||
* password. |
||||
*/ |
||||
_conference: Object, |
||||
|
||||
/** |
||||
* The name of the current conference. Used as part of inviting users. |
||||
*/ |
||||
_conferenceName: string, |
||||
|
||||
/** |
||||
* The number of digits to be used in the password. |
||||
*/ |
||||
_passwordNumberOfDigits: ?number, |
||||
|
||||
/** |
||||
* The current url of the conference to be copied onto the clipboard. |
||||
*/ |
||||
_inviteURL: string, |
||||
|
||||
/** |
||||
* The redux representation of the local participant. |
||||
*/ |
||||
_localParticipantName: ?string, |
||||
|
||||
/** |
||||
* The current location url of the conference. |
||||
*/ |
||||
_locationURL: Object, |
||||
|
||||
/** |
||||
* The value for how the conference is locked (or undefined if not locked) |
||||
* as defined by room-lock constants. |
||||
*/ |
||||
_locked: string, |
||||
|
||||
/** |
||||
* The current known password for the JitsiConference. |
||||
*/ |
||||
_password: string, |
||||
|
||||
/** |
||||
* The object representing the dialIn feature. |
||||
*/ |
||||
dialIn: Object, |
||||
|
||||
/** |
||||
* Invoked to open a dialog for adding participants to the conference. |
||||
*/ |
||||
dispatch: Dispatch<any>, |
||||
|
||||
/** |
||||
* Whether is Atlaskit InlineDialog or a normal dialog. |
||||
*/ |
||||
isInlineDialog: boolean, |
||||
|
||||
/** |
||||
* The current known URL for a live stream in progress. |
||||
*/ |
||||
liveStreamViewURL: string, |
||||
|
||||
/** |
||||
* Callback invoked when the dialog should be closed. |
||||
*/ |
||||
onClose: Function, |
||||
|
||||
/** |
||||
* Callback invoked when a mouse-related event has been detected. |
||||
*/ |
||||
onMouseOver: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of {@link InfoDialog}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* Whether or not to show the password in editing mode. |
||||
*/ |
||||
passwordEditEnabled: boolean, |
||||
|
||||
/** |
||||
* The conference dial-in number to display. |
||||
*/ |
||||
phoneNumber: ?string |
||||
}; |
||||
|
||||
/** |
||||
* A React Component with the contents for a dialog that shows information about |
||||
* the current conference. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class InfoDialog extends Component<Props, State> { |
||||
_copyElement: ?Object; |
||||
_copyUrlElement: ?Object; |
||||
|
||||
/** |
||||
* Implements React's {@link Component#getDerivedStateFromProps()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
static getDerivedStateFromProps(props, state) { |
||||
let phoneNumber = state.phoneNumber; |
||||
|
||||
if (!state.phoneNumber && props.dialIn.numbers) { |
||||
phoneNumber = _getDefaultPhoneNumber(props.dialIn.numbers); |
||||
} |
||||
|
||||
return { |
||||
// Exit edit mode when a password is set locally or remotely.
|
||||
passwordEditEnabled: state.passwordEditEnabled && props._password |
||||
? false : state.passwordEditEnabled, |
||||
phoneNumber |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* {@code InfoDialog} component's local state. |
||||
* |
||||
* @type {Object} |
||||
* @property {boolean} passwordEditEnabled - Whether or not to show the |
||||
* {@code PasswordForm} in its editing state. |
||||
* @property {string} phoneNumber - The number to display for dialing into |
||||
* the conference. |
||||
*/ |
||||
state = { |
||||
passwordEditEnabled: false, |
||||
phoneNumber: undefined |
||||
}; |
||||
|
||||
/** |
||||
* Initializes new {@code InfoDialog} instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
if (props.dialIn && props.dialIn.numbers) { |
||||
this.state.phoneNumber |
||||
= _getDefaultPhoneNumber(props.dialIn.numbers); |
||||
} |
||||
|
||||
/** |
||||
* The internal reference to the DOM/HTML element backing the React |
||||
* {@code Component} text area. It is necessary for the implementation |
||||
* of copying to the clipboard. |
||||
* |
||||
* @private |
||||
* @type {HTMLTextAreaElement} |
||||
*/ |
||||
this._copyElement = null; |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onClickURLText = this._onClickURLText.bind(this); |
||||
this._onCopyInviteInfo = this._onCopyInviteInfo.bind(this); |
||||
this._onCopyInviteUrl = this._onCopyInviteUrl.bind(this); |
||||
this._onPasswordRemove = this._onPasswordRemove.bind(this); |
||||
this._onPasswordSubmit = this._onPasswordSubmit.bind(this); |
||||
this._onTogglePasswordEditState |
||||
= this._onTogglePasswordEditState.bind(this); |
||||
this._setCopyElement = this._setCopyElement.bind(this); |
||||
this._setCopyUrlElement = this._setCopyUrlElement.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
isInlineDialog, |
||||
liveStreamViewURL, |
||||
onMouseOver, |
||||
t |
||||
} = this.props; |
||||
|
||||
const inlineDialog = ( |
||||
<div |
||||
className = 'info-dialog' |
||||
onMouseOver = { onMouseOver } > |
||||
<div className = 'info-dialog-column'> |
||||
<h4 className = 'info-dialog-icon'> |
||||
<Icon src = { IconInfo } /> |
||||
</h4> |
||||
</div> |
||||
<div className = 'info-dialog-column'> |
||||
<div className = 'info-dialog-title'> |
||||
{ t('info.title') } |
||||
</div> |
||||
<div className = 'info-dialog-conference-url'> |
||||
<span className = 'info-label'> |
||||
{ t('info.conferenceURL') } |
||||
</span> |
||||
<span className = 'spacer'> </span> |
||||
<span className = 'info-value'> |
||||
<a |
||||
className = 'info-dialog-url-text info-dialog-url-text-unselectable' |
||||
href = { this.props._inviteURL } |
||||
onClick = { this._onClickURLText } > |
||||
{ decodeURI(this._getURLToDisplay()) } |
||||
</a> |
||||
</span> |
||||
<span className = 'info-dialog-url-icon'> |
||||
<Icon |
||||
onClick = { this._onCopyInviteUrl } |
||||
size = { 18 } |
||||
src = { IconCopy } /> |
||||
</span> |
||||
</div> |
||||
<div className = 'info-dialog-dial-in'> |
||||
{ this._renderDialInDisplay() } |
||||
</div> |
||||
{ liveStreamViewURL && this._renderLiveStreamURL() } |
||||
<div className = 'info-dialog-password'> |
||||
<PasswordForm |
||||
editEnabled = { this.state.passwordEditEnabled } |
||||
locked = { this.props._locked } |
||||
onSubmit = { this._onPasswordSubmit } |
||||
password = { this.props._password } |
||||
passwordNumberOfDigits = { this.props._passwordNumberOfDigits } /> |
||||
</div> |
||||
<div className = 'info-dialog-action-links'> |
||||
<div className = 'info-dialog-action-link'> |
||||
<a |
||||
className = 'info-copy' |
||||
onClick = { this._onCopyInviteInfo }> |
||||
{ t('dialog.copy') } |
||||
</a> |
||||
</div> |
||||
{ this._renderPasswordAction() } |
||||
</div> |
||||
</div> |
||||
<textarea |
||||
className = 'info-dialog-copy-element' |
||||
readOnly = { true } |
||||
ref = { this._setCopyElement } |
||||
tabIndex = '-1' |
||||
value = { this._getTextToCopy() } /> |
||||
<textarea |
||||
className = 'info-dialog-copy-element' |
||||
readOnly = { true } |
||||
ref = { this._setCopyUrlElement } |
||||
tabIndex = '-1' |
||||
value = { this.props._inviteURL } /> |
||||
</div> |
||||
); |
||||
|
||||
if (isInlineDialog) { |
||||
return inlineDialog; |
||||
} |
||||
|
||||
return ( |
||||
<Dialog |
||||
cancelTitleKey = 'dialog.close' |
||||
submitDisabled = { true } |
||||
titleKey = 'info.label' |
||||
width = 'small'> |
||||
{ inlineDialog } |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Creates a message describing how to dial in to the conference. |
||||
* |
||||
* @private |
||||
* @returns {string} |
||||
*/ |
||||
_getTextToCopy() { |
||||
const { _localParticipantName, liveStreamViewURL, t } = this.props; |
||||
const _inviteURL = _decodeRoomURI(this.props._inviteURL); |
||||
|
||||
let invite = _localParticipantName |
||||
? t('info.inviteURLFirstPartPersonal', { name: _localParticipantName }) |
||||
: t('info.inviteURLFirstPartGeneral'); |
||||
|
||||
invite += t('info.inviteURLSecondPart', { |
||||
url: _inviteURL |
||||
}); |
||||
|
||||
if (liveStreamViewURL) { |
||||
const liveStream = t('info.inviteLiveStream', { |
||||
url: liveStreamViewURL |
||||
}); |
||||
|
||||
invite = `${invite}\n${liveStream}`; |
||||
} |
||||
|
||||
if (shouldDisplayDialIn(this.props.dialIn)) { |
||||
const dial = t('info.invitePhone', { |
||||
number: this.state.phoneNumber, |
||||
conferenceID: this.props.dialIn.conferenceID |
||||
}); |
||||
const moreNumbers = t('info.invitePhoneAlternatives', { |
||||
url: getDialInfoPageURL( |
||||
this.props._conferenceName, |
||||
this.props._locationURL |
||||
), |
||||
silentUrl: `${_inviteURL}#config.startSilent=true` |
||||
}); |
||||
|
||||
invite = `${invite}\n${dial}\n${moreNumbers}`; |
||||
} |
||||
|
||||
return invite; |
||||
} |
||||
|
||||
/** |
||||
* Modifies the inviteURL for display in the modal. |
||||
* |
||||
* @private |
||||
* @returns {string} |
||||
*/ |
||||
_getURLToDisplay() { |
||||
return this.props._inviteURL.replace(/^https?:\/\//i, ''); |
||||
} |
||||
|
||||
_onClickURLText: (Object) => void; |
||||
|
||||
/** |
||||
* Callback invoked when a displayed URL link is clicked to prevent actual |
||||
* navigation from happening. The URL links have an href to display the |
||||
* action "Copy Link Address" in the context menu but otherwise it should |
||||
* not behave like links. |
||||
* |
||||
* @param {Object} event - The click event from clicking on the link. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onClickURLText(event) { |
||||
event.preventDefault(); |
||||
} |
||||
|
||||
_onCopyInviteInfo: () => void; |
||||
|
||||
/** |
||||
* Callback invoked to copy the contents of {@code this._copyElement} to the |
||||
* clipboard. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onCopyInviteInfo() { |
||||
try { |
||||
if (!this._copyElement) { |
||||
throw new Error('No element to copy from.'); |
||||
} |
||||
|
||||
this._copyElement && this._copyElement.select(); |
||||
document.execCommand('copy'); |
||||
this._copyElement && this._copyElement.blur(); |
||||
} catch (err) { |
||||
logger.error('error when copying the text', err); |
||||
} |
||||
} |
||||
|
||||
_onCopyInviteUrl: () => void; |
||||
|
||||
/** |
||||
* Callback invoked to copy the contents of {@code this._copyUrlElement} to the clipboard. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onCopyInviteUrl() { |
||||
try { |
||||
if (!this._copyUrlElement) { |
||||
throw new Error('No element to copy from.'); |
||||
} |
||||
|
||||
this._copyUrlElement && this._copyUrlElement.select(); |
||||
document.execCommand('copy'); |
||||
this._copyUrlElement && this._copyUrlElement.blur(); |
||||
} catch (err) { |
||||
logger.error('error when copying the text', err); |
||||
} |
||||
} |
||||
|
||||
_onPasswordRemove: () => void; |
||||
|
||||
/** |
||||
* Callback invoked to unlock the current JitsiConference. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onPasswordRemove() { |
||||
this._onPasswordSubmit(''); |
||||
} |
||||
|
||||
_onPasswordSubmit: (string) => void; |
||||
|
||||
/** |
||||
* Callback invoked to set a password on the current JitsiConference. |
||||
* |
||||
* @param {string} enteredPassword - The new password to be used to lock the |
||||
* current JitsiConference. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onPasswordSubmit(enteredPassword) { |
||||
const { _conference } = this.props; |
||||
|
||||
this.props.dispatch(setPassword( |
||||
_conference, |
||||
_conference.lock, |
||||
enteredPassword |
||||
)); |
||||
} |
||||
|
||||
_onTogglePasswordEditState: () => void; |
||||
|
||||
/** |
||||
* Toggles whether or not the password should currently be shown as being |
||||
* edited locally. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onTogglePasswordEditState() { |
||||
this.setState({ |
||||
passwordEditEnabled: !this.state.passwordEditEnabled |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Returns a ReactElement for showing how to dial into the conference, if |
||||
* dialing in is available. |
||||
* |
||||
* @private |
||||
* @returns {null|ReactElement} |
||||
*/ |
||||
_renderDialInDisplay() { |
||||
if (!shouldDisplayDialIn(this.props.dialIn)) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div> |
||||
<DialInNumber |
||||
conferenceID = { this.props.dialIn.conferenceID } |
||||
phoneNumber = { this.state.phoneNumber } /> |
||||
<a |
||||
className = 'more-numbers' |
||||
href = { |
||||
getDialInfoPageURL( |
||||
this.props._conferenceName, |
||||
this.props._locationURL |
||||
) |
||||
} |
||||
rel = 'noopener noreferrer' |
||||
target = '_blank'> |
||||
{ this.props.t('info.moreNumbers') } |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Returns a ReactElement for interacting with the password field. |
||||
* |
||||
* @private |
||||
* @returns {null|ReactElement} |
||||
*/ |
||||
_renderPasswordAction() { |
||||
const { t } = this.props; |
||||
let className, onClick, textKey; |
||||
|
||||
|
||||
if (!this.props._canEditPassword) { |
||||
// intentionally left blank to prevent rendering anything
|
||||
} else if (this.state.passwordEditEnabled) { |
||||
className = 'cancel-password'; |
||||
onClick = this._onTogglePasswordEditState; |
||||
textKey = 'info.cancelPassword'; |
||||
} else if (this.props._locked) { |
||||
className = 'remove-password'; |
||||
onClick = this._onPasswordRemove; |
||||
textKey = 'dialog.removePassword'; |
||||
} else { |
||||
className = 'add-password'; |
||||
onClick = this._onTogglePasswordEditState; |
||||
textKey = 'info.addPassword'; |
||||
} |
||||
|
||||
return className && onClick && textKey |
||||
? <div className = 'info-dialog-action-link'> |
||||
<a |
||||
className = { className } |
||||
onClick = { onClick }> |
||||
{ t(textKey) } |
||||
</a> |
||||
</div> |
||||
: null; |
||||
} |
||||
|
||||
/** |
||||
* Returns a ReactElement for display a link to the current url of a |
||||
* live stream in progress. |
||||
* |
||||
* @private |
||||
* @returns {null|ReactElement} |
||||
*/ |
||||
_renderLiveStreamURL() { |
||||
const { liveStreamViewURL, t } = this.props; |
||||
|
||||
return ( |
||||
<div className = 'info-dialog-live-stream-url'> |
||||
<span className = 'info-label'> |
||||
{ t('info.liveStreamURL') } |
||||
</span> |
||||
<span className = 'spacer'> </span> |
||||
<span className = 'info-value'> |
||||
<a |
||||
className = 'info-dialog-url-text' |
||||
href = { liveStreamViewURL } |
||||
onClick = { this._onClickURLText } > |
||||
{ liveStreamViewURL } |
||||
</a> |
||||
</span> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
_setCopyElement: () => void; |
||||
|
||||
/** |
||||
* Sets the internal reference to the DOM/HTML element backing the React |
||||
* {@code Component} input. |
||||
* |
||||
* @param {HTMLInputElement} element - The DOM/HTML element for this |
||||
* {@code Component}'s input. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_setCopyElement(element: Object) { |
||||
this._copyElement = element; |
||||
} |
||||
|
||||
_setCopyUrlElement: () => void; |
||||
|
||||
/** |
||||
* Sets the internal reference to the DOM/HTML element backing the React |
||||
* {@code Component} input. |
||||
* |
||||
* @param {HTMLInputElement} element - The DOM/HTML element for this |
||||
* {@code Component}'s input. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_setCopyUrlElement(element: Object) { |
||||
this._copyUrlElement = element; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated props for the |
||||
* {@code InfoDialog} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _canEditPassword: boolean, |
||||
* _conference: Object, |
||||
* _conferenceName: string, |
||||
* _inviteURL: string, |
||||
* _localParticipantName: ?string, |
||||
* _locationURL: string, |
||||
* _locked: string, |
||||
* _password: string |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { |
||||
conference, |
||||
locked, |
||||
password, |
||||
room |
||||
} = state['features/base/conference']; |
||||
const localParticipant = getLocalParticipant(state); |
||||
|
||||
return { |
||||
_canEditPassword: isLocalParticipantModerator(state, state['features/base/config'].lockRoomGuestEnabled), |
||||
_conference: conference, |
||||
_conferenceName: room, |
||||
_passwordNumberOfDigits: state['features/base/config'].roomPasswordNumberOfDigits, |
||||
_inviteURL: getInviteURL(state), |
||||
_localParticipantName: localParticipant?.name, |
||||
_locationURL: state['features/base/connection'].locationURL, |
||||
_locked: locked, |
||||
_password: password |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(InfoDialog)); |
@ -1,268 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import InlineDialog from '@atlaskit/inline-dialog'; |
||||
import React, { Component } from 'react'; |
||||
import type { Dispatch } from 'redux'; |
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../../../analytics'; |
||||
import { openDialog } from '../../../../base/dialog'; |
||||
import { translate } from '../../../../base/i18n'; |
||||
import { IconInfo } from '../../../../base/icons'; |
||||
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet'; |
||||
import { getParticipantCount } from '../../../../base/participants'; |
||||
import { OverflowMenuItem } from '../../../../base/toolbox'; |
||||
import { connect } from '../../../../base/redux'; |
||||
import { getActiveSession } from '../../../../recording'; |
||||
import { ToolbarButton } from '../../../../toolbox'; |
||||
import { updateDialInNumbers } from '../../../actions'; |
||||
|
||||
import InfoDialog from './InfoDialog'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link InfoDialogButton}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The redux state representing the dial-in numbers feature. |
||||
*/ |
||||
_dialIn: Object, |
||||
|
||||
/** |
||||
* Whether or not the {@code InfoDialog} should display automatically when |
||||
* in a lonely call. |
||||
*/ |
||||
_disableAutoShow: boolean, |
||||
|
||||
/** |
||||
* Whether or not the local participant has joined a |
||||
* {@code JitsiConference}. Used to trigger auto showing of the |
||||
* {@code InfoDialog}. |
||||
*/ |
||||
_isConferenceJoined: Boolean, |
||||
|
||||
/** |
||||
* The URL for a currently active live broadcast |
||||
*/ |
||||
_liveStreamViewURL: ?string, |
||||
|
||||
/** |
||||
* True if the number of real participants in the call is less than 2. If in a lonely call, the |
||||
* {@code InfoDialog} will be automatically shown. |
||||
*/ |
||||
_isLonelyCall: boolean, |
||||
|
||||
/** |
||||
* Whether or not the toolbox, in which this component exists, is visible. |
||||
*/ |
||||
_toolboxVisible: boolean, |
||||
|
||||
/** |
||||
* Invoked to toggle display of the info dialog. |
||||
*/ |
||||
dispatch: Dispatch<any>, |
||||
|
||||
/** |
||||
* Whether to show the label or not. |
||||
*/ |
||||
showLabel: boolean, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of {@link InfoDialogButton}. |
||||
*/ |
||||
type State = { |
||||
|
||||
/** |
||||
* Cache the conference connection state to derive when transitioning from |
||||
* not joined to join, in order to auto-show the InfoDialog. |
||||
*/ |
||||
hasConnectedToConference: boolean, |
||||
|
||||
/** |
||||
* Whether or not {@code InfoDialog} should be visible. |
||||
*/ |
||||
showDialog: boolean |
||||
}; |
||||
|
||||
/** |
||||
* A React Component for displaying a button which opens a dialog with |
||||
* information about the conference and with ways to invite people. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class InfoDialogButton extends Component<Props, State> { |
||||
/** |
||||
* Implements React's {@link Component#getDerivedStateFromProps()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
static getDerivedStateFromProps(props, state) { |
||||
return { |
||||
hasConnectedToConference: props._isConferenceJoined, |
||||
showDialog: (props._toolboxVisible && state.showDialog) |
||||
|| (!state.hasConnectedToConference |
||||
&& props._isConferenceJoined |
||||
&& props._isLonelyCall |
||||
&& props._toolboxVisible |
||||
&& !props._disableAutoShow) |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Initializes new {@code InfoDialogButton} instance. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
hasConnectedToConference: props._isConferenceJoined, |
||||
showDialog: false |
||||
}; |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onDialogClose = this._onDialogClose.bind(this); |
||||
this._onDialogToggle = this._onDialogToggle.bind(this); |
||||
this._onClickOverflowMenuButton |
||||
= this._onClickOverflowMenuButton.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Update dial-in numbers {@code InfoDialog}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentDidMount() { |
||||
if (!this.props._dialIn.numbers) { |
||||
this.props.dispatch(updateDialInNumbers()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { _dialIn, _liveStreamViewURL, showLabel, t } = this.props; |
||||
const { showDialog } = this.state; |
||||
|
||||
if (showLabel) { |
||||
return ( |
||||
<OverflowMenuItem |
||||
accessibilityLabel = { t('info.accessibilityLabel') } |
||||
icon = 'icon-info' |
||||
key = 'info-button' |
||||
onClick = { this._onClickOverflowMenuButton } |
||||
text = { t('info.label') } /> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className = 'toolbox-button-wth-dialog'> |
||||
<InlineDialog |
||||
content = { |
||||
<InfoDialog |
||||
dialIn = { _dialIn } |
||||
isInlineDialog = { true } |
||||
liveStreamViewURL = { _liveStreamViewURL } |
||||
onClose = { this._onDialogClose } /> } |
||||
isOpen = { showDialog } |
||||
onClose = { this._onDialogClose } |
||||
position = { 'top right' }> |
||||
<ToolbarButton |
||||
accessibilityLabel = { t('info.accessibilityLabel') } |
||||
icon = { IconInfo } |
||||
onClick = { this._onDialogToggle } |
||||
tooltip = { t('info.tooltip') } /> |
||||
</InlineDialog> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
_onDialogClose: () => void; |
||||
|
||||
/** |
||||
* Hides {@code InfoDialog}. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onDialogClose() { |
||||
this.setState({ showDialog: false }); |
||||
} |
||||
|
||||
_onClickOverflowMenuButton: () => void; |
||||
|
||||
/** |
||||
* Opens the Info dialog. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onClickOverflowMenuButton() { |
||||
const { _dialIn, _liveStreamViewURL } = this.props; |
||||
|
||||
this.props.dispatch(openDialog(InfoDialog, { |
||||
dialIn: _dialIn, |
||||
liveStreamViewURL: _liveStreamViewURL, |
||||
isInlineDialog: false |
||||
})); |
||||
} |
||||
|
||||
_onDialogToggle: () => void; |
||||
|
||||
/** |
||||
* Toggles the display of {@code InfoDialog}. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onDialogToggle() { |
||||
sendAnalytics(createToolbarEvent('info')); |
||||
|
||||
this.setState({ showDialog: !this.state.showDialog }); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated {@code InfoDialogButton} |
||||
* component's props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _dialIn: Object, |
||||
* _disableAutoShow: boolean, |
||||
* _isConferenceIsJoined: boolean, |
||||
* _liveStreamViewURL: string, |
||||
* _isLonelyCall: boolean, |
||||
* _toolboxVisible: boolean |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const currentLiveStreamingSession |
||||
= getActiveSession(state, JitsiRecordingConstants.mode.STREAM); |
||||
const { iAmRecorder, iAmSipGateway } = state['features/base/config']; |
||||
|
||||
return { |
||||
_dialIn: state['features/invite'], |
||||
_disableAutoShow: iAmRecorder || iAmSipGateway, |
||||
_isConferenceJoined: |
||||
Boolean(state['features/base/conference'].conference), |
||||
_liveStreamViewURL: |
||||
currentLiveStreamingSession |
||||
&& currentLiveStreamingSession.liveStreamViewURL, |
||||
_isLonelyCall: getParticipantCount(state) < 2, |
||||
_toolboxVisible: state['features/toolbox'].visible |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(InfoDialogButton)); |
@ -1,4 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export { default as InfoDialog } from './InfoDialog'; |
||||
export { default as InfoDialogButton } from './InfoDialogButton'; |
@ -0,0 +1,15 @@ |
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog'; |
||||
import { SecurityDialog } from './components/security-dialog'; |
||||
|
||||
/** |
||||
* Action that triggers opening the security options dialog. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function openSecurityDialog() { |
||||
return function(dispatch: (Object) => Object) { |
||||
dispatch(openDialog(SecurityDialog)); |
||||
}; |
||||
} |
@ -0,0 +1,3 @@ |
||||
// @flow
|
||||
|
||||
export * from './security-dialog'; |
@ -0,0 +1,38 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import { Icon, IconClose } from '../../../base/icons'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The {@link ModalDialog} closing function. |
||||
*/ |
||||
onClose: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Custom header of the {@code SecurityDialog}. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function Header({ onClose, t }: Props) { |
||||
return ( |
||||
<div |
||||
className = 'invite-more-dialog header'> |
||||
{ t('security.securityOptions') } |
||||
<Icon |
||||
onClick = { onClose } |
||||
src = { IconClose } /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export default translate(Header); |
@ -0,0 +1,190 @@ |
||||
/* eslint-disable react/no-multi-comp */ |
||||
// @flow
|
||||
|
||||
import React, { useRef } from 'react'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import { copyText } from '../../../invite'; |
||||
|
||||
import PasswordForm from './PasswordForm'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Whether or not the current user can modify the current password. |
||||
*/ |
||||
canEditPassword: boolean, |
||||
|
||||
/** |
||||
* The JitsiConference for which to display a lock state and change the |
||||
* password. |
||||
*/ |
||||
conference: Object, |
||||
|
||||
/** |
||||
* The value for how the conference is locked (or undefined if not locked) |
||||
* as defined by room-lock constants. |
||||
*/ |
||||
locked: string, |
||||
|
||||
/** |
||||
* The current known password for the JitsiConference. |
||||
*/ |
||||
password: string, |
||||
|
||||
/** |
||||
* Whether or not to show the password in editing mode. |
||||
*/ |
||||
passwordEditEnabled: boolean, |
||||
|
||||
/** |
||||
* The number of digits to be used in the password. |
||||
*/ |
||||
passwordNumberOfDigits: ?number, |
||||
|
||||
/** |
||||
* Action that sets the conference password. |
||||
*/ |
||||
setPassword: Function, |
||||
|
||||
/** |
||||
* Method that sets whether the password editing is enabled or not. |
||||
*/ |
||||
setPasswordEditEnabled: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Component that handles the password manipulation from the invite dialog. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function PasswordSection({ |
||||
canEditPassword, |
||||
conference, |
||||
locked, |
||||
password, |
||||
passwordEditEnabled, |
||||
passwordNumberOfDigits, |
||||
setPassword, |
||||
setPasswordEditEnabled, |
||||
t }: Props) { |
||||
|
||||
const formRef: Object = useRef(null); |
||||
|
||||
/** |
||||
* Callback invoked to set a password on the current JitsiConference. |
||||
* |
||||
* @param {string} enteredPassword - The new password to be used to lock the |
||||
* current JitsiConference. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
function onPasswordSubmit(enteredPassword) { |
||||
setPassword(conference, conference.lock, enteredPassword); |
||||
} |
||||
|
||||
/** |
||||
* Toggles whether or not the password should currently be shown as being |
||||
* edited locally. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
function onTogglePasswordEditState() { |
||||
setPasswordEditEnabled(!passwordEditEnabled); |
||||
} |
||||
|
||||
/** |
||||
* Method to remotely submit the password from outside of the password form. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onPasswordSave() { |
||||
if (formRef.current) { |
||||
formRef.current.querySelector('form').requestSubmit(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Callback invoked to unlock the current JitsiConference. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onPasswordRemove() { |
||||
onPasswordSubmit(''); |
||||
} |
||||
|
||||
/** |
||||
* Copies the password to the clipboard. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
function onPasswordCopy() { |
||||
copyText(password); |
||||
} |
||||
|
||||
/** |
||||
* Method that renders the password action(s) based on the current |
||||
* locked-status of the conference. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function renderPasswordActions() { |
||||
if (!canEditPassword) { |
||||
return null; |
||||
} |
||||
|
||||
if (passwordEditEnabled) { |
||||
return ( |
||||
<> |
||||
<a onClick = { onTogglePasswordEditState }>{ t('dialog.Cancel') }</a> |
||||
<a onClick = { onPasswordSave }>{ t('dialog.add') }</a> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
if (locked) { |
||||
return ( |
||||
<> |
||||
<a |
||||
className = 'remove-password' |
||||
onClick = { onPasswordRemove }>{ t('dialog.Remove') }</a> |
||||
<a |
||||
className = 'copy-password' |
||||
onClick = { onPasswordCopy }>{ t('dialog.copy') }</a> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<a |
||||
className = 'add-password' |
||||
onClick = { onTogglePasswordEditState }>{ t('info.addPassword') }</a> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className = 'security-dialog password'> |
||||
<div |
||||
className = 'info-dialog info-dialog-column info-dialog-password' |
||||
ref = { formRef }> |
||||
<PasswordForm |
||||
editEnabled = { passwordEditEnabled } |
||||
locked = { locked } |
||||
onSubmit = { onPasswordSubmit } |
||||
password = { password } |
||||
passwordNumberOfDigits = { passwordNumberOfDigits } /> |
||||
</div> |
||||
<div className = 'security-dialog password-actions'> |
||||
{ renderPasswordActions() } |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export default translate(PasswordSection); |
@ -0,0 +1,126 @@ |
||||
// @flow
|
||||
|
||||
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 Header from './Header'; |
||||
import PasswordSection from './PasswordSection'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Whether or not the current user can modify the current password. |
||||
*/ |
||||
_canEditPassword: boolean, |
||||
|
||||
/** |
||||
* The JitsiConference for which to display a lock state and change the |
||||
* password. |
||||
*/ |
||||
_conference: Object, |
||||
|
||||
/** |
||||
* The value for how the conference is locked (or undefined if not locked) |
||||
* as defined by room-lock constants. |
||||
*/ |
||||
_locked: string, |
||||
|
||||
/** |
||||
* The current known password for the JitsiConference. |
||||
*/ |
||||
_password: string, |
||||
|
||||
/** |
||||
* The number of digits to be used in the password. |
||||
*/ |
||||
_passwordNumberOfDigits: ?number, |
||||
|
||||
/** |
||||
* Action that sets the conference password. |
||||
*/ |
||||
setPassword: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Component that renders the security options dialog. |
||||
* |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
function SecurityDialog({ |
||||
_canEditPassword, |
||||
_conference, |
||||
_locked, |
||||
_password, |
||||
_passwordNumberOfDigits, |
||||
setPassword, |
||||
t |
||||
}: Props) { |
||||
const [ passwordEditEnabled, setPasswordEditEnabled ] = useState(false); |
||||
|
||||
useEffect(() => { |
||||
if (passwordEditEnabled && _password) { |
||||
setPasswordEditEnabled(false); |
||||
} |
||||
}, [ _password ]); |
||||
|
||||
return ( |
||||
<Dialog |
||||
customHeader = { Header } |
||||
hideCancelButton = { true } |
||||
submitDisabled = { true } |
||||
titleKey = 'security.securityOptions' |
||||
width = { 'small' }> |
||||
<div className = 'security-dialog'> |
||||
{ t('security.about') } |
||||
<div className = 'invite-more-dialog separator' /> |
||||
<PasswordSection |
||||
canEditPassword = { _canEditPassword } |
||||
conference = { _conference } |
||||
locked = { _locked } |
||||
password = { _password } |
||||
passwordEditEnabled = { passwordEditEnabled } |
||||
passwordNumberOfDigits = { _passwordNumberOfDigits } |
||||
setPassword = { setPassword } |
||||
setPasswordEditEnabled = { setPasswordEditEnabled } /> |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated props for the |
||||
* {@code SecurityDialog} component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function mapStateToProps(state) { |
||||
const { |
||||
conference, |
||||
locked, |
||||
password |
||||
} = state['features/base/conference']; |
||||
|
||||
return { |
||||
_canEditPassword: isLocalParticipantModerator(state, state['features/base/config'].lockRoomGuestEnabled), |
||||
_conference: conference, |
||||
_dialIn: state['features/invite'], |
||||
_locked: locked, |
||||
_password: password |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { setPassword: setPass }; |
||||
|
||||
export default translate(connect(mapStateToProps, mapDispatchToProps)(SecurityDialog)); |
@ -0,0 +1,83 @@ |
||||
// @flow
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../../analytics'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { IconLockPassword, IconUnlockPassword } from '../../../base/icons'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox'; |
||||
|
||||
import { openSecurityDialog } from '../../actions'; |
||||
|
||||
|
||||
type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* Whether the shared document is being edited or not. |
||||
*/ |
||||
_locked: boolean, |
||||
|
||||
/** |
||||
* On click handler that opens the security dialog. |
||||
*/ |
||||
onClick: Function |
||||
|
||||
}; |
||||
|
||||
|
||||
/** |
||||
* Implements an {@link AbstractButton} to open the security dialog. |
||||
*/ |
||||
class SecurityDialogButton extends AbstractButton<Props, *> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.security'; |
||||
icon = IconUnlockPassword; |
||||
label = 'toolbar.security'; |
||||
toggledIcon = IconLockPassword; |
||||
tooltip = 'toolbar.security'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and opens / closes the appropriate dialog. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
sendAnalytics(createToolbarEvent('toggle.security', { enable: !this.props._locked })); |
||||
this.props.onClick(); |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether this button is in toggled state or not. |
||||
* |
||||
* @override |
||||
* @returns {boolean} |
||||
*/ |
||||
_isToggled() { |
||||
return this.props._locked; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps part of the redux state to the component's props. |
||||
* |
||||
* @param {Object} state - The redux store/state. |
||||
* @returns {Props} |
||||
*/ |
||||
function mapStateToProps(state: Object) { |
||||
const { locked } = state['features/base/conference']; |
||||
|
||||
return { |
||||
_locked: locked |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Maps dispatching of some action to React component props. |
||||
* |
||||
* @param {Function} dispatch - Redux action dispatcher. |
||||
* @returns {Props} |
||||
*/ |
||||
const mapDispatchToProps = { |
||||
onClick: () => openSecurityDialog() |
||||
}; |
||||
|
||||
export default translate(connect(mapStateToProps, mapDispatchToProps)(SecurityDialogButton)); |
@ -0,0 +1,4 @@ |
||||
// @flow
|
||||
|
||||
export { default as SecurityDialog } from './SecurityDialog'; |
||||
export { default as SecurityDialogButton } from './SecurityDialogButton'; |
@ -0,0 +1,4 @@ |
||||
// @flow
|
||||
|
||||
export * from './actions'; |
||||
export * from './components'; |