feat: convert invite dialog to react and redux
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.
This commit is contained in:
parent
4097be1908
commit
44b81b20e3
|
@ -2,7 +2,6 @@
|
|||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import {openConnection} from './connection';
|
||||
import Invite from './modules/UI/invite/Invite';
|
||||
import ContactList from './modules/UI/side_pannels/contactlist/ContactList';
|
||||
|
||||
import AuthHandler from './modules/UI/authentication/AuthHandler';
|
||||
|
@ -28,7 +27,8 @@ import {
|
|||
conferenceFailed,
|
||||
conferenceJoined,
|
||||
conferenceLeft,
|
||||
EMAIL_COMMAND
|
||||
EMAIL_COMMAND,
|
||||
lockStateChanged
|
||||
} from './react/features/base/conference';
|
||||
import {
|
||||
updateDeviceList
|
||||
|
@ -373,10 +373,9 @@ function createLocalTracks(options, checkForPermissionPrompt) {
|
|||
}
|
||||
|
||||
class ConferenceConnector {
|
||||
constructor(resolve, reject, invite) {
|
||||
constructor(resolve, reject) {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
this._invite = invite;
|
||||
this.reconnectTimeout = null;
|
||||
room.on(ConferenceEvents.CONFERENCE_JOINED,
|
||||
this._handleConferenceJoined.bind(this));
|
||||
|
@ -411,14 +410,17 @@ class ConferenceConnector {
|
|||
|
||||
// not enough rights to create conference
|
||||
case ConferenceErrors.AUTHENTICATION_REQUIRED:
|
||||
// schedule reconnect to check if someone else created the room
|
||||
this.reconnectTimeout = setTimeout(function () {
|
||||
room.join();
|
||||
}, 5000);
|
||||
{
|
||||
// schedule reconnect to check if someone else created the room
|
||||
this.reconnectTimeout = setTimeout(function () {
|
||||
room.join();
|
||||
}, 5000);
|
||||
|
||||
// notify user that auth is required
|
||||
AuthHandler.requireAuth(
|
||||
room, this._invite.getRoomLocker().password);
|
||||
const { password }
|
||||
= APP.store.getState()['features/base/conference'];
|
||||
|
||||
AuthHandler.requireAuth(room, password);
|
||||
}
|
||||
break;
|
||||
|
||||
case ConferenceErrors.RESERVATION_ERROR:
|
||||
|
@ -630,8 +632,7 @@ export default {
|
|||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
(new ConferenceConnector(
|
||||
resolve, reject, this.invite)).connect();
|
||||
(new ConferenceConnector(resolve, reject)).connect();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -985,7 +986,6 @@ export default {
|
|||
room = connection.initJitsiConference(APP.conference.roomName,
|
||||
this._getConferenceOptions());
|
||||
this._setLocalAudioVideoStreams(localTracks);
|
||||
this.invite = new Invite(room);
|
||||
this._room = room; // FIXME do not use this
|
||||
|
||||
_setupLocalParticipantProperties();
|
||||
|
@ -1449,6 +1449,10 @@ export default {
|
|||
APP.UI.changeDisplayName(id, formattedDisplayName);
|
||||
});
|
||||
|
||||
room.on(ConferenceEvents.LOCK_STATE_CHANGED, (...args) => {
|
||||
APP.store.dispatch(lockStateChanged(room, ...args));
|
||||
});
|
||||
|
||||
room.on(ConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
|
||||
(participant, name, oldValue, newValue) => {
|
||||
if (name === "raisedHand") {
|
||||
|
|
|
@ -83,6 +83,8 @@ $rateStarSize: 34px;
|
|||
* Modals
|
||||
*/
|
||||
$modalButtonFontSize: 14px;
|
||||
$modalMockAKInputBackground: #fafbfc;
|
||||
$modalMockAKInputBorder: 1px solid #f4f5f7;
|
||||
$modalTextColor: #333;
|
||||
|
||||
/**
|
||||
|
|
|
@ -79,6 +79,12 @@
|
|||
|
||||
.modal-dialog-form {
|
||||
color: $modalTextColor;
|
||||
|
||||
.input-control {
|
||||
background: $modalMockAKInputBackground;
|
||||
border: $modalMockAKInputBorder;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
.modal-dialog-footer {
|
||||
font-size: $modalButtonFontSize;
|
||||
|
|
|
@ -4,4 +4,44 @@
|
|||
*/
|
||||
#inviteDialogRemovePassword {
|
||||
cursor: hand;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-dialog {
|
||||
.form-control {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inviteLink {
|
||||
color: $readOnlyInputColor;
|
||||
}
|
||||
|
||||
.lock-state {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.password-overview {
|
||||
.form-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.password-overview-toggle-edit,
|
||||
.remove-password-link {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.password-overview-status,
|
||||
.remove-password {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.remove-password {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-password-current {
|
||||
color: $inputControlEmColor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,8 +153,7 @@
|
|||
"cameraAndMic": "Camera and microphone",
|
||||
"moderator": "MODERATOR",
|
||||
"password": "SET PASSWORD",
|
||||
"audioVideo": "AUDIO AND VIDEO",
|
||||
"setPasswordLabel": "Lock your room with a password."
|
||||
"audioVideo": "AUDIO AND VIDEO"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile",
|
||||
|
@ -225,8 +224,6 @@
|
|||
"connecting": "Connecting",
|
||||
"copy": "Copy",
|
||||
"error": "Error",
|
||||
"roomLocked": "This call is locked. New callers must have the link and enter the password to join",
|
||||
"addPassword": "Add a password",
|
||||
"createPassword": "Create password",
|
||||
"detectext": "Error when trying to detect desktopsharing extension.",
|
||||
"failtoinstall": "Failed to install desktop sharing extension",
|
||||
|
@ -429,5 +426,13 @@
|
|||
"noOtherDevices": "No other devices available",
|
||||
"selectADevice": "Select a device",
|
||||
"testAudio": "Test sound"
|
||||
},
|
||||
"invite": {
|
||||
"addPassword": "Add password",
|
||||
"hidePassword": "Hide password",
|
||||
"inviteTo": "Invite people to __conferenceName__",
|
||||
"locked": "This call is locked. New callers must have the link and enter the password to join.",
|
||||
"showPassword": "Show password",
|
||||
"unlocked": "This call is unlocked. Any new caller with the link may join the call."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
};
|
||||
}
|
|
@ -19,14 +19,14 @@ class ContactList {
|
|||
}
|
||||
|
||||
/**
|
||||
* Is locked flag.
|
||||
* Delegates to Invite module
|
||||
* TO FIX: find a better way to access the IS LOCKED state of the invite.
|
||||
* Returns true if the current conference is locked.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isLocked() {
|
||||
return APP.conference.invite.isLocked();
|
||||
const conference = APP.store.getState()['features/base/conference'];
|
||||
|
||||
return conference.locked;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* global $, APP, interfaceConfig */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import { openInviteDialog } from '../../../../react/features/invite';
|
||||
|
||||
import Avatar from '../../avatar/Avatar';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
|
@ -96,7 +98,7 @@ var ContactListView = {
|
|||
this.model = model;
|
||||
this.addInviteButton();
|
||||
this.registerListeners();
|
||||
this.toggleLock();
|
||||
this.setLockDisplay(false);
|
||||
},
|
||||
/**
|
||||
* Adds layout for invite button
|
||||
|
@ -108,8 +110,9 @@ var ContactListView = {
|
|||
.insertAdjacentHTML('afterend', this.getInviteButtonLayout());
|
||||
|
||||
APP.translation.translateElement($(container));
|
||||
|
||||
$(document).on('click', '#addParticipantsBtn', () => {
|
||||
APP.UI.emitEvent(UIEvents.INVITE_CLICKED);
|
||||
APP.store.dispatch(openInviteDialog());
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
@ -125,8 +128,8 @@ var ContactListView = {
|
|||
|
||||
return (
|
||||
`<div class="sideToolbarBlock first">
|
||||
<button id="addParticipantsBtn"
|
||||
data-i18n="${key}"
|
||||
<button id="addParticipantsBtn"
|
||||
data-i18n="${key}"
|
||||
class="${classes}"></button>
|
||||
<div>
|
||||
${lockedHtml}
|
||||
|
@ -158,7 +161,7 @@ var ContactListView = {
|
|||
let displayNameChange = this.onDisplayNameChange.bind(this);
|
||||
|
||||
APP.UI.addListener( UIEvents.TOGGLE_ROOM_LOCK,
|
||||
this.toggleLock.bind(this));
|
||||
this.setLockDisplay.bind(this));
|
||||
APP.UI.addListener( UIEvents.CONTACT_ADDED,
|
||||
this.onAddContact.bind(this));
|
||||
|
||||
|
@ -167,16 +170,15 @@ var ContactListView = {
|
|||
APP.UI.addListener(UIEvents.DISPLAY_NAME_CHANGED, displayNameChange);
|
||||
},
|
||||
/**
|
||||
* Updating the view according the model
|
||||
* @param type {String} type of change
|
||||
* @returns {Promise}
|
||||
* Updates the view according to the passed in lock state.
|
||||
*
|
||||
* @param {boolean} isLocked - True if the locked UI state should display.
|
||||
*/
|
||||
toggleLock() {
|
||||
let isLocked = this.model.isLocked();
|
||||
let showKey = isLocked ? this.lockKey : this.unlockKey;
|
||||
let hideKey = !isLocked ? this.lockKey : this.unlockKey;
|
||||
let showId = `contactList${showKey}`;
|
||||
let hideId = `contactList${hideKey}`;
|
||||
setLockDisplay(isLocked) {
|
||||
const showKey = isLocked ? this.lockKey : this.unlockKey;
|
||||
const hideKey = !isLocked ? this.lockKey : this.unlockKey;
|
||||
const showId = `contactList${showKey}`;
|
||||
const hideId = `contactList${hideKey}`;
|
||||
|
||||
$(`#${showId}`).show();
|
||||
$(`#${hideId}`).hide();
|
||||
|
|
|
@ -116,6 +116,17 @@ export const SET_LASTN = Symbol('SET_LASTN');
|
|||
*/
|
||||
export const SET_PASSWORD = Symbol('SET_PASSWORD');
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that setting a password on a
|
||||
* JitsiConference encountered an error and failed.
|
||||
*
|
||||
* {
|
||||
* type: SET_PASSWORD_FAILED,
|
||||
* error: string
|
||||
* }
|
||||
*/
|
||||
export const SET_PASSWORD_FAILED = Symbol('SET_PASSWORD_FAILED');
|
||||
|
||||
/**
|
||||
* The type of the Redux action which sets the name of the room of the
|
||||
* conference to be joined.
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
_SET_AUDIO_ONLY_VIDEO_MUTED,
|
||||
SET_LASTN,
|
||||
SET_PASSWORD,
|
||||
SET_PASSWORD_FAILED,
|
||||
SET_ROOM
|
||||
} from './actionTypes';
|
||||
import {
|
||||
|
@ -56,7 +57,7 @@ function _addConferenceListeners(conference, dispatch) {
|
|||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.LOCK_STATE_CHANGED,
|
||||
(...args) => dispatch(_lockStateChanged(conference, ...args)));
|
||||
(...args) => dispatch(lockStateChanged(conference, ...args)));
|
||||
|
||||
// Dispatches into features/base/tracks follow:
|
||||
|
||||
|
@ -292,7 +293,7 @@ export function createConference() {
|
|||
* locked: boolean
|
||||
* }}
|
||||
*/
|
||||
function _lockStateChanged(conference, locked) {
|
||||
export function lockStateChanged(conference, locked) {
|
||||
return {
|
||||
type: LOCK_STATE_CHANGED,
|
||||
conference,
|
||||
|
@ -440,7 +441,12 @@ export function setPassword(conference, method, password) {
|
|||
conference,
|
||||
method,
|
||||
password
|
||||
})));
|
||||
}))
|
||||
.catch(error => dispatch({
|
||||
type: SET_PASSWORD_FAILED,
|
||||
error
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
|
||||
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import {
|
||||
ReducerRegistry,
|
||||
|
@ -83,7 +85,7 @@ function _conferenceFailed(state, action) {
|
|||
audioOnlyVideoMuted: undefined,
|
||||
conference: undefined,
|
||||
leaving: undefined,
|
||||
locked: undefined,
|
||||
locked: passwordRequired ? LOCKED_REMOTELY : undefined,
|
||||
password: undefined,
|
||||
|
||||
/**
|
||||
|
@ -112,7 +114,7 @@ function _conferenceJoined(state, action) {
|
|||
// i.e. password-protected is private to lib-jitsi-meet. However, the
|
||||
// library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
|
||||
// with a password.
|
||||
const locked = conference.room.locked || undefined;
|
||||
const locked = conference.room.locked ? LOCKED_REMOTELY : undefined;
|
||||
|
||||
return (
|
||||
setStateProperties(state, {
|
||||
|
@ -209,7 +211,16 @@ function _lockStateChanged(state, action) {
|
|||
return state;
|
||||
}
|
||||
|
||||
return setStateProperty(state, 'locked', action.locked || undefined);
|
||||
let lockState;
|
||||
|
||||
if (action.locked) {
|
||||
lockState = state.locked || LOCKED_REMOTELY;
|
||||
}
|
||||
|
||||
return setStateProperties(state, {
|
||||
locked: lockState,
|
||||
password: action.locked ? state.password : null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,10 +265,12 @@ function _setPassword(state, action) {
|
|||
const conference = action.conference;
|
||||
|
||||
switch (action.method) {
|
||||
case conference.join:
|
||||
case conference.join: {
|
||||
if (state.passwordRequired === conference) {
|
||||
return (
|
||||
setStateProperties(state, {
|
||||
locked: LOCKED_REMOTELY,
|
||||
|
||||
/**
|
||||
* The password with which the conference is to be joined.
|
||||
*
|
||||
|
@ -267,8 +280,16 @@ function _setPassword(state, action) {
|
|||
passwordRequired: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case conference.lock: {
|
||||
return setStateProperties(state, {
|
||||
locked: action.password ? LOCKED_LOCALLY : undefined,
|
||||
password: action.password
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -3,6 +3,8 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import AKFieldText from '@atlaskit/field-text';
|
||||
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
import { setPassword } from '../../base/conference';
|
||||
import { Dialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
|
@ -109,9 +111,9 @@ class PasswordRequiredPrompt extends Component {
|
|||
// password required will be received and the room again
|
||||
// will be marked as locked.
|
||||
if (!this.state.password || this.state.password === '') {
|
||||
// XXX temporary solution till we move the whole invite logic
|
||||
// in react
|
||||
APP.conference.invite.setLockedFromElsewhere(false);
|
||||
// XXX temporary solution while some components are not listening
|
||||
// for lock state updates in redux
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, false);
|
||||
}
|
||||
|
||||
this.props.dispatch(setPassword(
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
/* global APP */
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
import { CONFERENCE_FAILED } from '../base/conference';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
LOCK_STATE_CHANGED,
|
||||
SET_PASSWORD_FAILED
|
||||
} from '../base/conference';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import { _showPasswordDialog } from './actions';
|
||||
|
||||
/**
|
||||
|
@ -20,17 +28,60 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
if (action.conference
|
||||
&& JitsiConferenceErrors.PASSWORD_REQUIRED === action.error) {
|
||||
// XXX temporary solution till we move the whole invite
|
||||
// logic in react
|
||||
// XXX temporary solution while some components are not listening
|
||||
// for lock state updates in redux
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.conference.invite.setLockedFromElsewhere(true);
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, true);
|
||||
}
|
||||
|
||||
store.dispatch(_showPasswordDialog(action.conference));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LOCK_STATE_CHANGED: {
|
||||
// TODO Remove this logic when all components interested in the lock
|
||||
// state change event are moved into react/redux.
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, action.locked);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case SET_PASSWORD_FAILED:
|
||||
return _notifySetPasswordError(store, next, action);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles errors that occur when a password is failed to be set.
|
||||
*
|
||||
* @param {Store} store - The Redux store in which the specified action is being
|
||||
* dispatched.
|
||||
* @param {Dispatch} next - The Redux dispatch function to dispatch the
|
||||
* specified action to the specified store.
|
||||
* @param {Action} action - The Redux action SET_PASSWORD_ERROR which has the
|
||||
* error type that should be handled.
|
||||
* @private
|
||||
* @returns {Object} The new state that is the result of the reduction of the
|
||||
* specified action.
|
||||
*/
|
||||
function _notifySetPasswordError(store, next, action) {
|
||||
if (typeof APP !== 'undefined') {
|
||||
// TODO remove this logic when displaying of error messages on web is
|
||||
// handled through react/redux
|
||||
if (action.error
|
||||
=== JitsiMeetJS.errors.conference.PASSWORD_NOT_SUPPORTED) {
|
||||
logger.warn('room passwords not supported');
|
||||
APP.UI.messageHandler.showError(
|
||||
'dialog.warning', 'dialog.passwordNotSupported');
|
||||
} else {
|
||||
logger.warn('setting password failed', action.error);
|
||||
APP.UI.messageHandler.showError(
|
||||
'dialog.lockTitle', 'dialog.lockMessage');
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
|
|
@ -322,7 +322,7 @@ function _mapStateToProps(state) {
|
|||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
_locked: conference.locked
|
||||
_locked: Boolean(conference.locked)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -223,7 +223,6 @@ function _mapDispatchToProps(dispatch: Function): Object {
|
|||
* @returns {{
|
||||
* _alwaysVisible: boolean,
|
||||
* _audioMuted: boolean,
|
||||
* _locked: boolean,
|
||||
* _subjectSlideIn: boolean,
|
||||
* _videoMuted: boolean
|
||||
* }}
|
||||
|
|
|
@ -4,6 +4,8 @@ import React from 'react';
|
|||
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import { openInviteDialog } from '../invite';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
declare var JitsiMeetJS: Object;
|
||||
|
@ -222,7 +224,7 @@ export default {
|
|||
id: 'toolbar_button_link',
|
||||
onClick() {
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
|
||||
APP.UI.emitEvent(UIEvents.INVITE_CLICKED);
|
||||
APP.store.dispatch(openInviteDialog());
|
||||
},
|
||||
tooltipKey: 'toolbar.invite'
|
||||
},
|
||||
|
|
|
@ -22,10 +22,6 @@ export default {
|
|||
VIDEO_MUTED: "UI.video_muted",
|
||||
ETHERPAD_CLICKED: "UI.etherpad_clicked",
|
||||
SHARED_VIDEO_CLICKED: "UI.start_shared_video",
|
||||
/**
|
||||
* Indicates that an invite button has been clicked.
|
||||
*/
|
||||
INVITE_CLICKED: "UI.invite_clicked",
|
||||
/**
|
||||
* Updates shared video with params: url, state, time(optional)
|
||||
* Where url is the video link, state is stop/start/pause and time is the
|
||||
|
|
Loading…
Reference in New Issue