feat(AddPeopleDialog): Update design; feat(SecurityDialog): Implement

This commit is contained in:
Mihai Uscat 2020-04-01 10:47:51 +03:00 committed by Saúl Ibarra Corretgé
parent 70d8fe91c3
commit 75c836c70c
52 changed files with 2255 additions and 1485 deletions

View File

@ -94,5 +94,7 @@ $flagsImagePath: "../images/";
@import 'prejoin';
@import 'prejoin-dialog';
@import 'country-picker';
@import 'modals/invite/invite_more';
@import 'modals/security/security';
/* Modules END */

View File

@ -3,6 +3,7 @@
*/
.modal-dialog-form {
.add-people-form-wrap {
margin-top: 8px;
.error {
padding-left: 5px;

View File

@ -3,47 +3,6 @@
display: flex;
font-size: 14px;
.info-dialog-action-link {
display: inline-block;
line-height: 1.5em;
a {
cursor: pointer;
vertical-align: middle;
}
}
.info-dialog-action-link:before {
color: $linkFontColor;
content: '\2022';
font-size: 1.5em;
padding: 0 10px;
vertical-align: middle;
}
.info-dialog-action-link:first-child:before {
content: '';
padding: 0;
}
.info-dialog-action-links {
font-weight: bold;
margin-top: 10px;
white-space: nowrap;
}
.info-dialog-action-separator {
display: inline-block;
}
.info-dialog-copy-element {
opacity: 0;
pointer-events: none;
position: absolute;
-webkit-user-select: text;
user-select: text;
}
.info-dialog-column {
margin-right: 10px;
overflow: hidden;
@ -56,52 +15,6 @@
}
}
.info-dialog-conference-url,
.info-dialog-live-stream-url {
width: max-content;
width: -moz-max-content;
width: -webkit-max-content;
word-break: break-all;
max-width: 400px;
display: flex;
align-items: center;
}
.info-dialog-dial-in {
word-break: break-all;
.conference-id,
.phone-number {
user-select: text;
}
}
.info-dialog-icon {
color: #6453C0;
font-size: 16px;
min-width: 30px;
}
.info-dialog-url-text,
.info-dialog-url-text:hover {
color: inherit;
cursor: inherit;
}
.info-dialog-url-icon {
display: inline-block;
margin-left: 5px;
svg {
cursor: pointer;
}
}
.info-dialog-title {
font-weight: bold;
margin-bottom: 10px;
}
.info-dialog-password,
.info-password,
.info-password-form {
@ -223,10 +136,4 @@
-moz-user-select: text;
-webkit-user-select: text;
}
.info-dialog-url-text-unselectable {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}

View File

@ -48,11 +48,11 @@ var interfaceConfig = {
*/
TOOLBAR_BUTTONS: [
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
'e2ee'
'e2ee', 'security'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

View File

@ -1,21 +1,36 @@
{
"addPeople": {
"add": "Invite",
"addContacts": "Invite your contacts",
"copyInvite": "Copy meeting invitation",
"copyLink": "Copy meeting link",
"copyStream": "Copy live streaming link",
"countryNotSupported": "We do not support this destination yet.",
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
"defaultEmail": "Your Default Email",
"disabled": "You can't invite people.",
"failedToAdd": "Failed to add participants",
"footerText": "Dialing out is disabled.",
"googleEmail": "Google Email",
"inviteMoreHeader": "You are the only one in the meeting",
"inviteMoreMailSubject": "Join {{appName}} meeting",
"inviteMorePrompt": "Invite more people",
"linkCopied": "Link copied to clipboard",
"loading": "Searching for people and phone numbers",
"loadingNumber": "Validating phone number",
"loadingPeople": "Searching for people to invite",
"noResults": "No matching search results",
"noValidNumbers": "Please enter a phone number",
"outlookEmail": "Outlook Email",
"searchNumbers": "Add phone numbers",
"searchPeople": "Search for people",
"searchPeopleAndNumbers": "Search for people or add their phone numbers",
"shareInvite": "Share meeting invitation",
"shareLink": "Share the meeting link to invite others",
"shareStream": "Share the live streaming link",
"telephone": "Telephone: {{number}}",
"title": "Invite people to this meeting"
"title": "Invite people to this meeting",
"yahooEmail": "Yahoo Email"
},
"audioDevices": {
"bluetooth": "Bluetooth",
@ -146,6 +161,7 @@
"accessibilityLabel": {
"liveStreaming": "Live Stream"
},
"add": "Add",
"allow": "Allow",
"alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
@ -562,7 +578,9 @@
"pullToRefresh": "Pull to refresh"
},
"security": {
"insecureRoomNameWarning": "The room name is insecure. Unwanted participants may join your conference."
"about": "You can add a passcode to your meeting. Participants will need to provide the passcode before they are allowed to join the meeting.",
"insecureRoomNameWarning": "The room name is insecure. Unwanted participants may join your conference.",
"securityOptions": "Security options"
},
"settings": {
"calendar": {
@ -662,6 +680,7 @@
"raiseHand": "Toggle raise hand",
"recording": "Toggle recording",
"remoteMute": "Mute participant",
"security": "Security options",
"Settings": "Toggle settings",
"sharedvideo": "Toggle Youtube video sharing",
"shareRoom": "Invite someone",
@ -715,6 +734,7 @@
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"raiseYourHand": "Raise your hand",
"security": "Security options",
"Settings": "Settings",
"sharedvideo": "Share a YouTube video",
"shareRoom": "Invite someone",

View File

@ -29,7 +29,10 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button';
type Props = {
...DialogProps,
i18n: Object,
/**
* Custom dialog header that replaces the standard heading.
*/
customHeader?: React$Element<any> | Function,
/**
* Disables dismissing the dialog when the blanket is clicked. Enabled
@ -43,6 +46,8 @@ type Props = {
*/
hideCancelButton: boolean,
i18n: Object,
/**
* Whether the dialog is modal. This means clicking on the blanket will
* leave the dialog open. No cancel button.
@ -106,6 +111,7 @@ class StatelessDialog extends Component<Props> {
*/
render() {
const {
customHeader,
children,
t /* The following fixes a flow error: */ = _.identity,
titleString,
@ -116,8 +122,11 @@ class StatelessDialog extends Component<Props> {
return (
<Modal
autoFocus = { true }
components = {{
Header: customHeader
}}
footer = { this._renderFooter }
heading = { titleString || t(titleKey) }
heading = { customHeader ? undefined : titleString || t(titleKey) }
i18n = { this.props.i18n }
onClose = { this._onDialogDismissed }
onDialogDismissed = { this._onDialogDismissed }

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0933 8.33104C16.4628 7.92053 17.0951 7.88726 17.5056 8.25672C17.9161 8.62617 17.9494 9.25846 17.5799 9.66897L12.8749 14.9247C12.4777 15.3661 11.7856 15.3661 11.3883 14.9247L6.75666 9.66897C6.3872 9.25846 6.42048 8.62617 6.83099 8.25672C7.2415 7.88726 7.87379 7.92053 8.24325 8.33104L12.1316 12.7609L16.0933 8.33104Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4H20C21.1046 4 22 4.89543 22 6V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V6C2 4.89543 2.89543 4 4 4ZM4 8V18H20V8L12 12L4 8ZM20 6H4L12 10L20 6Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.2359 10.3544H19.56V10.32H12V13.68H16.7479C16.054 15.6356 14.1935 17.04 12 17.04C9.21583 17.04 6.95998 14.7841 6.95998 12C6.95998 9.21583 9.21583 6.95998 12 6.95998C13.2846 6.95998 14.4543 7.44396 15.3436 8.23638L17.7192 5.86076C16.2197 4.46294 14.2132 3.59998 12 3.59998C7.36029 3.59998 3.59998 7.36029 3.59998 12C3.59998 16.6397 7.36029 20.4 12 20.4C16.6397 20.4 20.4 16.6397 20.4 12C20.4 11.4372 20.3426 10.8876 20.2359 10.3544Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@ -4,6 +4,7 @@ export { default as IconAdd } from './add.svg';
export { default as IconAddPeople } from './link.svg';
export { default as IconArrowBack } from './arrow_back.svg';
export { default as IconArrowDown } from './arrow_down.svg';
export { default as IconArrowDownSmall } from './arrow-down-small.svg';
export { default as IconArrowLeft } from './arrow-left.svg';
export { default as IconAudioOnly } from './visibility.svg';
export { default as IconAudioOnlyOff } from './visibility-off.svg';
@ -30,18 +31,21 @@ export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
export { default as IconDragHandle } from './drag-handle.svg';
export { default as IconE2EE } from './e2ee.svg';
export { default as IconEmail } from './envelope.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExclamation } from './exclamation.svg';
export { default as IconExclamationSolid } from './exclamation-solid.svg';
export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconFullScreen } from './full-screen.svg';
export { default as IconGoogle } from './google.svg';
export { default as IconHangup } from './hangup.svg';
export { default as IconHelp } from './help.svg';
export { default as IconInfo } from './info.svg';
export { default as IconInvite } from './invite.svg';
export { default as IconInviteMore } from './user-plus.svg';
export { default as IconKick } from './kick.svg';
export { default as IconLiveStreaming } from './public.svg';
export { default as IconLockPassword } from './lock.svg';
export { default as IconMenu } from './menu.svg';
export { default as IconMenuDown } from './menu-down.svg';
export { default as IconMenuThumb } from './thumb-menu.svg';
@ -56,6 +60,7 @@ export { default as IconMuteEveryone } from './mute-everyone.svg';
export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
export { default as IconNotificationJoin } from './navigate_next.svg';
export { default as IconOpenInNew } from './open_in_new.svg';
export { default as IconOutlook } from './office365.svg';
export { default as IconPhone } from './phone.svg';
export { default as IconPin } from './enlarge.svg';
export { default as IconPresentation } from './presentation.svg';
@ -79,6 +84,7 @@ export { default as IconShareVideo } from './shared-video.svg';
export { default as IconSwitchCamera } from './switch-camera.svg';
export { default as IconTileView } from './tiles-many.svg';
export { default as IconToggleRecording } from './camera-take-picture.svg';
export { default as IconUnlockPassword } from './unlock.svg';
export { default as IconVideoQualityAudioOnly } from './AUD.svg';
export { default as IconVideoQualityHD } from './HD.svg';
export { default as IconVideoQualityLD } from './LD.svg';
@ -87,3 +93,4 @@ export { default as IconVolume } from './volume.svg';
export { default as IconVolumeEmpty } from './volume-empty.svg';
export { default as IconVolumeOff } from './volume-off.svg';
export { default as IconWarning } from './warning.svg';
export { default as IconYahoo } from './yahoo.svg';

View File

@ -1,5 +0,0 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>invite</title>
<path d="M18.984 12.984h-6v6h-1.969v-6h-6v-1.969h6v-6h1.969v6h6v1.969z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 240 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 10H7H17H19V20H5V10ZM19 8H17V7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7V8H5C3.89543 8 3 8.89543 3 10V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V10C21 8.89543 20.1046 8 19 8ZM12.9686 15.7502C13.5837 15.4091 14 14.7532 14 14C14 12.8954 13.1046 12 12 12C10.8954 12 10 12.8954 10 14C10 14.7532 10.4163 15.4091 11.0314 15.7502C11.0109 15.8301 11 15.9138 11 16V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V16C13 15.9138 12.9891 15.8301 12.9686 15.7502ZM12 4C13.6569 4 15 5.34315 15 7V8H9V7C9 5.34315 10.3431 4 12 4Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 720 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 6L14.0138 2L20.0213 3.5V20.5L14.0138 22L3 18L14.0138 19.5V5L7.00501 6.5V16.5L3 18V6Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8V7C7 4.23858 9.23858 2 12 2C14.0608 2 15.8304 3.24676 16.5957 5.02716L14.7583 5.81698C14.2882 4.72339 13.2108 4 12 4C10.3431 4 9 5.34315 9 7V8H12H16.8374H16.8818H19C20.1046 8 21 8.89543 21 10V20C21 21.1046 20.1046 22 19 22H5C3.89543 22 3 21.1046 3 20V10C3 8.89543 3.89543 8 5 8H7ZM5 20V10H19V20H5ZM12.9686 15.7502C13.5837 15.4091 14 14.7532 14 14C14 12.8954 13.1046 12 12 12C10.8954 12 10 12.8954 10 14C10 14.7532 10.4163 15.4091 11.0314 15.7502C11.0109 15.8301 11 15.9138 11 16V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V16C13 15.9138 12.9891 15.8301 12.9686 15.7502Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 757 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 7C14 8.10457 13.1046 9 12 9C10.8954 9 10 8.10457 10 7C10 5.89543 10.8954 5 12 5C13.1046 5 14 5.89543 14 7ZM8 7C8 9.20914 9.79086 11 12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7ZM12 12C5.98633 12 4 14.25 4 18.75C4 20.25 4.66667 21 6 21H14.7578C15.565 22.206 16.9398 23 18.5 23C20.9853 23 23 20.9853 23 18.5C23 16.0381 21.0231 14.038 18.5701 14.0005C17.3541 12.6668 15.2739 12 12 12ZM16.1639 14.6531C15.2365 14.1909 13.8943 14 12 14C7.30232 14 6 15.1737 6 18.75C6 18.8592 6.00376 18.9414 6.00693 19H14.0275C14.0093 18.8358 14 18.669 14 18.5C14 16.8702 14.8665 15.4427 16.1639 14.6531ZM19 18H21V19H19V21H18V19H16V18H18V16H19V18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3H9.10397L12.076 10.6035L15.0865 3H20.0561L12.573 21H7.57197L9.62033 16.2303L4 3Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

View File

@ -35,6 +35,12 @@ export type Props = {
*/
accessibilityLabel: string,
/**
* An extra class name to be added at the end of the element's class name
* in order to enable custom styling.
*/
customClass?: string,
/**
* Whether this item is disabled or not. When disabled, clicking an the item
* has no effect, and it may reflect on its style.

View File

@ -67,11 +67,11 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
* @returns {ReactElement}
*/
_renderIcon() {
const { disabled, icon, showLabel, toggled } = this.props;
const { customClass, disabled, icon, showLabel, toggled } = this.props;
const iconComponent = <Icon src = { icon } />;
const elementType = showLabel ? 'span' : 'div';
const className = `${showLabel ? 'overflow-menu-item-icon' : 'toolbox-icon'} ${
toggled ? 'toggled' : ''} ${disabled ? 'disabled' : ''}`;
toggled ? 'toggled' : ''} ${disabled ? 'disabled' : ''} ${customClass ?? ''}`;
return React.createElement(elementType, { className }, iconComponent);
}

View File

@ -25,15 +25,17 @@ import {
import { maybeShowSuboptimalExperienceNotification } from '../../functions';
import Labels from './Labels';
import { default as Notice } from './Notice';
import { default as Subject } from './Subject';
import {
AbstractConference,
abstractMapStateToProps
} from '../AbstractConference';
import type { AbstractProps } from '../AbstractConference';
import InviteMore from './InviteMore';
import Labels from './Labels';
import { default as Notice } from './Notice';
import { default as Subject } from './Subject';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
@ -202,6 +204,7 @@ class Conference extends AbstractConference<Props, *> {
<Notice />
<Subject />
<InviteMore />
<div id = 'videospace'>
<LargeVideo />
{ hideVideoQualityLabel

View File

@ -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));

View File

@ -75,7 +75,7 @@ function _mapStateToProps(state) {
return {
_showParticipantCount: participantCount > 2,
_subject: getConferenceName(state),
_visible: isToolboxVisible(state)
_visible: isToolboxVisible(state) && participantCount > 1
};
}

View File

@ -3,8 +3,8 @@
import type { Dispatch } from 'redux';
import { getInviteURL } from '../base/connection';
import { inviteVideoRooms } from '../videosipgw';
import { getParticipants } from '../base/participants';
import { inviteVideoRooms } from '../videosipgw';
import {
ADD_PENDING_INVITE_REQUEST,

View File

@ -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,
/**
* The current url of the conference to be copied onto the clipboard.
*/
_inviteUrl: string,
/**
* The current known URL for a live stream in progress.
*/
_liveStreamViewURL: string,
/**
* The redux representation of the local participant.
*/
_localParticipantName: ?string,
/**
* The current location url of the conference.
*/
_locationUrl: Object,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* Method to update the dial in numbers.
*/
updateNumbers: Function
};
/**
* The dialog that allows to invite people to the call.
* Invite More component.
*
* @returns {React$Element<any>}
*/
class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
_multiselect = null;
_resourceClient: Object;
state = {
addToCallError: false,
addToCallInProgress: false,
inviteItems: []
};
function AddPeopleDialog({
_conferenceName,
_dialIn,
_hideInviteContacts,
_inviteUrl,
_liveStreamViewURL,
_localParticipantName,
_locationUrl,
t,
updateNumbers }: Props) {
const [ phoneNumber, setPhoneNumber ] = useState(undefined);
/**
* Initializes a new {@code AddPeopleDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* Updates the dial-in numbers.
*/
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
};
}
useEffect(() => {
if (!_dialIn.numbers) {
updateNumbers();
}
}, []);
/**
* Sends an analytics event to record the dialog has been shown.
* Sends analytics events when the dialog opens/closes.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
useEffect(() => {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.opened', 'dialog'));
}
return () => {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.closed', 'dialog'));
};
}, []);
/**
* React Component method that executes once component is updated.
* Updates the phone number in the state once the dial-in numbers are fetched.
*
* @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([]);
useEffect(() => {
if (!phoneNumber && _dialIn && _dialIn.numbers) {
setPhoneNumber(_getDefaultPhoneNumber(_dialIn.numbers));
}
}
}, [ _dialIn ]);
/**
* Sends an analytics event to record the dialog has been closed.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.closed', 'dialog'));
}
const invite = getInviteText({
_conferenceName,
_localParticipantName,
_inviteUrl,
_locationUrl,
_dialIn,
_liveStreamViewURL,
phoneNumber,
t
});
const inviteSubject = t('addPeople.inviteMoreMailSubject', {
appName: interfaceConfig.APP_NAME
});
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
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;
/**
* 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 {Map} 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 {
this.props.dispatch(hideDialog());
return (
<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 } />
}
});
}
_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 = [];
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 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>
);
}
/**
* 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;
}
</Dialog>
);
}
/**
* 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)
);

View File

@ -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);

View File

@ -3,9 +3,12 @@
import React, { Component } from 'react';
import { translate } from '../../../../base/i18n';
import { Icon, IconCopy } from '../../../../base/icons';
import { _formatConferenceIDPin } from '../../../_utils';
import { copyText } from './utils';
/**
* The type of the React {@code Component} props of {@link DialInNumber}.
*/
@ -36,6 +39,37 @@ type Props = {
* @extends Component
*/
class DialInNumber extends Component<Props> {
/**
* Initializes a new DialInNumber instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onCopyText = this._onCopyText.bind(this);
}
_onCopyText: () => void;
/**
* Copies the dial-in information to the clipboard.
*
* @returns {void}
*/
_onCopyText() {
const { conferenceID, phoneNumber, t } = this.props;
const dialInLabel = t('info.dialInNumber');
const passcode = t('info.dialInConferenceID');
const conferenceIDPin = `${_formatConferenceIDPin(conferenceID)}#`;
const textToCopy = `${dialInLabel} ${phoneNumber} ${passcode} ${conferenceIDPin}`;
copyText(textToCopy);
}
/**
* Implements React's {@link Component#render()}.
*
@ -66,6 +100,11 @@ class DialInNumber extends Component<Props> {
{ `${_formatConferenceIDPin(conferenceID)}#` }
</span>
</span>
<a
className = 'dial-in-copy'
onClick = { this._onCopyText }>
<Icon src = { IconCopy } />
</a>
</div>
);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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);

View File

@ -1,3 +1,4 @@
// @flow
export { default as AddPeopleDialog } from './AddPeopleDialog';
export * from './utils';

View File

@ -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;
}

View File

@ -2,5 +2,4 @@
export * from './add-people-dialog';
export * from './dial-in-summary';
export * from './info-dialog';
export * from './callee-info';

View File

@ -1,3 +0,0 @@
// @flow
export * from './web';

View File

@ -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'>&nbsp;</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'>&nbsp;</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));

View File

@ -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));

View File

@ -1,4 +0,0 @@
// @flow
export { default as InfoDialog } from './InfoDialog';
export { default as InfoDialogButton } from './InfoDialogButton';

View File

@ -232,6 +232,58 @@ export function getInviteResultsForQuery(
});
}
/**
* Creates a message describing how to dial in to the conference.
*
* @returns {string}
*/
export function getInviteText({
_conferenceName,
_localParticipantName,
_inviteUrl,
_locationUrl,
_dialIn,
_liveStreamViewURL,
phoneNumber,
t
}: Object) {
const inviteURL = _decodeRoomURI(_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(_dialIn)) {
const dial = t('info.invitePhone', {
number: phoneNumber,
conferenceID: _dialIn.conferenceID
});
const moreNumbers = t('info.invitePhoneAlternatives', {
url: getDialInfoPageURL(
_conferenceName,
_locationUrl
),
silentUrl: `${inviteURL}#config.startSilent=true`
});
invite = `${invite}\n${dial}\n${moreNumbers}`;
}
return invite;
}
/**
* Helper for determining how many of each type of user is being invited. Used
* for logging and sending analytics related to invites.

View File

@ -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));
};
}

View File

@ -0,0 +1,3 @@
// @flow
export * from './security-dialog';

View File

@ -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);

View File

@ -2,8 +2,8 @@
import React, { Component } from 'react';
import { translate } from '../../../../base/i18n';
import { LOCKED_LOCALLY } from '../../../../room-lock';
import { translate } from '../../../base/i18n';
import { LOCKED_LOCALLY } from '../../../room-lock';
/**
* The type of the React {@code Component} props of {@link PasswordForm}.

View File

@ -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);

View File

@ -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));

View File

@ -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));

View File

@ -0,0 +1,4 @@
// @flow
export { default as SecurityDialog } from './SecurityDialog';
export { default as SecurityDialogButton } from './SecurityDialogButton';

View File

@ -0,0 +1,4 @@
// @flow
export * from './actions';
export * from './components';

View File

@ -15,7 +15,7 @@ import {
IconExitFullScreen,
IconFeedback,
IconFullScreen,
IconInvite,
IconInviteMore,
IconOpenInNew,
IconPresentation,
IconRaisedHand,
@ -36,12 +36,7 @@ import { ChatCounter, toggleChat } from '../../../chat';
import { E2EEButton } from '../../../e2ee';
import { SharedDocumentButton } from '../../../etherpad';
import { openFeedbackDialog } from '../../../feedback';
import {
beginAddPeople,
InfoDialogButton,
isAddPeopleEnabled,
isDialOutEnabled
} from '../../../invite';
import { beginAddPeople } from '../../../invite';
import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
import {
LocalRecordingButton,
@ -51,6 +46,7 @@ import {
LiveStreamButton,
RecordButton
} from '../../../recording';
import { SecurityDialogButton } from '../../../security';
import {
SETTINGS_TABS,
SettingsButton,
@ -132,12 +128,6 @@ type Props = {
*/
_tileViewEnabled: boolean,
/**
* Whether or not invite should be hidden, regardless of feature
* availability.
*/
_hideInviteButton: boolean,
/**
* Whether or not the current user is logged in through a JWT.
*/
@ -153,6 +143,12 @@ type Props = {
*/
_localRecState: Object,
/**
* The value for how the conference is locked (or undefined if not locked)
* as defined by room-lock constants.
*/
_locked: boolean,
/**
* Whether or not the overflow menu is visible.
*/
@ -1093,13 +1089,17 @@ class Toolbox extends Component<Props, State> {
);
case 'closedcaptions':
return <ClosedCaptionButton showLabel = { true } />;
case 'info':
return <InfoDialogButton showLabel = { true } />;
case 'security':
return (
<SecurityDialogButton
key = 'security'
showLabel = { true } />
);
case 'invite':
return (
<OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.invite') }
icon = { IconInvite }
icon = { IconInviteMore }
key = 'invite'
onClick = { this._onToolbarOpenInvite }
text = { t('toolbar.invite') } />
@ -1155,7 +1155,6 @@ class Toolbox extends Component<Props, State> {
_renderToolboxContent() {
const {
_chatOpen,
_hideInviteButton,
_overflowMenuVisible,
_raisedHand,
t
@ -1192,12 +1191,13 @@ class Toolbox extends Component<Props, State> {
if (overflowHasItems) {
buttonsRight.push('overflowmenu');
}
if (this._shouldShowButton('info')) {
buttonsRight.push('info');
}
if (this._shouldShowButton('invite') && !_hideInviteButton) {
if (this._shouldShowButton('invite')) {
buttonsRight.push('invite');
}
if (this._shouldShowButton('security') || this._shouldShowButton('info')) {
buttonsRight.push('security');
}
if (this._shouldShowButton('tileview')) {
buttonsRight.push('tileview');
}
@ -1283,13 +1283,11 @@ class Toolbox extends Component<Props, State> {
&& <ToolbarButton
accessibilityLabel =
{ t('toolbar.accessibilityLabel.invite') }
icon = { IconInvite }
icon = { IconInviteMore }
onClick = { this._onToolbarOpenInvite }
tooltip = { t('toolbar.invite') } /> }
{
buttonsRight.indexOf('info') !== -1
&& <InfoDialogButton />
}
{ buttonsRight.indexOf('security') !== -1
&& <SecurityDialogButton customClass = 'security-toolbar-button' /> }
{ buttonsRight.indexOf('overflowmenu') !== -1
&& <OverflowMenuButton
isOpen = { _overflowMenuVisible }
@ -1328,12 +1326,11 @@ class Toolbox extends Component<Props, State> {
* @returns {{}}
*/
function _mapStateToProps(state) {
const { conference } = state['features/base/conference'];
const { conference, locked } = state['features/base/conference'];
let { desktopSharingEnabled } = state['features/base/conference'];
const {
callStatsID,
enableFeaturesBasedOnToken,
iAmRecorder
enableFeaturesBasedOnToken
} = state['features/base/config'];
const sharedVideoStatus = state['features/shared-video'].status;
const {
@ -1343,8 +1340,6 @@ function _mapStateToProps(state) {
const localParticipant = getLocalParticipant(state);
const localRecordingStates = state['features/local-recording'];
const localVideo = getLocalVideoTrack(state['features/base/tracks']);
const addPeopleEnabled = isAddPeopleEnabled(state);
const dialOutEnabled = isDialOutEnabled(state);
let desktopSharingDisabledTooltipKey;
@ -1376,13 +1371,12 @@ function _mapStateToProps(state) {
_desktopSharingDisabledTooltipKey: desktopSharingDisabledTooltipKey,
_dialog: Boolean(state['features/base/dialog'].component),
_feedbackConfigured: Boolean(callStatsID),
_hideInviteButton:
iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
_isGuest: state['features/base/jwt'].isGuest,
_fullScreen: fullScreen,
_tileViewEnabled: state['features/video-layout'].tileViewEnabled,
_localParticipantID: localParticipant.id,
_localRecState: localRecordingStates,
_locked: locked,
_overflowMenuVisible: overflowMenuVisible,
_raisedHand: localParticipant.raisedHand,
_screensharing: localVideo && localVideo.videoType === 'desktop',