mirror of https://github.com/jitsi/jitsi-meet
parent
d5541f612f
commit
74f31db434
@ -0,0 +1,84 @@ |
||||
/** |
||||
* Project animations |
||||
**/ |
||||
|
||||
/** |
||||
* START of slide in animation for extended toolbar. |
||||
*/ |
||||
@include keyframes(slideInX) { |
||||
0% { transform: translateX(-100%); } |
||||
100% { transform: translateX(0%); } |
||||
} |
||||
|
||||
@include keyframes(slideOutX) { |
||||
0% { transform: translateX(0%); } |
||||
100% { transform: translateX(-100%); } |
||||
} |
||||
|
||||
@include keyframes(slideInExtX) { |
||||
0% { transform: translateX(-500%); } |
||||
100% { transform: translateX(0%); } |
||||
} |
||||
|
||||
@include keyframes(slideOutExtX) { |
||||
0% { transform: translateX(0%); } |
||||
100% { transform: translateX(-500%); } |
||||
} |
||||
|
||||
/** |
||||
* END of slide out animation for extended toolbar. |
||||
*/ |
||||
|
||||
/** |
||||
* START of slide in / out animation for main toolbar. |
||||
*/ |
||||
@include keyframes(slideInY) { |
||||
100% { transform: translateY(0%); } |
||||
} |
||||
|
||||
@include keyframes(slideOutY) { |
||||
0% { transform: translateY(0%); } |
||||
100% { transform: translateY(-100%); } |
||||
} |
||||
|
||||
/** |
||||
* END of slide in / out animation for main toolbar. |
||||
*/ |
||||
|
||||
/** |
||||
* START of slide in animation for extended toolbar (inner) panel. |
||||
*/ |
||||
|
||||
// FIX: Can't use percentage because of breaking animation when width is changed |
||||
// (100% of 0 is also zero) Extracted this to config variable. |
||||
@include keyframes(slideInExt) { |
||||
from { left: -$sidebarWidth; } |
||||
to { left: 0; } |
||||
} |
||||
|
||||
@include keyframes(slideOutExt) { |
||||
from { left: 0; } |
||||
to { left: -$sidebarWidth; } |
||||
} |
||||
|
||||
/** |
||||
* END of slide in animation for extended toolbar (inner) panel. |
||||
*/ |
||||
|
||||
/** |
||||
* START of slide in animation for extended toolbar container |
||||
**/ |
||||
|
||||
@include keyframes(slideOutExtContainer) { |
||||
from { width: $sidebarWidth; } |
||||
to { width: 0; } |
||||
} |
||||
|
||||
@include keyframes(slideInExtContainer) { |
||||
from { width: 0; } |
||||
to { width: $sidebarWidth; } |
||||
} |
||||
|
||||
/** |
||||
* END of slide in animation for extended toolbar container |
||||
**/ |
@ -0,0 +1,6 @@ |
||||
/* Functions */ |
||||
|
||||
/* Pixels to Ems function */ |
||||
@function em($value, $base: 16) { |
||||
@return #{$value / $base}em; |
||||
} |
@ -0,0 +1,81 @@ |
||||
.button-control { |
||||
box-sizing: border-box; |
||||
display: inline-block; |
||||
border: 1px solid $buttonBorder; |
||||
vertical-align: baseline; |
||||
height: 30px; |
||||
padding: 4px 10px; |
||||
margin: 0; |
||||
line-height: 1.5em; |
||||
outline: none; |
||||
background-color: transparent; |
||||
float: right; |
||||
font-size: 14px; |
||||
margin-left: 10px; |
||||
color: $buttonColor; |
||||
letter-spacing: $letterSpacing; |
||||
font-weight: $buttonFontWeight; |
||||
@include transition(background-color .1s ease-out); |
||||
|
||||
&[disabled] { |
||||
color: #666; |
||||
cursor: default; |
||||
} |
||||
|
||||
&_full-width { |
||||
margin: 0; |
||||
width: 100%; |
||||
} |
||||
|
||||
&:hover { |
||||
border: 1px solid $buttonHoverBorder; |
||||
background-color: $buttonHoverBackground; |
||||
@include transition(background-color .1s ease-in); |
||||
} |
||||
|
||||
&:active { |
||||
@include box-shadow(inset, 0, 0, 1px, $buttonShadowColor); |
||||
} |
||||
|
||||
&_light { |
||||
color: $defaultDarkColor; |
||||
background-color: $buttonLightBackground; |
||||
border: 1px solid $buttonLightBorder; |
||||
|
||||
&:hover { |
||||
border: 1px solid $buttonLightHoverBorder; |
||||
background-color: $buttonLightHoverBackground; |
||||
} |
||||
} |
||||
|
||||
&_link { |
||||
color: $buttonLinkColor; |
||||
background-color: $buttonLinkBackground; |
||||
|
||||
&:hover { |
||||
background-color: $buttonLinkBackground; |
||||
} |
||||
} |
||||
|
||||
&_primary { |
||||
background-color: $primaryButtonBackground; |
||||
border: 1px solid $primaryButtonBackground; |
||||
color: $primaryButtonColor; |
||||
font-weight: $primaryButtonFontWeight; |
||||
|
||||
&:hover { |
||||
border: 1px solid $primaryButtonHoverBackground; |
||||
background-color: $primaryButtonHoverBackground; |
||||
} |
||||
} |
||||
|
||||
&_close { |
||||
color: $defaultFontColor; |
||||
} |
||||
&_submit { |
||||
color: $linkFontColor; |
||||
&:hover { |
||||
color: $linkHoverFontColor; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
.input-control { |
||||
padding: 16px 0; |
||||
|
||||
&__text { |
||||
margin: 8px 0; |
||||
font-size: 1em |
||||
} |
||||
|
||||
&__label { |
||||
font-size: 1em; |
||||
font-weight: $labelFontWeight; |
||||
} |
||||
|
||||
&__input { |
||||
margin: 8px 0; |
||||
|
||||
&::selection { |
||||
background-color: $defaultDarkSelectionColor; |
||||
} |
||||
} |
||||
|
||||
&__em { |
||||
color: $inputControlEmColor; |
||||
} |
||||
|
||||
&__hint { |
||||
margin-top: 0; |
||||
font-size: $hintFontSize; |
||||
|
||||
span { |
||||
vertical-align: middle; |
||||
} |
||||
} |
||||
|
||||
&__container { |
||||
position: relative; |
||||
width: 100%; |
||||
@include flex(); |
||||
|
||||
.button-control { |
||||
margin: 9px 0 9px 10px; |
||||
} |
||||
} |
||||
|
||||
&__right { |
||||
position: absolute; |
||||
right: 0; |
||||
} |
||||
} |
@ -1,53 +1,80 @@ |
||||
.dialog{ |
||||
.dialog { |
||||
visibility: visible; |
||||
height: auto; |
||||
|
||||
p { |
||||
color: $defaultDarkColor; |
||||
h3 { |
||||
color: $auiDialogColor; |
||||
} |
||||
textarea { |
||||
background: none; |
||||
border: 1px solid $inputBorderColor; |
||||
} |
||||
.aui-dialog2-content:last-child { |
||||
border-bottom-right-radius: 5px; |
||||
border-bottom-left-radius: 5px; |
||||
} |
||||
.aui-dialog2-content:first-child { |
||||
border-top-right-radius: 5px; |
||||
border-top-left-radius: 5px; |
||||
} |
||||
.aui-dialog2-footer{ |
||||
border-top: 0; |
||||
border-radius: 0; |
||||
padding-top: 0; |
||||
background: none; |
||||
border: none; |
||||
height: auto; |
||||
margin-top: 10px; |
||||
} |
||||
.aui-button { |
||||
height: 28px; |
||||
font-size: 12px; |
||||
padding: 3px 6px 3px 6px; |
||||
border: none; |
||||
box-shadow: none; |
||||
outline: none; |
||||
|
||||
&_close { |
||||
font-weight: 400 !important; |
||||
color: $buttonBackground; |
||||
background: none !important; |
||||
|
||||
:hover { |
||||
text-decoration: underline; |
||||
|
||||
.aui { |
||||
|
||||
&-icon { |
||||
color: $auiDialogColor; |
||||
|
||||
&-small { |
||||
width: 14px; |
||||
height: 14px; |
||||
} |
||||
} |
||||
&_submit { |
||||
font-weight: 700 !important; |
||||
color: $defaultColor; |
||||
background: $buttonBackground; |
||||
border-radius: 3px; |
||||
|
||||
&-iconfont-close-dialog { |
||||
cursor: pointer; |
||||
right: 20px; |
||||
position: absolute; |
||||
top: -49px; |
||||
} |
||||
|
||||
&-dialog2 { |
||||
&-header, &-footer { |
||||
background-color: $auiDialogBg; |
||||
border: none; |
||||
} |
||||
|
||||
&-header { |
||||
height: em(58, 12); |
||||
border-bottom: 1px solid $auiBorderColor; |
||||
|
||||
h2 { |
||||
font-size: em(20, 12); |
||||
font-weight: $dialogTitleFontWeight; |
||||
letter-spacing: $titleLetterSpacing; |
||||
color: $auiDialogColor; |
||||
} |
||||
|
||||
&-main { |
||||
padding-right: 0; |
||||
} |
||||
} |
||||
|
||||
&-footer { |
||||
border-top: 1px solid $auiBorderColor; |
||||
} |
||||
|
||||
&-content { |
||||
font-size: em(14, 12); |
||||
min-height: 0; |
||||
background-color: $auiDialogContentBg; |
||||
color: $auiDialogColor; |
||||
|
||||
p,span, h3 { |
||||
font-weight: $labelFontWeight; |
||||
letter-spacing: $letterSpacing; |
||||
} |
||||
|
||||
&:last-child { |
||||
border-bottom-right-radius: 5px; |
||||
border-bottom-left-radius: 5px; |
||||
} |
||||
|
||||
&:first-child { |
||||
border-top-right-radius: 5px; |
||||
border-top-left-radius: 5px; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.input-control:not(:last-child) { |
||||
border-bottom: 1px solid $auiBorderColor; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,4 @@ |
||||
/* Import shortcuts blocks */ |
||||
|
||||
@import 'regular-key'; |
||||
@import 'shortcuts-list'; |
@ -0,0 +1,11 @@ |
||||
.regular-key { |
||||
display: table-cell; |
||||
width: 25px; |
||||
height: 20px; |
||||
padding: 0; |
||||
text-align: center; |
||||
vertical-align: middle; |
||||
font-family: $baseFontFamily; |
||||
color: $defaultDarkColor; |
||||
font-size: 12px; |
||||
} |
@ -0,0 +1,12 @@ |
||||
.shortcuts-list { |
||||
padding: 0; |
||||
|
||||
&__description { |
||||
margin-left: em(16, 14); |
||||
vertical-align: top; |
||||
} |
||||
|
||||
&__item { |
||||
margin-bottom: em(7, 14); |
||||
} |
||||
} |
@ -0,0 +1,46 @@ |
||||
/** |
||||
* Buttons |
||||
*/ |
||||
$buttonBackground: #f5f5f5; |
||||
$buttonHoverBackground: #e9e9e9; |
||||
$buttonBorder: #ccc; |
||||
$buttonHoverBorder: #999; |
||||
$buttonColor: #333; |
||||
|
||||
$buttonLightBackground: #f5f5f5; |
||||
$buttonLightHoverBackground: #e9e9e9; |
||||
$buttonLightBorder: #ccc; |
||||
$buttonLightHoverBorder: #999; |
||||
|
||||
$buttonLinkBackground: transparent; |
||||
$buttonLinkColor: #0090e8; |
||||
|
||||
$primaryButtonBackground: #3572b0; |
||||
$primaryButtonHoverBackground: #2a67a5; |
||||
$primaryButtonColor: #fff; |
||||
$primaryButtonFontWeight: 400; |
||||
|
||||
$buttonShadowColor: #192d4f; |
||||
|
||||
/** |
||||
* Dialog colors |
||||
**/ |
||||
$auiDialogColor: #333; |
||||
$auiDialogBg: #f5f5f5; |
||||
$auiDialogContentBg: #fff; |
||||
$auiBorderColor: #ccc; |
||||
$dialogTitleFontWeight: 400; |
||||
|
||||
// Main controls |
||||
$inputBackground: #fff; |
||||
$inputBorderColor: #ccc; |
||||
$inputColor: #333; |
||||
$defaultDarkSelectionColor: #ccc; |
||||
$titleLetterSpacing: 0; |
||||
$letterSpacing: 0; |
||||
$buttonFontWeight: 400; |
||||
$labelFontWeight: 400; |
||||
$hintFontSize: em(13, 14); |
||||
$linkFontColor: #3572b0; |
||||
$linkHoverFontColor: darken(#3572b0, 10%); |
||||
$dropdownColor: #333; |
@ -1,244 +0,0 @@ |
||||
/* global APP, JitsiMeetJS */ |
||||
import UIUtil from '../util/UIUtil'; |
||||
|
||||
/** |
||||
* Show dialog which asks user for new password for the conference. |
||||
* @returns {Promise<string>} password or nothing if user canceled |
||||
*/ |
||||
function askForNewPassword () { |
||||
let passMsg = APP.translation.generateTranslationHTML("dialog.passwordMsg"); |
||||
let yourPassMsg = APP.translation.translateString("dialog.yourPassword"); |
||||
let msg = ` |
||||
<h2>${passMsg}</h2> |
||||
<input name="lockKey" type="text" |
||||
data-i18n="[placeholder]dialog.yourPassword" |
||||
placeholder="${yourPassMsg}" autofocus> |
||||
`;
|
||||
|
||||
return new Promise(function (resolve, reject) { |
||||
APP.UI.messageHandler.openTwoButtonDialog( |
||||
null, null, null, |
||||
msg, false, "dialog.Save", |
||||
function (e, v, m, f) { |
||||
if (v && f.lockKey) { |
||||
resolve(UIUtil.escapeHtml(f.lockKey)); |
||||
} |
||||
else { |
||||
reject(APP.UI.messageHandler.CANCEL); |
||||
} |
||||
}, |
||||
null, null, 'input:first' |
||||
); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Show dialog which asks for required conference password. |
||||
* @returns {Promise<string>} password or nothing if user canceled |
||||
*/ |
||||
function askForPassword () { |
||||
let passRequiredMsg = APP.translation.translateString( |
||||
"dialog.passwordRequired" |
||||
); |
||||
let passMsg = APP.translation.translateString("dialog.password"); |
||||
let msg = ` |
||||
<h2 data-i18n="dialog.passwordRequired">${passRequiredMsg}</h2> |
||||
<input name="lockKey" type="text" |
||||
data-i18n="[placeholder]dialog.password" |
||||
placeholder="${passMsg}" autofocus> |
||||
`;
|
||||
return new Promise(function (resolve, reject) { |
||||
APP.UI.messageHandler.openTwoButtonDialog( |
||||
null, null, null, msg, |
||||
true, "dialog.Ok", |
||||
function () {}, null, |
||||
function (e, v, m, f) { |
||||
if (v && f.lockKey) { |
||||
resolve(UIUtil.escapeHtml(f.lockKey)); |
||||
} else { |
||||
reject(APP.UI.messageHandler.CANCEL); |
||||
} |
||||
}, |
||||
':input:first' |
||||
); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Show dialog which asks if user want remove password from the conference. |
||||
* @returns {Promise} |
||||
*/ |
||||
function askToUnlock () { |
||||
return new Promise(function (resolve, reject) { |
||||
APP.UI.messageHandler.openTwoButtonDialog( |
||||
null, null, "dialog.passwordCheck", |
||||
null, false, "dialog.Remove", |
||||
function (e, v) { |
||||
if (v) { |
||||
resolve(); |
||||
} else { |
||||
reject(APP.UI.messageHandler.CANCEL); |
||||
} |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Show notification that user cannot set password for the conference |
||||
* because server doesn't support that. |
||||
*/ |
||||
function notifyPasswordNotSupported () { |
||||
console.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) { |
||||
console.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; |
||||
let dialog = null; |
||||
|
||||
/** |
||||
* 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; |
||||
|
||||
function lock (newPass) { |
||||
return room.lock(newPass).then(function () { |
||||
password = newPass; |
||||
}).catch(function (err) { |
||||
console.error(err); |
||||
if (err === ConferenceErrors.PASSWORD_NOT_SUPPORTED) { |
||||
notifyPasswordNotSupported(); |
||||
} else { |
||||
notifyPasswordFailed(err); |
||||
} |
||||
throw err; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @class RoomLocker |
||||
*/ |
||||
return { |
||||
get isLocked () { |
||||
return !!password || lockedElsewhere; |
||||
}, |
||||
|
||||
get password () { |
||||
return password; |
||||
}, |
||||
|
||||
/** |
||||
* 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; |
||||
}, |
||||
|
||||
/** |
||||
* Allows to remove password from the conference (asks user first). |
||||
* @returns {Promise} |
||||
*/ |
||||
askToUnlock () { |
||||
return askToUnlock().then( |
||||
() => { return lock(); } |
||||
).then(function () { |
||||
JitsiMeetJS.analytics.sendEvent('toolbar.lock.disabled'); |
||||
}).catch( |
||||
reason => { |
||||
if (reason !== APP.UI.messageHandler.CANCEL) |
||||
console.error(reason); |
||||
} |
||||
); |
||||
}, |
||||
|
||||
/** |
||||
* Allows to set password for the conference. |
||||
* It asks user for new password and locks the room. |
||||
* @returns {Promise} |
||||
*/ |
||||
askToLock () { |
||||
return askForNewPassword().then( |
||||
newPass => { return lock(newPass);} |
||||
).then(function () { |
||||
JitsiMeetJS.analytics.sendEvent('toolbar.lock.enabled'); |
||||
}).catch( |
||||
reason => { |
||||
if (reason !== APP.UI.messageHandler.CANCEL) |
||||
console.error(reason); |
||||
} |
||||
); |
||||
}, |
||||
|
||||
/** |
||||
* Asks user for required conference password. |
||||
*/ |
||||
requirePassword () { |
||||
return askForPassword().then( |
||||
newPass => { password = newPass; } |
||||
).catch( |
||||
reason => { |
||||
// user canceled, no pass was entered.
|
||||
// clear, as if we use the same instance several times
|
||||
// pass stays between attempts
|
||||
password = null; |
||||
if (reason !== APP.UI.messageHandler.CANCEL) |
||||
console.error(reason); |
||||
} |
||||
); |
||||
}, |
||||
|
||||
/** |
||||
* Show notification that to set/remove password user must be moderator. |
||||
*/ |
||||
notifyModeratorRequired () { |
||||
if (dialog) |
||||
return; |
||||
|
||||
let closeCallback = function () { |
||||
dialog = null; |
||||
}; |
||||
|
||||
if (this.isLocked) { |
||||
dialog = APP.UI.messageHandler |
||||
.openMessageDialog(null, "dialog.passwordError", |
||||
null, null, closeCallback); |
||||
} else { |
||||
dialog = APP.UI.messageHandler |
||||
.openMessageDialog(null, "dialog.passwordError2", |
||||
null, null, closeCallback); |
||||
} |
||||
} |
||||
}; |
||||
} |
@ -0,0 +1,33 @@ |
||||
/* global APP, $ */ |
||||
|
||||
import UIUtil from '../util/UIUtil'; |
||||
|
||||
/** |
||||
* Show dialog which asks for required conference password. |
||||
* @returns {Promise<string>} password or nothing if user canceled |
||||
*/ |
||||
export default function askForPassword () { |
||||
let titleKey = "dialog.passwordRequired"; |
||||
let passMsg = APP.translation.translateString("dialog.password"); |
||||
let msgString = ` |
||||
<input name="lockKey" type="text" |
||||
data-i18n="[placeholder]dialog.password" |
||||
placeholder="${passMsg}" autofocus> |
||||
`;
|
||||
return new Promise(function (resolve, reject) { |
||||
APP.UI.messageHandler.openTwoButtonDialog({ |
||||
titleKey, |
||||
msgString, |
||||
leftButtonKey: "dialog.Ok", |
||||
submitFunction: $.noop, |
||||
closeFunction: function (e, v, m, f) { |
||||
if (v && f.lockKey) { |
||||
resolve(UIUtil.escapeHtml(f.lockKey)); |
||||
} else { |
||||
reject(APP.UI.messageHandler.CANCEL); |
||||
} |
||||
}, |
||||
focus: ':input:first' |
||||
}); |
||||
}); |
||||
} |
@ -0,0 +1,216 @@ |
||||
/* global JitsiMeetJS, APP */ |
||||
|
||||
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.createRoomLocker(conference); |
||||
this.initDialog(); |
||||
this.registerListeners(); |
||||
} |
||||
|
||||
/** |
||||
* Registering listeners. |
||||
* Primarily listeners for conference events. |
||||
*/ |
||||
registerListeners() { |
||||
|
||||
this.conference.on(ConferenceEvents.LOCK_STATE_CHANGED, |
||||
(locked, error) => { |
||||
|
||||
console.log("Received channel password lock change: ", locked, |
||||
error); |
||||
|
||||
if (!locked) { |
||||
this.roomLocker.resetPassword(); |
||||
} |
||||
|
||||
this.setLockedFromElsewhere(locked); |
||||
}); |
||||
|
||||
this.conference.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => { |
||||
if (APP.conference.isLocalId(id) |
||||
&& this.isModerator !== this.conference.isModerator) { |
||||
|
||||
this.setModerator(this.conference.isModerator); |
||||
} |
||||
}); |
||||
|
||||
APP.UI.addListener( UIEvents.INVITE_CLICKED, |
||||
() => { this.openLinkDialog(); }); |
||||
|
||||
APP.UI.addListener( UIEvents.INVITE_URL_INITIALISED, |
||||
(inviteUrl) => { |
||||
this.updateInviteUrl(inviteUrl); |
||||
}); |
||||
|
||||
APP.UI.addListener( UIEvents.PASSWORD_REQUIRED, |
||||
() => { |
||||
this.setLockedFromElsewhere(true); |
||||
this.roomLocker.requirePassword().then(() => { |
||||
let pass = this.roomLocker.password; |
||||
// we received that password is required, but user is trying
|
||||
// anyway to login without a password, mark room as not
|
||||
// locked in case he succeeds (maybe someone removed the
|
||||
// password meanwhile), if it is still locked another
|
||||
// password required will be received and the room again
|
||||
// will be marked as locked.
|
||||
if (!pass) |
||||
this.setLockedFromElsewhere(false); |
||||
this.conference.join(this.roomLocker.password); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 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.password = this.getPassword(); |
||||
this.view = new InviteDialogView(this); |
||||
} |
||||
|
||||
/** |
||||
* Password getter |
||||
* @returns {String} password |
||||
*/ |
||||
getPassword() { |
||||
return this.roomLocker.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.roomLocker.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.roomLocker.isLocked)) { |
||||
this.roomLocker.lock(newPass).then(() => { |
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK); |
||||
this.updateView(); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Updates the room invite url. |
||||
*/ |
||||
updateInviteUrl (newInviteUrl) { |
||||
this.inviteUrl = newInviteUrl; |
||||
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.roomLocker.isLocked; |
||||
} |
||||
|
||||
/** |
||||
* Set flag locked from elsewhere to room locker. |
||||
* @param isLocked |
||||
*/ |
||||
setLockedFromElsewhere(isLocked) { |
||||
let oldLockState = this.roomLocker.lockedElsewhere; |
||||
if (oldLockState !== isLocked) { |
||||
this.roomLocker.lockedElsewhere = isLocked; |
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK); |
||||
this.updateView(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export default Invite; |
@ -0,0 +1,382 @@ |
||||
/* global $, APP, JitsiMeetJS */ |
||||
|
||||
/** |
||||
* 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) { |
||||
let InviteAttributesKey = 'inviteUrlDefaultMsg'; |
||||
let title = APP.translation.translateString(InviteAttributesKey); |
||||
|
||||
this.unlockHint = "unlockHint"; |
||||
this.lockHint = "lockHint"; |
||||
this.model = model; |
||||
|
||||
if (this.model.inviteUrl === null) { |
||||
this.inviteAttributes = ( |
||||
`data-i18n="[value]inviteUrlDefaultMsg" value="${title}"` |
||||
); |
||||
} else { |
||||
let encodedInviteUrl = this.model.getEncodedInviteUrl(); |
||||
this.inviteAttributes = `value="${encodedInviteUrl}"`; |
||||
} |
||||
|
||||
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); |
||||
|
||||
let titleKey = "dialog.shareLink"; |
||||
let titleString = APP.translation.generateTranslationHTML(titleKey); |
||||
|
||||
dialog.titleKey = titleKey; |
||||
dialog.titleString = titleString; |
||||
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 { |
||||
titleString |
||||
} = this.dialog; |
||||
|
||||
let states = {}; |
||||
|
||||
states[States.UNLOCKED] = { |
||||
title: titleString, |
||||
html: this.getShareLinkBlock() + this.getAddPasswordBlock() |
||||
}; |
||||
states[States.LOCKED] = { |
||||
title: titleString, |
||||
html: this.getShareLinkBlock() + this.getPasswordBlock() |
||||
}; |
||||
|
||||
return states; |
||||
} |
||||
|
||||
/** |
||||
* Layout for invite link input |
||||
* @returns {string} |
||||
*/ |
||||
getShareLinkBlock() { |
||||
let copyKey = 'dialog.copy'; |
||||
let copyText = APP.translation.translateString(copyKey); |
||||
let roomLockDescKey = 'roomLocked'; |
||||
let roomLockDesc = APP.translation.translateString(roomLockDescKey); |
||||
let roomUnlockKey = 'roomUnlocked'; |
||||
let roomUnlock = APP.translation.translateString(roomUnlockKey); |
||||
let classes = 'button-control button-control_light copyInviteLink'; |
||||
return ( |
||||
`<div class="input-control">
|
||||
<label class="input-control__label" for="inviteLinkRef"> |
||||
${this.dialog.titleString} |
||||
</label> |
||||
<div class="input-control__container"> |
||||
<input class="input-control__input inviteLink" |
||||
id="inviteLinkRef" type="text" |
||||
${this.inviteAttributes} readonly> |
||||
<button data-i18n="${copyKey}" |
||||
class="${classes}"> |
||||
${copyText} |
||||
</button> |
||||
</div> |
||||
<p class="input-control__hint ${this.lockHint}"> |
||||
<span class="icon-security-locked"></span> |
||||
<span data-i18n="${roomLockDescKey}">${roomLockDesc}</span> |
||||
</p> |
||||
<p class="input-control__hint ${this.unlockHint}"> |
||||
<span class="icon-security"></span> |
||||
<span data-i18n="${roomUnlockKey}">${roomUnlock}</span> |
||||
</p> |
||||
</div>` |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Layout for adding password input |
||||
* @returns {string} |
||||
*/ |
||||
getAddPasswordBlock() { |
||||
let addPassKey = 'dialog.addPassword'; |
||||
let addPassText = APP.translation.translateString(addPassKey); |
||||
let addKey = 'dialog.add'; |
||||
let addText = APP.translation.translateString(addKey); |
||||
let html; |
||||
|
||||
if (this.model.isModerator) { |
||||
html = (` |
||||
<div class="input-control"> |
||||
<label class="input-control__label |
||||
for="newPasswordInput" |
||||
data-i18n="${addPassKey}">${addPassText}</label> |
||||
<div class="input-control__container"> |
||||
<input class="input-control__input" id="newPasswordInput" |
||||
type="text"> |
||||
<button id="addPasswordBtn" id="inviteDialogAddPassword" |
||||
disabled data-i18n="${addKey}" |
||||
class="button-control button-control_light"> |
||||
${addText} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
`);
|
||||
} else { |
||||
html = ''; |
||||
} |
||||
|
||||
return html; |
||||
} |
||||
|
||||
/** |
||||
* Layout for password (when room is locked) |
||||
* @returns {string} |
||||
*/ |
||||
getPasswordBlock() { |
||||
let { password, isModerator } = this.model; |
||||
let removePassKey = 'dialog.removePassword'; |
||||
let removePassText = APP.translation.translateString(removePassKey); |
||||
let currentPassKey = 'dialog.currentPassword'; |
||||
let currentPassText = APP.translation.translateString(currentPassKey); |
||||
let passwordKey = "dialog.passwordLabel"; |
||||
let passwordText = APP.translation.translateString(passwordKey); |
||||
|
||||
if (isModerator) { |
||||
return (` |
||||
<div class="input-control"> |
||||
<label class="input-control__label" |
||||
data-i18n="${passwordKey}">${passwordText}</label> |
||||
<div class="input-control__container"> |
||||
<p class="input-control__text" |
||||
data-i18n="${currentPassKey}"> |
||||
${currentPassText} |
||||
<span id="inviteDialogPassword" |
||||
class="input-control__em"> |
||||
${password} |
||||
</span> |
||||
</p> |
||||
<a class="link input-control__right" |
||||
id="inviteDialogRemovePassword" |
||||
href="#" data-i18n="${removePassKey}"> |
||||
${removePassText} |
||||
</a> |
||||
</div> |
||||
</div> |
||||
`);
|
||||
} else { |
||||
return (` |
||||
<div class="input-control"> |
||||
<p>A participant protected this call with a password.</p> |
||||
</div> |
||||
`);
|
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* Opening the dialog |
||||
*/ |
||||
open() { |
||||
let leftButton; |
||||
let { |
||||
submitFunction, |
||||
loadedFunction, |
||||
closeFunction |
||||
} = this.dialog; |
||||
|
||||
let states = this.getStates(); |
||||
|
||||
let buttons = []; |
||||
let leftButtonKey = "dialog.Invite"; |
||||
let cancelButton |
||||
= APP.translation.generateTranslationHTML("dialog.Cancel"); |
||||
buttons.push({title: cancelButton, value: false}); |
||||
|
||||
leftButton = APP.translation.generateTranslationHTML(leftButtonKey); |
||||
buttons.push({ title: leftButton, value: true}); |
||||
|
||||
let initial = this.model.roomLocked ? States.LOCKED : States.UNLOCKED; |
||||
|
||||
APP.UI.messageHandler.openDialogWithStates(states, { |
||||
submit: submitFunction, |
||||
loaded: loadedFunction, |
||||
close: closeFunction, |
||||
buttons, |
||||
size: 'medium' |
||||
}); |
||||
$.prompt.goToState(initial); |
||||
|
||||
this.registerListeners(); |
||||
this.updateView(); |
||||
} |
||||
|
||||
/** |
||||
* Setting event handlers |
||||
* used in dialog |
||||
*/ |
||||
registerListeners() { |
||||
let $passInput = $('#newPasswordInput'); |
||||
let $addPassBtn = $('#addPasswordBtn'); |
||||
|
||||
$(document).on('click', '.copyInviteLink', this.copyToClipboard); |
||||
$addPassBtn.on('click', () => { |
||||
let newPass = $passInput.val(); |
||||
|
||||
if(newPass) { |
||||
this.model.setRoomLocked(newPass); |
||||
} |
||||
}); |
||||
$('#inviteDialogRemovePassword').on('click', () => { |
||||
this.model.setRoomUnlocked(); |
||||
}); |
||||
$passInput.keyup(this.disableAddPassIfInputEmpty.bind(this)); |
||||
} |
||||
|
||||
/** |
||||
* 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) { |
||||
console.error('error when copy the text'); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Method syncing the view and the model |
||||
*/ |
||||
updateView() { |
||||
let pass = this.model.getPassword(); |
||||
if (!pass) |
||||
pass = APP.translation.translateString("passwordSetRemotely"); |
||||
|
||||
$('#inviteDialogPassword').text(pass); |
||||
$('#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); |
||||
} |
||||
} |
@ -0,0 +1,121 @@ |
||||
/* global APP, JitsiMeetJS */ |
||||
import askForPassword from './AskForPassword'; |
||||
|
||||
/** |
||||
* Show notification that user cannot set password for the conference |
||||
* because server doesn't support that. |
||||
*/ |
||||
function notifyPasswordNotSupported () { |
||||
console.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) { |
||||
console.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) { |
||||
console.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; |
||||
}, |
||||
|
||||
/** |
||||
* Asks user for required conference password. |
||||
*/ |
||||
requirePassword () { |
||||
return askForPassword().then( |
||||
newPass => { password = newPass; } |
||||
).catch( |
||||
reason => { |
||||
// user canceled, no pass was entered.
|
||||
// clear, as if we use the same instance several times
|
||||
// pass stays between attempts
|
||||
password = null; |
||||
if (reason !== APP.UI.messageHandler.CANCEL) |
||||
console.error(reason); |
||||
} |
||||
); |
||||
} |
||||
}; |
||||
} |
@ -0,0 +1,19 @@ |
||||
/** |
||||
* Class representing Contact model |
||||
* @class Contact |
||||
*/ |
||||
export default class Contact { |
||||
constructor(opts) { |
||||
let { |
||||
id, |
||||
avatar, |
||||
name, |
||||
isLocal |
||||
} = opts; |
||||
|
||||
this.id = id; |
||||
this.avatar = avatar || ''; |
||||
this.name = name || ''; |
||||
this.isLocal = isLocal || false; |
||||
} |
||||
} |
@ -1,151 +1,94 @@ |
||||
/* global $, APP, interfaceConfig */ |
||||
import Avatar from '../../avatar/Avatar'; |
||||
import UIEvents from '../../../../service/UI/UIEvents'; |
||||
import UIUtil from '../../util/UIUtil'; |
||||
|
||||
let numberOfContacts = 0; |
||||
|
||||
/** |
||||
* Updates the number of participants in the contact list button and sets |
||||
* the glow |
||||
* @param delta indicates whether a new user has joined (1) or someone has |
||||
* left(-1) |
||||
*/ |
||||
function updateNumberOfParticipants(delta) { |
||||
numberOfContacts += delta; |
||||
/* global APP */ |
||||
|
||||
if (numberOfContacts <= 0) { |
||||
console.error("Invalid number of participants: " + numberOfContacts); |
||||
return; |
||||
} |
||||
|
||||
$("#numberOfParticipants").text(numberOfContacts); |
||||
|
||||
$("#contacts_container>div.title").text( |
||||
APP.translation.translateString("contactlist") |
||||
+ ' (' + numberOfContacts + ')'); |
||||
} |
||||
|
||||
/** |
||||
* Creates the avatar element. |
||||
* |
||||
* @return {object} the newly created avatar element |
||||
*/ |
||||
function createAvatar(jid) { |
||||
let avatar = document.createElement('img'); |
||||
avatar.className = "icon-avatar avatar"; |
||||
avatar.src = Avatar.getAvatarUrl(jid); |
||||
|
||||
return avatar; |
||||
} |
||||
import UIEvents from '../../../../service/UI/UIEvents'; |
||||
import ContactListView from './ContactListView'; |
||||
import Contact from './Contact'; |
||||
|
||||
/** |
||||
* Creates the display name paragraph. |
||||
* Model for the Contact list. |
||||
* |
||||
* @param displayName the display name to set |
||||
* @class ContactList |
||||
*/ |
||||
function createDisplayNameParagraph(key, displayName) { |
||||
let p = document.createElement('p'); |
||||
if (displayName) { |
||||
p.innerHTML = displayName; |
||||
} else if(key) { |
||||
p.setAttribute("data-i18n",key); |
||||
p.innerHTML = APP.translation.translateString(key); |
||||
class ContactList { |
||||
constructor(conference) { |
||||
this.conference = conference; |
||||
this.contacts = []; |
||||
this.roomLocked = false; |
||||
ContactListView.init(this); |
||||
} |
||||
|
||||
return p; |
||||
} |
||||
|
||||
function getContactEl (id) { |
||||
return $(`#contacts>li[id="${id}"]`); |
||||
} |
||||
|
||||
/** |
||||
* Contact list. |
||||
*/ |
||||
var ContactList = { |
||||
init (emitter) { |
||||
this.emitter = emitter; |
||||
}, |
||||
/** |
||||
* Indicates if the chat is currently visible. |
||||
* Is locked flag. |
||||
* Delegates to Invite module |
||||
* TO FIX: find a better way to access the IS LOCKED state of the invite. |
||||
* |
||||
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> - |
||||
* otherwise |
||||
* @returns {Boolean} |
||||
*/ |
||||
isVisible () { |
||||
return UIUtil.isVisible(document.getElementById("contactlist")); |
||||
}, |
||||
isLocked() { |
||||
return APP.conference.invite.isLocked(); |
||||
} |
||||
|
||||
/** |
||||
* Adds a contact for the given id. |
||||
* @param isLocal is an id for the local user. |
||||
* Adding new participant. |
||||
* |
||||
* @param id |
||||
* @param isLocal |
||||
*/ |
||||
addContact (id, isLocal) { |
||||
let contactlist = $('#contacts'); |
||||
|
||||
let newContact = document.createElement('li'); |
||||
newContact.id = id; |
||||
newContact.className = "clickable"; |
||||
newContact.onclick = (event) => { |
||||
if (event.currentTarget.className === "clickable") { |
||||
this.emitter.emit(UIEvents.CONTACT_CLICKED, id); |
||||
} |
||||
}; |
||||
|
||||
if (interfaceConfig.SHOW_CONTACTLIST_AVATARS) |
||||
newContact.appendChild(createAvatar(id)); |
||||
|
||||
newContact.appendChild( |
||||
createDisplayNameParagraph( |
||||
isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null, |
||||
isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)); |
||||
addContact(id, isLocal) { |
||||
let isExist = this.contacts.some((el) => el.id === id); |
||||
|
||||
if (APP.conference.isLocalId(id)) { |
||||
contactlist.prepend(newContact); |
||||
} else { |
||||
contactlist.append(newContact); |
||||
if (!isExist) { |
||||
let newContact = new Contact({ id, isLocal }); |
||||
this.contacts.push(newContact); |
||||
APP.UI.emitEvent(UIEvents.CONTACT_ADDED, { id, isLocal }); |
||||
} |
||||
updateNumberOfParticipants(1); |
||||
}, |
||||
} |
||||
|
||||
/** |
||||
* Removes a contact for the given id. |
||||
* Removing participant. |
||||
* |
||||
* @param id |
||||
* @returns {Array|*} |
||||
*/ |
||||
removeContact (id) { |
||||
let contact = getContactEl(id); |
||||
|
||||
if (contact.length > 0) { |
||||
contact.remove(); |
||||
updateNumberOfParticipants(-1); |
||||
} |
||||
}, |
||||
|
||||
setClickable (id, isClickable) { |
||||
getContactEl(id).toggleClass('clickable', isClickable); |
||||
}, |
||||
removeContact(id) { |
||||
this.contacts = this.contacts.filter((el) => el.id !== id); |
||||
APP.UI.emitEvent(UIEvents.CONTACT_REMOVED, { id }); |
||||
return this.contacts; |
||||
} |
||||
|
||||
onDisplayNameChange (id, displayName) { |
||||
if(!displayName) |
||||
/** |
||||
* Changing the display name. |
||||
* |
||||
* @param id |
||||
* @param name |
||||
*/ |
||||
onDisplayNameChange (id, name) { |
||||
if(!name) |
||||
return; |
||||
if (id === 'localVideoContainer') { |
||||
id = APP.conference.getMyUserId(); |
||||
} |
||||
let contactName = $(`#contacts #${id}>p`); |
||||
|
||||
if (contactName.text() !== displayName) { |
||||
contactName.text(displayName); |
||||
} |
||||
}, |
||||
let contacts = this.contacts.filter((el) => el.id === id); |
||||
contacts.forEach((el) => { |
||||
el.name = name; |
||||
}); |
||||
APP.UI.emitEvent(UIEvents.DISPLAY_NAME_CHANGED, { id, name }); |
||||
} |
||||
|
||||
changeUserAvatar (id, avatarUrl) { |
||||
// set the avatar in the contact list
|
||||
let contact = $(`#${id}>img`); |
||||
if (contact.length > 0) { |
||||
contact.attr('src', avatarUrl); |
||||
} |
||||
/** |
||||
* Changing the avatar. |
||||
* |
||||
* @param id |
||||
* @param avatar |
||||
*/ |
||||
changeUserAvatar (id, avatar) { |
||||
let contacts = this.contacts.filter((el) => el.id === id); |
||||
contacts.forEach((el) => { |
||||
el.avatar = avatar; |
||||
}); |
||||
APP.UI.emitEvent(UIEvents.USER_AVATAR_CHANGED, { id, avatar }); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
export default ContactList; |
||||
export default ContactList; |
@ -0,0 +1,262 @@ |
||||
/* global $, APP, interfaceConfig */ |
||||
import Avatar from '../../avatar/Avatar'; |
||||
import UIEvents from '../../../../service/UI/UIEvents'; |
||||
import UIUtil from '../../util/UIUtil'; |
||||
|
||||
let numberOfContacts = 0; |
||||
|
||||
/** |
||||
* Updates the number of participants in the contact list button and sets |
||||
* the glow |
||||
* @param delta indicates whether a new user has joined (1) or someone has |
||||
* left(-1) |
||||
*/ |
||||
function updateNumberOfParticipants(delta) { |
||||
numberOfContacts += delta; |
||||
|
||||
if (numberOfContacts <= 0) { |
||||
console.error("Invalid number of participants: " + numberOfContacts); |
||||
return; |
||||
} |
||||
|
||||
$("#numberOfParticipants").text(numberOfContacts); |
||||
|
||||
$("#contacts_container>div.title").text( |
||||
APP.translation.translateString("contactlist") |
||||
+ ' (' + numberOfContacts + ')'); |
||||
} |
||||
|
||||
/** |
||||
* Creates the avatar element. |
||||
* |
||||
* @return {object} the newly created avatar element |
||||
*/ |
||||
function createAvatar(jid) { |
||||
let avatar = document.createElement('img'); |
||||
avatar.className = "icon-avatar avatar"; |
||||
avatar.src = Avatar.getAvatarUrl(jid); |
||||
|
||||
return avatar; |
||||
} |
||||
|
||||
/** |
||||
* Creates the display name paragraph. |
||||
* |
||||
* @param displayName the display name to set |
||||
*/ |
||||
function createDisplayNameParagraph(key, displayName) { |
||||
let p = document.createElement('p'); |
||||
if (displayName) { |
||||
p.innerHTML = displayName; |
||||
} else if(key) { |
||||
p.setAttribute("data-i18n",key); |
||||
p.innerHTML = APP.translation.translateString(key); |
||||
} |
||||
|
||||
return p; |
||||
} |
||||
|
||||
/** |
||||
* Getter for current contact element |
||||
* @param id |
||||
* @returns {JQuery} |
||||
*/ |
||||
function getContactEl (id) { |
||||
return $(`#contacts>li[id="${id}"]`); |
||||
} |
||||
|
||||
/** |
||||
* Contact list. |
||||
*/ |
||||
var ContactListView = { |
||||
init (model) { |
||||
this.model = model; |
||||
this.lockKey = 'roomLocked'; |
||||
this.unlockKey = 'roomUnlocked'; |
||||
this.addInviteButton(); |
||||
this.registerListeners(); |
||||
this.toggleLock(); |
||||
}, |
||||
/** |
||||
* Adds layout for invite button |
||||
*/ |
||||
addInviteButton() { |
||||
let container = document.getElementById('contacts_container'); |
||||
let title = container.firstElementChild; |
||||
|
||||
let htmlLayout = this.getInviteButtonLayout(); |
||||
title.insertAdjacentHTML('afterend', htmlLayout); |
||||
$(document).on('click', '#addParticipantsBtn', () => { |
||||
APP.UI.emitEvent(UIEvents.INVITE_CLICKED); |
||||
}); |
||||
}, |
||||
/** |
||||
* Returns layout for invite button |
||||
*/ |
||||
getInviteButtonLayout() { |
||||
let classes = 'button-control button-control_primary'; |
||||
classes += ' button-control_full-width'; |
||||
let key = 'addParticipants'; |
||||
let text = APP.translation.translateString(key); |
||||
|
||||
let lockedHtml = this.getLockDescriptionLayout(this.lockKey); |
||||
let unlockedHtml = this.getLockDescriptionLayout(this.unlockKey); |
||||
|
||||
let html = ( |
||||
`<div class="sideToolbarBlock first">
|
||||
<button id="addParticipantsBtn"
|
||||
data-i18n="${key}"
|
||||
class="${classes}"> |
||||
${text} |
||||
</button> |
||||
<div> |
||||
${lockedHtml} |
||||
${unlockedHtml} |
||||
</div> |
||||
</div>`); |
||||
|
||||
return html; |
||||
}, |
||||
/** |
||||
* Adds layout for lock description |
||||
*/ |
||||
getLockDescriptionLayout(key) { |
||||
let classes = "input-control__hint input-control_full-width"; |
||||
let description = APP.translation.translateString(key); |
||||
let padlockSuffix = ''; |
||||
if (key === this.lockKey) { |
||||
padlockSuffix = '-locked'; |
||||
} |
||||
|
||||
return `<p id="contactList${key}" class="${classes}">
|
||||
<span class="icon-security${padlockSuffix}"></span> |
||||
<span data-i18n="${key}">${description}</span> |
||||
</p>`; |
||||
}, |
||||
/** |
||||
* Setup listeners |
||||
*/ |
||||
registerListeners() { |
||||
let model = this.model; |
||||
let removeContact = this.onRemoveContact.bind(this); |
||||
let changeAvatar = this.changeUserAvatar.bind(this); |
||||
let displayNameChange = this.onDisplayNameChange.bind(this); |
||||
|
||||
APP.UI.addListener( UIEvents.TOGGLE_ROOM_LOCK, |
||||
this.toggleLock.bind(this)); |
||||
APP.UI.addListener( UIEvents.CONTACT_ADDED, |
||||
this.onAddContact.bind(this)); |
||||
|
||||
APP.UI.addListener(UIEvents.CONTACT_REMOVED, removeContact); |
||||
APP.UI.addListener(UIEvents.USER_AVATAR_CHANGED, changeAvatar); |
||||
APP.UI.addListener(UIEvents.DISPLAY_NAME_CHANGED, displayNameChange); |
||||
}, |
||||
/** |
||||
* Updating the view according the model |
||||
* @param type {String} type of change |
||||
* @returns {Promise} |
||||
*/ |
||||
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}`; |
||||
|
||||
$(`#${showId}`).show(); |
||||
$(`#${hideId}`).hide(); |
||||
}, |
||||
/** |
||||
* Indicates if the chat is currently visible. |
||||
* |
||||
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> - |
||||
* otherwise |
||||
*/ |
||||
isVisible () { |
||||
return UIUtil.isVisible(document.getElementById("contactlist")); |
||||
}, |
||||
|
||||
/** |
||||
* Handler for Adding a contact for the given id. |
||||
* @param isLocal is an id for the local user. |
||||
*/ |
||||
onAddContact (data) { |
||||
let { id, isLocal } = data; |
||||
let contactlist = $('#contacts'); |
||||
let newContact = document.createElement('li'); |
||||
newContact.id = id; |
||||
newContact.className = "clickable"; |
||||
newContact.onclick = (event) => { |
||||
if (event.currentTarget.className === "clickable") { |
||||
APP.UI.emitEvent(UIEvents.CONTACT_CLICKED, id); |
||||
} |
||||
}; |
||||
|
||||
if (interfaceConfig.SHOW_CONTACTLIST_AVATARS) |
||||
newContact.appendChild(createAvatar(id)); |
||||
|
||||
newContact.appendChild( |
||||
createDisplayNameParagraph( |
||||
isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null, |
||||
isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)); |
||||
|
||||
if (APP.conference.isLocalId(id)) { |
||||
contactlist.prepend(newContact); |
||||
} else { |
||||
contactlist.append(newContact); |
||||
} |
||||
updateNumberOfParticipants(1); |
||||
}, |
||||
|
||||
/** |
||||
* Handler for removing |
||||
* a contact for the given id. |
||||
*/ |
||||
onRemoveContact (data) { |
||||
let { id } = data; |
||||
let contact = getContactEl(id); |
||||
|
||||
if (contact.length > 0) { |
||||
contact.remove(); |
||||
updateNumberOfParticipants(-1); |
||||
} |
||||
}, |
||||
|
||||
setClickable (id, isClickable) { |
||||
getContactEl(id).toggleClass('clickable', isClickable); |
||||
}, |
||||
|
||||
/** |
||||
* Changes display name of the user |
||||
* defined by its id |
||||
* @param data |
||||
*/ |
||||
onDisplayNameChange (data) { |
||||
let { id, name } = data; |
||||
if(!name) |
||||
return; |
||||
if (id === 'localVideoContainer') { |
||||
id = APP.conference.getMyUserId(); |
||||
} |
||||
let contactName = $(`#contacts #${id}>p`); |
||||
|
||||
if (contactName.text() !== name) { |
||||
contactName.text(name); |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Changes user avatar |
||||
* @param data |
||||
*/ |
||||
changeUserAvatar (data) { |
||||
let { id, avatar } = data; |
||||
// set the avatar in the contact list
|
||||
let contact = $(`#${id}>img`); |
||||
if (contact.length > 0) { |
||||
contact.attr('src', avatar); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
export default ContactListView; |
Loading…
Reference in new issue