mirror of https://github.com/jitsi/jitsi-meet
Converting the invite modal includes the following: - Creating new react components to display InviteDialog. The main parent components are ShareLink and PasswordOverview, the later handles displaying lock state and password editing. These components do not make use of atlaskit as the component for input does not yet support readonly, so for consistency within the modal content no atlaskit was used. - Using redux for keeping and accessing lock state instead of RoomLocker. - Publicly exposing the redux action lockStateChanged for direct calling on lock events experienced on the web client. - Removing Invite, InviteDialogView, and RoomLocker and references to them. - Handling errors that occur when setting a password to preserve existing web funtionality.pull/1499/head
parent
4097be1908
commit
44b81b20e3
@ -1,187 +0,0 @@ |
||||
/* global JitsiMeetJS, APP */ |
||||
const logger = require("jitsi-meet-logger").getLogger(__filename); |
||||
|
||||
import InviteDialogView from './InviteDialogView'; |
||||
import createRoomLocker from './RoomLocker'; |
||||
import UIEvents from '../../../service/UI/UIEvents'; |
||||
|
||||
const ConferenceEvents = JitsiMeetJS.events.conference; |
||||
|
||||
/** |
||||
* Invite module |
||||
* Constructor takes conference object giving |
||||
* ability to subscribe on its events |
||||
*/ |
||||
class Invite { |
||||
constructor(conference) { |
||||
this.conference = conference; |
||||
this.inviteUrl = APP.ConferenceUrl.getInviteUrl(); |
||||
this.createRoomLocker(conference); |
||||
this.registerListeners(); |
||||
} |
||||
|
||||
/** |
||||
* Registering listeners. |
||||
* Primarily listeners for conference events. |
||||
*/ |
||||
registerListeners() { |
||||
|
||||
this.conference.on(ConferenceEvents.LOCK_STATE_CHANGED, |
||||
(locked, error) => { |
||||
|
||||
logger.log("Received channel password lock change: ", locked, |
||||
error); |
||||
|
||||
if (!locked) { |
||||
this.getRoomLocker().resetPassword(); |
||||
} |
||||
|
||||
this.setLockedFromElsewhere(locked); |
||||
}); |
||||
|
||||
this.conference.on(ConferenceEvents.USER_ROLE_CHANGED, (id) => { |
||||
if (APP.conference.isLocalId(id) |
||||
&& this.isModerator !== this.conference.isModerator()) { |
||||
|
||||
this.setModerator(this.conference.isModerator()); |
||||
} |
||||
}); |
||||
|
||||
APP.UI.addListener( UIEvents.INVITE_CLICKED, |
||||
() => { this.openLinkDialog(); }); |
||||
} |
||||
|
||||
/** |
||||
* Updates the view. |
||||
* If dialog hasn't been defined - |
||||
* creates it and updates |
||||
*/ |
||||
updateView() { |
||||
if (!this.view) { |
||||
this.initDialog(); |
||||
} |
||||
|
||||
this.view.updateView(); |
||||
} |
||||
|
||||
/** |
||||
* Room locker factory |
||||
* @param room |
||||
* @returns {Object} RoomLocker |
||||
* @factory |
||||
*/ |
||||
createRoomLocker(room = this.conference) { |
||||
let roomLocker = createRoomLocker(room); |
||||
this.roomLocker = roomLocker; |
||||
return this.getRoomLocker(); |
||||
} |
||||
|
||||
/** |
||||
* Room locker getter |
||||
* @returns {Object} RoomLocker |
||||
*/ |
||||
getRoomLocker() { |
||||
return this.roomLocker; |
||||
} |
||||
|
||||
/** |
||||
* Opens the invite link dialog. |
||||
*/ |
||||
openLinkDialog () { |
||||
if (!this.view) { |
||||
this.initDialog(); |
||||
} |
||||
|
||||
this.view.open(); |
||||
} |
||||
|
||||
/** |
||||
* Dialog initialization. |
||||
* creating view object using as a model this module |
||||
*/ |
||||
initDialog() { |
||||
this.view = new InviteDialogView(this); |
||||
} |
||||
|
||||
/** |
||||
* Password getter |
||||
* @returns {String} password |
||||
*/ |
||||
getPassword() { |
||||
return this.getRoomLocker().password; |
||||
} |
||||
|
||||
/** |
||||
* Switches between the moderator view and normal view. |
||||
* |
||||
* @param isModerator indicates if the participant is moderator |
||||
*/ |
||||
setModerator(isModerator) { |
||||
this.isModerator = isModerator; |
||||
|
||||
this.updateView(); |
||||
} |
||||
|
||||
/** |
||||
* Allows to unlock the room. |
||||
* If the current user is moderator. |
||||
*/ |
||||
setRoomUnlocked() { |
||||
if (this.isModerator) { |
||||
this.getRoomLocker().lock().then(() => { |
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK); |
||||
this.updateView(); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Allows to lock the room if |
||||
* the current user is moderator. |
||||
* Takes the password. |
||||
* @param {String} newPass |
||||
*/ |
||||
setRoomLocked(newPass) { |
||||
let isModerator = this.isModerator; |
||||
if (isModerator && (newPass || !this.getRoomLocker().isLocked)) { |
||||
this.getRoomLocker().lock(newPass).then(() => { |
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK); |
||||
this.updateView(); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Helper method for encoding |
||||
* Invite URL |
||||
* @returns {string} |
||||
*/ |
||||
getEncodedInviteUrl() { |
||||
return encodeURI(this.inviteUrl); |
||||
} |
||||
|
||||
/** |
||||
* Is locked flag. |
||||
* Delegates to room locker |
||||
* @returns {Boolean} isLocked |
||||
*/ |
||||
isLocked() { |
||||
return this.getRoomLocker().isLocked; |
||||
} |
||||
|
||||
/** |
||||
* Set flag locked from elsewhere to room locker. |
||||
* @param isLocked |
||||
*/ |
||||
setLockedFromElsewhere(isLocked) { |
||||
let roomLocker = this.getRoomLocker(); |
||||
let oldLockState = roomLocker.isLocked; |
||||
if (oldLockState !== isLocked) { |
||||
roomLocker.lockedElsewhere = isLocked; |
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK); |
||||
this.updateView(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export default Invite; |
||||
@ -1,378 +0,0 @@ |
||||
/* global $, APP, JitsiMeetJS */ |
||||
const logger = require("jitsi-meet-logger").getLogger(__filename); |
||||
|
||||
/** |
||||
* Substate for password |
||||
* @type {{LOCKED: string, UNLOCKED: string}} |
||||
*/ |
||||
const States = { |
||||
LOCKED: 'locked', |
||||
UNLOCKED: 'unlocked' |
||||
}; |
||||
|
||||
/** |
||||
* Class representing view for Invite dialog |
||||
* @class InviteDialogView |
||||
*/ |
||||
export default class InviteDialogView { |
||||
constructor(model) { |
||||
this.unlockHint = "unlockHint"; |
||||
this.lockHint = "lockHint"; |
||||
this.model = model; |
||||
|
||||
if (this.model.inviteUrl === null) { |
||||
this.inviteAttributes = `data-i18n="[value]inviteUrlDefaultMsg"`; |
||||
} else { |
||||
this.inviteAttributes |
||||
= `value="${this.model.getEncodedInviteUrl()}"`; |
||||
} |
||||
|
||||
this.initDialog(); |
||||
} |
||||
|
||||
/** |
||||
* Initialization of dialog property |
||||
*/ |
||||
initDialog() { |
||||
let dialog = {}; |
||||
dialog.closeFunction = this.closeFunction.bind(this); |
||||
dialog.submitFunction = this.submitFunction.bind(this); |
||||
dialog.loadedFunction = this.loadedFunction.bind(this); |
||||
|
||||
dialog.titleKey = "dialog.shareLink"; |
||||
this.dialog = dialog; |
||||
|
||||
this.dialog.states = this.getStates(); |
||||
} |
||||
|
||||
/** |
||||
* Event handler for submitting dialog |
||||
* @param e |
||||
* @param v |
||||
*/ |
||||
submitFunction(e, v) { |
||||
if (v && this.model.inviteUrl) { |
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.button'); |
||||
} else { |
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Event handler for load dialog |
||||
* @param event |
||||
*/ |
||||
loadedFunction(event) { |
||||
if (this.model.inviteUrl) { |
||||
document.getElementById('inviteLinkRef').select(); |
||||
} else { |
||||
if (event && event.target) { |
||||
$(event.target).find('button[value=true]') |
||||
.prop('disabled', true); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Event handler for closing dialog |
||||
* @param e |
||||
* @param v |
||||
* @param m |
||||
* @param f |
||||
*/ |
||||
closeFunction(e, v, m, f) { |
||||
$(document).off('click', '.copyInviteLink', this.copyToClipboard); |
||||
|
||||
if(!v && !m && !f) |
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.close'); |
||||
} |
||||
|
||||
/** |
||||
* Returns all states of the dialog |
||||
* @returns {{}} |
||||
*/ |
||||
getStates() { |
||||
let { |
||||
titleKey |
||||
} = this.dialog; |
||||
let doneMsg = APP.translation.generateTranslationHTML('dialog.done'); |
||||
let states = {}; |
||||
let buttons = {}; |
||||
buttons[`${doneMsg}`] = true; |
||||
|
||||
states[States.UNLOCKED] = { |
||||
titleKey, |
||||
html: this.getShareLinkBlock() + this.getAddPasswordBlock(), |
||||
buttons |
||||
}; |
||||
states[States.LOCKED] = { |
||||
titleKey, |
||||
html: this.getShareLinkBlock() + this.getPasswordBlock(), |
||||
buttons |
||||
}; |
||||
|
||||
return states; |
||||
} |
||||
|
||||
/** |
||||
* Layout for invite link input |
||||
* @returns {string} |
||||
*/ |
||||
getShareLinkBlock() { |
||||
let classes = 'button-control button-control_light copyInviteLink'; |
||||
return ( |
||||
`<div class="form-control">
|
||||
<label class="form-control__label" for="inviteLinkRef" |
||||
data-i18n="${this.dialog.titleKey}"></label> |
||||
<div class="form-control__container"> |
||||
<input class="input-control inviteLink" |
||||
id="inviteLinkRef" type="text" |
||||
${this.inviteAttributes} readonly> |
||||
<button data-i18n="dialog.copy" class="${classes}"></button> |
||||
</div> |
||||
<p class="form-control__hint ${this.lockHint}"> |
||||
<span class="icon-security-locked"></span> |
||||
<span data-i18n="dialog.roomLocked"></span> |
||||
</p> |
||||
<p class="form-control__hint ${this.unlockHint}"> |
||||
<span class="icon-security"></span> |
||||
<span data-i18n="roomUnlocked"></span> |
||||
</p> |
||||
</div>` |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Layout for adding password input |
||||
* @returns {string} |
||||
*/ |
||||
getAddPasswordBlock() { |
||||
let html; |
||||
|
||||
if (this.model.isModerator) { |
||||
html = (` |
||||
<div class="form-control"> |
||||
<label class="form-control__label" |
||||
for="newPasswordInput" data-i18n="dialog.addPassword"> |
||||
</label> |
||||
<div class="form-control__container"> |
||||
<input class="input-control" |
||||
id="newPasswordInput" |
||||
type="text"
|
||||
data-i18n="[placeholder]dialog.createPassword"> |
||||
<button id="addPasswordBtn" id="inviteDialogAddPassword" |
||||
disabled data-i18n="dialog.add" |
||||
class="button-control button-control_light"> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
`);
|
||||
} else { |
||||
html = ''; |
||||
} |
||||
|
||||
return html; |
||||
} |
||||
|
||||
/** |
||||
* Layout for password (when room is locked) |
||||
* @returns {string} |
||||
*/ |
||||
getPasswordBlock() { |
||||
let password = this.model.getPassword(); |
||||
let { isModerator } = this.model; |
||||
|
||||
if (isModerator) { |
||||
return (` |
||||
<div class="form-control"> |
||||
<label class="form-control__label" |
||||
data-i18n="dialog.passwordLabel"></label> |
||||
<div class="form-control__container"> |
||||
<p> |
||||
<span class="form-control__text" |
||||
data-i18n="dialog.currentPassword"></span> |
||||
<span id="inviteDialogPassword" |
||||
class="form-control__em"> |
||||
${password} |
||||
</span> |
||||
</p> |
||||
<a class="link form-control__right" |
||||
id="inviteDialogRemovePassword" |
||||
data-i18n="dialog.removePassword"></a> |
||||
</div> |
||||
</div> |
||||
`);
|
||||
} else { |
||||
return (` |
||||
<div class="form-control"> |
||||
<p>A participant protected this call with a password.</p> |
||||
</div> |
||||
`);
|
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* Opening the dialog |
||||
*/ |
||||
open() { |
||||
let { |
||||
submitFunction, |
||||
loadedFunction, |
||||
closeFunction |
||||
} = this.dialog; |
||||
|
||||
let states = this.getStates(); |
||||
let initial = this.model.roomLocked ? States.LOCKED : States.UNLOCKED; |
||||
|
||||
APP.UI.messageHandler.openDialogWithStates(states, { |
||||
submit: submitFunction, |
||||
loaded: loadedFunction, |
||||
close: closeFunction, |
||||
size: 'medium' |
||||
}); |
||||
$.prompt.goToState(initial); |
||||
|
||||
this.registerListeners(); |
||||
this.updateView(); |
||||
} |
||||
|
||||
/** |
||||
* Setting event handlers |
||||
* used in dialog |
||||
*/ |
||||
registerListeners() { |
||||
const ENTER_KEY = 13; |
||||
let addPasswordBtn = '#addPasswordBtn'; |
||||
let copyInviteLink = '.copyInviteLink'; |
||||
let newPasswordInput = '#newPasswordInput'; |
||||
let removePassword = '#inviteDialogRemovePassword'; |
||||
|
||||
$(document).on('click', copyInviteLink, this.copyToClipboard); |
||||
$(removePassword).on('click', () => { |
||||
this.model.setRoomUnlocked(); |
||||
}); |
||||
let boundSetPassword = this._setPassword.bind(this); |
||||
$(document).on('click', addPasswordBtn, boundSetPassword); |
||||
let boundDisablePass = this.disableAddPassIfInputEmpty.bind(this); |
||||
$(document).on('keypress', newPasswordInput, boundDisablePass); |
||||
|
||||
// We need to handle keydown event because impromptu
|
||||
// is listening to it too for closing the dialog
|
||||
$(newPasswordInput).on('keydown', (e) => { |
||||
if (e.keyCode === ENTER_KEY) { |
||||
e.stopPropagation(); |
||||
this._setPassword(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Marking room as locked |
||||
* @private |
||||
*/ |
||||
_setPassword() { |
||||
let $passInput = $('#newPasswordInput'); |
||||
let newPass = $passInput.val(); |
||||
|
||||
if(newPass) { |
||||
this.model.setRoomLocked(newPass); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Checking input and if it's empty then |
||||
* disable add pass button |
||||
*/ |
||||
disableAddPassIfInputEmpty() { |
||||
let $passInput = $('#newPasswordInput'); |
||||
let $addPassBtn = $('#addPasswordBtn'); |
||||
|
||||
if(!$passInput.val()) { |
||||
$addPassBtn.prop('disabled', true); |
||||
} else { |
||||
$addPassBtn.prop('disabled', false); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Copying text to clipboard |
||||
*/ |
||||
copyToClipboard() { |
||||
$('.inviteLink').each(function () { |
||||
let $el = $(this).closest('.jqistate'); |
||||
|
||||
// TOFIX: We can select only visible elements
|
||||
if($el.css('display') === 'block') { |
||||
this.select(); |
||||
|
||||
try { |
||||
document.execCommand('copy'); |
||||
this.blur(); |
||||
} |
||||
catch (err) { |
||||
logger.error('error when copy the text'); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Method syncing the view and the model |
||||
*/ |
||||
updateView() { |
||||
let pass = this.model.getPassword(); |
||||
let { isModerator } = this.model; |
||||
if (this.model.getRoomLocker().lockedElsewhere || !pass) { |
||||
$('#inviteDialogPassword').attr("data-i18n", "passwordSetRemotely"); |
||||
APP.translation.translateElement($('#inviteDialogPassword')); |
||||
} else { |
||||
$('#inviteDialogPassword').removeAttr("data-i18n"); |
||||
$('#inviteDialogPassword').text(pass); |
||||
} |
||||
|
||||
// if we are not moderator we cannot remove password
|
||||
if (isModerator) |
||||
$('#inviteDialogRemovePassword').show(); |
||||
else |
||||
$('#inviteDialogRemovePassword').hide(); |
||||
|
||||
$('#newPasswordInput').val(''); |
||||
this.disableAddPassIfInputEmpty(); |
||||
|
||||
this.updateInviteLink(); |
||||
|
||||
$.prompt.goToState( |
||||
(this.model.isLocked()) |
||||
? States.LOCKED |
||||
: States.UNLOCKED); |
||||
|
||||
let roomLocked = `.${this.lockHint}`; |
||||
let roomUnlocked = `.${this.unlockHint}`; |
||||
|
||||
let showDesc = this.model.isLocked() ? roomLocked : roomUnlocked; |
||||
let hideDesc = !this.model.isLocked() ? roomLocked : roomUnlocked; |
||||
|
||||
$(showDesc).show(); |
||||
$(hideDesc).hide(); |
||||
} |
||||
|
||||
/** |
||||
* Updates invite link |
||||
*/ |
||||
updateInviteLink() { |
||||
// If the invite dialog has been already opened we update the
|
||||
// information.
|
||||
let inviteLink = document.querySelectorAll('.inviteLink'); |
||||
let list = Array.from(inviteLink); |
||||
list.forEach((inviteLink) => { |
||||
inviteLink.value = this.model.inviteUrl; |
||||
inviteLink.select(); |
||||
}); |
||||
|
||||
$('#inviteLinkRef').parent() |
||||
.find('button[value=true]').prop('disabled', false); |
||||
} |
||||
} |
||||
@ -1,103 +0,0 @@ |
||||
/* global APP, JitsiMeetJS */ |
||||
const logger = require("jitsi-meet-logger").getLogger(__filename); |
||||
|
||||
/** |
||||
* Show notification that user cannot set password for the conference |
||||
* because server doesn't support that. |
||||
*/ |
||||
function notifyPasswordNotSupported () { |
||||
logger.warn('room passwords not supported'); |
||||
APP.UI.messageHandler.showError( |
||||
"dialog.warning", "dialog.passwordNotSupported"); |
||||
} |
||||
|
||||
/** |
||||
* Show notification that setting password for the conference failed. |
||||
* @param {Error} err error |
||||
*/ |
||||
function notifyPasswordFailed(err) { |
||||
logger.warn('setting password failed', err); |
||||
APP.UI.messageHandler.showError( |
||||
"dialog.lockTitle", "dialog.lockMessage"); |
||||
} |
||||
|
||||
const ConferenceErrors = JitsiMeetJS.errors.conference; |
||||
|
||||
/** |
||||
* Create new RoomLocker for the conference. |
||||
* It allows to set or remove password for the conference, |
||||
* or ask for required password. |
||||
* @returns {RoomLocker} |
||||
*/ |
||||
export default function createRoomLocker (room) { |
||||
let password; |
||||
/** |
||||
* If the room was locked from someone other than us, we indicate it with |
||||
* this property in order to have correct roomLocker state of isLocked. |
||||
* @type {boolean} whether room is locked, but not from us. |
||||
*/ |
||||
let lockedElsewhere = false; |
||||
|
||||
/** |
||||
* @class RoomLocker |
||||
*/ |
||||
return { |
||||
get isLocked () { |
||||
return !!password || lockedElsewhere; |
||||
}, |
||||
|
||||
get password () { |
||||
return password; |
||||
}, |
||||
|
||||
/** |
||||
* Allows to set new password |
||||
* @param newPass |
||||
* @returns {Promise.<TResult>} |
||||
*/ |
||||
lock (newPass) { |
||||
return room.lock(newPass).then(() => { |
||||
password = newPass; |
||||
// If the password is undefined this means that we're removing
|
||||
// it for everyone.
|
||||
if (!password) |
||||
lockedElsewhere = false; |
||||
}).catch(function (err) { |
||||
logger.error(err); |
||||
if (err === ConferenceErrors.PASSWORD_NOT_SUPPORTED) { |
||||
notifyPasswordNotSupported(); |
||||
} else { |
||||
notifyPasswordFailed(err); |
||||
} |
||||
throw err; |
||||
}); |
||||
}, |
||||
|
||||
/** |
||||
* Sets that the room is locked from another user, not us. |
||||
* @param {boolean} value locked/unlocked state |
||||
*/ |
||||
set lockedElsewhere (value) { |
||||
lockedElsewhere = value; |
||||
}, |
||||
|
||||
/** |
||||
* Whether room is locked from someone else. |
||||
* @returns {boolean} whether room is not locked locally, |
||||
* but it is still locked. |
||||
*/ |
||||
get lockedElsewhere () { |
||||
return lockedElsewhere; |
||||
}, |
||||
|
||||
/** |
||||
* Reset the password. Can be useful when room |
||||
* has been unlocked from elsewhere and we can use |
||||
* this method for sync the pass |
||||
*/ |
||||
resetPassword() { |
||||
password = null; |
||||
}, |
||||
|
||||
}; |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
/* globals APP */ |
||||
|
||||
import { openDialog } from '../../features/base/dialog'; |
||||
|
||||
import { InviteDialog } from './components'; |
||||
|
||||
/** |
||||
* Opens the Invite Dialog. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function openInviteDialog() { |
||||
return dispatch => { |
||||
dispatch(openDialog(InviteDialog, { |
||||
conferenceUrl: encodeURI(APP.ConferenceUrl.getInviteUrl()) |
||||
})); |
||||
}; |
||||
} |
||||
@ -0,0 +1,137 @@ |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { setPassword } from '../../base/conference'; |
||||
import { translate } from '../../base/i18n'; |
||||
|
||||
/** |
||||
* A React Component for locking a JitsiConference with a password. |
||||
*/ |
||||
class AddPasswordForm extends Component { |
||||
/** |
||||
* AddPasswordForm component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The JitsiConference on which to lock and set a password. |
||||
* |
||||
* @type {JitsiConference} |
||||
*/ |
||||
conference: React.PropTypes.object, |
||||
|
||||
/** |
||||
* Invoked to set a password on the conference. |
||||
*/ |
||||
dispatch: React.PropTypes.func, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func |
||||
} |
||||
|
||||
/** |
||||
* Initializes a new AddPasswordForm instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
password: '' |
||||
}; |
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this); |
||||
this._onPasswordChange = this._onPasswordChange.bind(this); |
||||
this._onSubmit = this._onSubmit.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<div |
||||
className = 'form-control' |
||||
onSubmit = { this._onSubmit } > |
||||
<div className = 'form-control__container'> |
||||
<input |
||||
autoFocus = { true } |
||||
className = 'input-control' |
||||
id = 'newPasswordInput' |
||||
onChange = { this._onPasswordChange } |
||||
onKeyDown = { this._onKeyDown } |
||||
placeholder |
||||
= { this.props.t('dialog.createPassword') } |
||||
type = 'text' /> |
||||
<button |
||||
className = 'button-control button-control_light' |
||||
disabled = { !this.state.password } |
||||
id = 'addPasswordBtn' |
||||
onClick = { this._onSubmit } |
||||
type = 'button'> |
||||
{ this.props.t('dialog.add') } |
||||
</button> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Mimics form behavior by listening for enter key press and submitting the |
||||
* entered password. |
||||
* |
||||
* @param {Object} event - DOM Event for keydown. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onKeyDown(event) { |
||||
event.stopPropagation(); |
||||
|
||||
if (event.keyCode === /* Enter */ 13) { |
||||
this._onSubmit(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Updates the internal state of the entered password. |
||||
* |
||||
* @param {Object} event - DOM Event for value change. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onPasswordChange(event) { |
||||
this.setState({ password: event.target.value }); |
||||
} |
||||
|
||||
/** |
||||
* Dispatches a request to lock the conference with a password. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onSubmit() { |
||||
if (!this.state.password) { |
||||
return; |
||||
} |
||||
|
||||
const conference = this.props.conference; |
||||
|
||||
this.props.dispatch(setPassword( |
||||
conference, |
||||
conference.lock, |
||||
this.state.password |
||||
)); |
||||
|
||||
this.setState({ password: '' }); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(AddPasswordForm)); |
||||
@ -0,0 +1,104 @@ |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { Dialog } from '../../base/dialog'; |
||||
import { translate } from '../../base/i18n'; |
||||
import JitsiMeetJS from '../../base/lib-jitsi-meet'; |
||||
import { |
||||
getLocalParticipant, |
||||
PARTICIPANT_ROLE |
||||
} from '../../base/participants'; |
||||
|
||||
import PasswordContainer from './PasswordContainer'; |
||||
import ShareLinkForm from './ShareLinkForm'; |
||||
|
||||
/** |
||||
* A React Component for displaying other components responsible for copying the |
||||
* current conference url and for setting or removing a conference password. |
||||
*/ |
||||
class InviteDialog extends Component { |
||||
/** |
||||
* InviteDialog component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The redux store representation of the JitsiConference. |
||||
* |
||||
*/ |
||||
_conference: React.PropTypes.object, |
||||
|
||||
/** |
||||
* Whether or not the current user is a conference moderator. |
||||
*/ |
||||
_isModerator: React.PropTypes.bool, |
||||
|
||||
/** |
||||
* The url for the JitsiConference. |
||||
*/ |
||||
conferenceUrl: React.PropTypes.string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func |
||||
} |
||||
|
||||
/** |
||||
* Reports an analytics event for the invite modal being closed. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
componentWillUnmount() { |
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.close'); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<Dialog |
||||
cancelDisabled = { true } |
||||
okTitleKey = 'dialog.done' |
||||
titleString = { this.props.t( |
||||
'invite.inviteTo', |
||||
{ conferenceName: this.props._conference.room }) } > |
||||
<div className = 'invite-dialog'> |
||||
<ShareLinkForm toCopy = { this.props.conferenceUrl } /> |
||||
<PasswordContainer |
||||
conference = { this.props._conference.conference } |
||||
locked = { this.props._conference.locked } |
||||
password = { this.props._conference.password } |
||||
showPasswordEdit = { this.props._isModerator } /> |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated InviteDialog's props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {{ |
||||
* _conference: Object, |
||||
* _isModerator: boolean |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { role } |
||||
= getLocalParticipant(state['features/base/participants']); |
||||
|
||||
return { |
||||
_conference: state['features/base/conference'], |
||||
_isModerator: role === PARTICIPANT_ROLE.MODERATOR |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(InviteDialog)); |
||||
@ -0,0 +1,48 @@ |
||||
import React, { Component } from 'react'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
|
||||
/** |
||||
* A React Component for displaying the conference lock state. |
||||
*/ |
||||
class LockStatePanel extends Component { |
||||
/** |
||||
* LockStatePanel component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* Whether or not the conference is currently locked. |
||||
*/ |
||||
locked: React.PropTypes.bool, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const [ lockStateClass, lockIconClass, lockTextKey ] = this.props.locked |
||||
? [ 'is-locked', 'icon-security-locked', 'invite.locked' ] |
||||
: [ 'is-unlocked', 'icon-security', 'invite.unlocked' ]; |
||||
|
||||
return ( |
||||
<div className = { `lock-state ${lockStateClass}` }> |
||||
<span className = { lockIconClass } /> |
||||
<span> |
||||
{ this.props.t(lockTextKey) } |
||||
</span> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(LockStatePanel); |
||||
@ -0,0 +1,147 @@ |
||||
import React, { Component } from 'react'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
import { LOCKED_LOCALLY } from '../../room-lock'; |
||||
|
||||
import AddPasswordForm from './AddPasswordForm'; |
||||
import LockStatePanel from './LockStatePanel'; |
||||
import RemovePasswordForm from './RemovePasswordForm'; |
||||
|
||||
/** |
||||
* React component for displaying the current room lock state as well as |
||||
* exposing features to modify the room lock. |
||||
*/ |
||||
class PasswordContainer extends Component { |
||||
/** |
||||
* PasswordContainer component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The JitsiConference for which to display a lock state and change the |
||||
* password. |
||||
* |
||||
* @type {JitsiConference} |
||||
*/ |
||||
conference: React.PropTypes.object, |
||||
|
||||
/** |
||||
* The value for how the conference is locked (or undefined if not |
||||
* locked) as defined by room-lock constants. |
||||
*/ |
||||
locked: React.PropTypes.string, |
||||
|
||||
/** |
||||
* The current known password for the JitsiConference. |
||||
*/ |
||||
password: React.PropTypes.string, |
||||
|
||||
/** |
||||
* Whether or not the password editing components should be displayed. |
||||
*/ |
||||
showPasswordEdit: React.PropTypes.bool, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func |
||||
} |
||||
|
||||
/** |
||||
* Initializes a new PasswordContainer instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.state = { |
||||
isEditingPassword: false |
||||
}; |
||||
|
||||
this._onTogglePasswordEdit = this._onTogglePasswordEdit.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<div className = 'password-overview'> |
||||
<div className = 'password-overview-status'> |
||||
<LockStatePanel locked = { Boolean(this.props.locked) } /> |
||||
{ this._renderShowPasswordLink() } |
||||
</div> |
||||
{ this._renderPasswordEdit() } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Toggles the display of the ReactElements used to edit the password. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onTogglePasswordEdit() { |
||||
this.setState({ |
||||
isEditingPassword: !this.state.isEditingPassword |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Creates a ReactElement used for setting or removing a password. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement|null} |
||||
*/ |
||||
_renderPasswordEdit() { |
||||
if (!this.state.isEditingPassword) { |
||||
return null; |
||||
} |
||||
|
||||
return this.props.locked |
||||
? <RemovePasswordForm |
||||
conference = { this.props.conference } |
||||
lockedLocally = { this.props.locked === LOCKED_LOCALLY } |
||||
password = { this.props.password } /> |
||||
: <AddPasswordForm conference = { this.props.conference } />; |
||||
} |
||||
|
||||
/** |
||||
* Creates a ReactElement that toggles displaying password edit components. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement|null} |
||||
*/ |
||||
_renderShowPasswordLink() { |
||||
if (!this.props.showPasswordEdit) { |
||||
return null; |
||||
} |
||||
|
||||
let toggleStatusKey; |
||||
|
||||
if (this.state.isEditingPassword) { |
||||
toggleStatusKey = 'invite.hidePassword'; |
||||
} else if (this.props.locked) { |
||||
toggleStatusKey = 'invite.showPassword'; |
||||
} else { |
||||
toggleStatusKey = 'invite.addPassword'; |
||||
} |
||||
|
||||
return ( |
||||
<a |
||||
className = 'password-overview-toggle-edit' |
||||
onClick = { this._onTogglePasswordEdit }> |
||||
{ this.props.t(toggleStatusKey) } |
||||
</a> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(PasswordContainer); |
||||
@ -0,0 +1,117 @@ |
||||
import React, { Component } from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { setPassword } from '../../base/conference'; |
||||
import { translate } from '../../base/i18n'; |
||||
|
||||
/** |
||||
* A React Component for removing a lock from a JitsiConference. |
||||
*/ |
||||
class RemovePasswordForm extends Component { |
||||
/** |
||||
* RemovePasswordForm component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* The JitsiConference on which remove a lock. |
||||
* |
||||
* @type {JitsiConference} |
||||
*/ |
||||
conference: React.PropTypes.object, |
||||
|
||||
/** |
||||
* Invoked to send a password removal request. |
||||
*/ |
||||
dispatch: React.PropTypes.func, |
||||
|
||||
/** |
||||
* Whether or not the room lock, if any, was set by the local user. |
||||
*/ |
||||
lockedLocally: React.PropTypes.bool, |
||||
|
||||
/** |
||||
* The current known password for the JitsiConference. |
||||
*/ |
||||
password: React.PropTypes.string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func |
||||
} |
||||
|
||||
/** |
||||
* Initializes a new RemovePasswordForm instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this._onClick = this._onClick.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<div className = 'remove-password'> |
||||
<div className = 'remove-password-description'> |
||||
{ this._getPasswordPreviewText() } |
||||
</div> |
||||
<a |
||||
className = 'remove-password-link' |
||||
id = 'inviteDialogRemovePassword' |
||||
onClick = { this._onClick }> |
||||
{ this.props.t('dialog.removePassword') } |
||||
</a> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Creates a ReactElement for displaying the current password. |
||||
* |
||||
* @private |
||||
* @returns {ReactElement} |
||||
*/ |
||||
_getPasswordPreviewText() { |
||||
return ( |
||||
<span> |
||||
<span> |
||||
{ `${this.props.t('dialog.currentPassword')} ` } |
||||
</span> |
||||
<span className = 'remove-password-current'> |
||||
{ this.props.lockedLocally |
||||
? this.props.password |
||||
: this.props.t('passwordSetRemotely') } |
||||
</span> |
||||
</span> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Dispatches a request to remove any set password on the JitsiConference. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onClick() { |
||||
const conference = this.props.conference; |
||||
|
||||
this.props.dispatch(setPassword( |
||||
conference, |
||||
conference.lock, |
||||
'' |
||||
)); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(RemovePasswordForm)); |
||||
@ -0,0 +1,110 @@ |
||||
import React, { Component } from 'react'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename); |
||||
|
||||
/** |
||||
* A React Component for displaying a value with a copy button that can be |
||||
* clicked to copy the value onto the clipboard. |
||||
*/ |
||||
class ShareLinkForm extends Component { |
||||
/** |
||||
* ShareLinkForm component's property types. |
||||
* |
||||
* @static |
||||
*/ |
||||
static propTypes = { |
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: React.PropTypes.func, |
||||
|
||||
/** |
||||
* The value to be displayed and copied onto the clipboard. |
||||
*/ |
||||
toCopy: React.PropTypes.string |
||||
} |
||||
|
||||
/** |
||||
* Initializes a new ShareLinkForm instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this._inputElement = null; |
||||
|
||||
this._onClick = this._onClick.bind(this); |
||||
this._setInput = this._setInput.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const inputValue = this.props.toCopy |
||||
|| this.props.t('inviteUrlDefaultMsg'); |
||||
|
||||
// FIXME input is used here instead of atlaskit field-text because
|
||||
// field-text does not currently support readonly
|
||||
return ( |
||||
<div className = 'form-control'> |
||||
<label className = 'form-control__label'> |
||||
{ this.props.t('dialog.shareLink') } |
||||
</label> |
||||
<div className = 'form-control__container'> |
||||
<input |
||||
className = 'input-control inviteLink' |
||||
id = 'inviteLinkRef' |
||||
readOnly = { true } |
||||
ref = { this._setInput } |
||||
type = 'text' |
||||
value = { inputValue } /> |
||||
<button |
||||
className = |
||||
'button-control button-control_light copyInviteLink' |
||||
onClick = { this._onClick } |
||||
type = 'button'> |
||||
{ this.props.t('dialog.copy') } |
||||
</button> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Copies the passed in value to the clipboard. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onClick() { |
||||
try { |
||||
this._inputElement.select(); |
||||
document.execCommand('copy'); |
||||
this._inputElement.blur(); |
||||
} catch (err) { |
||||
logger.error('error when copying the text', err); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets the internal reference to the DOM element for the input field so it |
||||
* may be accessed directly. |
||||
* |
||||
* @param {Object} element - DOM element for the component's input. |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_setInput(element) { |
||||
this._inputElement = element; |
||||
} |
||||
} |
||||
|
||||
export default translate(ShareLinkForm); |
||||
@ -0,0 +1 @@ |
||||
export { default as InviteDialog } from './InviteDialog'; |
||||
@ -0,0 +1,2 @@ |
||||
export * from './actions'; |
||||
export * from './components'; |
||||
@ -0,0 +1,12 @@ |
||||
/** |
||||
* The room lock state where the password was set by the current user. |
||||
* |
||||
* @type {string} |
||||
*/ |
||||
export const LOCKED_LOCALLY = 'LOCKED_LOCALLY'; |
||||
|
||||
/** |
||||
* The room lock state where the password was set by a remote user. |
||||
* @type {string} |
||||
*/ |
||||
export const LOCKED_REMOTELY = 'LOCKED_REMOTELY'; |
||||
@ -1,4 +1,5 @@ |
||||
export * from './actions'; |
||||
export * from './components'; |
||||
export * from './constants'; |
||||
|
||||
import './middleware'; |
||||
|
||||
Loading…
Reference in new issue