mirror of
https://github.com/dani/vroom.git
synced 2024-06-26 17:43:29 +02:00
![Daniel Berteaud](/assets/img/avatar_default.png)
Render a plain JS file and include it as a standard JS script instead of relying on an ajax call This makes it available immediatly (before that there could be a slight delay), but more importantly, makes sure the response can be cached Chrome wasn't caching XHR get responses (no matter what the header were)
2235 lines
71 KiB
JavaScript
2235 lines
71 KiB
JavaScript
/*
|
|
This file is part of the VROOM project
|
|
released under the MIT licence
|
|
Copyright 2014-2015 Daniel Berteaud <daniel@firewall-services.com>
|
|
*/
|
|
|
|
// Default notifications
|
|
$.notify.defaults({
|
|
globalPosition: 'bottom left'
|
|
});
|
|
|
|
// Enable tooltip on required elements
|
|
$('.help').tooltip({
|
|
container: 'body',
|
|
trigger: 'hover'
|
|
});
|
|
$('.popup').popover({
|
|
container: 'body',
|
|
trigger: 'focus'
|
|
});
|
|
$('.modal').on('show.bs.modal', function(){
|
|
$('.help').tooltip('hide');
|
|
});
|
|
|
|
// Enable bootstrap-swicth
|
|
$('.bs-switch').bootstrapSwitch();
|
|
|
|
// Date pickers
|
|
$('.date-picker').datepicker({
|
|
autoclose: true,
|
|
format: 'yyyy-mm-dd',
|
|
todayHighlight: true,
|
|
todayBtn: 'linked',
|
|
weekStart: 1,
|
|
endDate: '1',
|
|
language: currentLang
|
|
});
|
|
|
|
// When pagination is done, how many item per page
|
|
var itemPerPage = 20;
|
|
|
|
// Regex to check a date
|
|
var dateRe = /^\d{4}\-\d{1,2}\-\d{1,2}$/;
|
|
|
|
// Some global vars, like
|
|
// the SimpleWebRTC object
|
|
var webrtc = undefined;
|
|
// The current room configuration
|
|
var roomInfo = {};
|
|
// The current peers (we init the list with only ourself)
|
|
var peers = {
|
|
local: {
|
|
screenShared: false,
|
|
micMuted: false,
|
|
videoPaused: false,
|
|
displayName: '',
|
|
color: chooseColor(),
|
|
role: 'participant',
|
|
hasVideo: true
|
|
}
|
|
};
|
|
|
|
// Mark current page link as active
|
|
$('#lnk_' + page).addClass('active');
|
|
|
|
// Default ajax setup
|
|
$.ajaxSetup({
|
|
url: rootUrl + 'api',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
headers: {
|
|
'X-VROOM-API-Key': api_key
|
|
}
|
|
});
|
|
|
|
|
|
//
|
|
// Define a few functions
|
|
//
|
|
|
|
// Localize a string, or just print it if localization doesn't exist
|
|
function localize(string){
|
|
if (locale[string]){
|
|
return locale[string];
|
|
}
|
|
else if (fallback_locale[string]){
|
|
return fallback_locale[string];
|
|
}
|
|
return string;
|
|
}
|
|
|
|
// POST a JSON req on the backend API
|
|
function vroomApi(action, param, success, error){
|
|
if (typeof error !== 'function'){
|
|
error = function(data){
|
|
data = data.responseJSON;
|
|
if (data.msg){
|
|
$.notify(data.msg, 'error');
|
|
}
|
|
else{
|
|
$.notify(localize('ERROR_OCCURRED'), 'error');
|
|
}
|
|
};
|
|
}
|
|
if (typeof success !== 'function'){
|
|
success = function(){};
|
|
}
|
|
$.ajax({
|
|
data: {
|
|
req: JSON.stringify({
|
|
action: action,
|
|
param: param
|
|
})
|
|
},
|
|
error: function(data){
|
|
error(data);
|
|
},
|
|
success: function(data){
|
|
success(data);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle lang switch drop down menu
|
|
$('#switch_lang').change(function(){
|
|
vroomApi(
|
|
'switch_lang',
|
|
{
|
|
language: $('#switch_lang').val()
|
|
},
|
|
function(data){
|
|
window.location.reload();
|
|
}
|
|
);
|
|
});
|
|
|
|
// Escape entities to prevent XSS
|
|
function stringEscape(string){
|
|
return $('<div>').text(string).html();
|
|
}
|
|
|
|
// Select a color (randomly) from this list, used for text chat, and the name under the preview
|
|
function chooseColor(){
|
|
// Shamelessly taken from http://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
|
|
var colors = [
|
|
'#B1C7FD', '#DDFDB1', '#FDB1F3', '#B1FDF0', '#FDDAB1', '#C4B1FD', '#B4FDB1', '#FDB1CA',
|
|
'#B1E1FD', '#F7FDB1', '#EDB1FD', '#B1FDD7', '#FDC1B1', '#B1B7FD', '#CEFDB1', '#FDB1E4',
|
|
'#B1FAFD', '#FDEAB1', '#D4B1FD', '#B1FDBD', '#FDB1BB', '#B1D1FD', '#E7FDB1', '#FDB1FD',
|
|
'#B1FDE7', '#B1FDE7'
|
|
];
|
|
return colors[Math.floor(Math.random() * colors.length)];
|
|
}
|
|
|
|
// Just play a sound
|
|
function playSound(sound){
|
|
var audio = new Audio(rootUrl + 'snd/' + sound);
|
|
audio.play();
|
|
}
|
|
|
|
// Request full screen
|
|
function fullScreen(el){
|
|
if (el.requestFullScreen){
|
|
el.requestFullScreen();
|
|
}
|
|
else if (el.webkitRequestFullScreen){
|
|
el.webkitRequestFullScreen();
|
|
}
|
|
else if (el.mozRequestFullScreen){
|
|
el.mozRequestFullScreen();
|
|
}
|
|
}
|
|
|
|
// Linkify urls in a string
|
|
// Taken from http://rickyrosario.com/blog/converting-a-url-into-a-link-in-javascript-linkify-function/
|
|
function linkify(text){
|
|
if (text) {
|
|
text = text.replace(
|
|
/((https?\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi,
|
|
function(url){
|
|
var full_url = url;
|
|
if (!full_url.match('^https?:\/\/')) {
|
|
full_url = 'http://' + full_url;
|
|
}
|
|
return '<a href="' + full_url + '" target="_blank">' + url + '</a>';
|
|
}
|
|
);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
// Save content to a file
|
|
function downloadContent(filename, type, content){
|
|
var blob = new Blob([content], {type: type});
|
|
saveAs(blob, filename);
|
|
}
|
|
|
|
// Return current time formatted as hh:mm:ss
|
|
function getTime(){
|
|
var d = new Date();
|
|
var hours = d.getHours().toString(),
|
|
minutes = d.getMinutes().toString(),
|
|
seconds = d.getSeconds().toString();
|
|
hours = (hours.length < 2) ? '0' + hours : hours;
|
|
minutes = (minutes.length < 2) ? '0' + minutes : minutes;
|
|
seconds = (seconds.length < 2) ? '0' + seconds : seconds;
|
|
return hours + ':' + minutes + ':' + seconds;
|
|
}
|
|
|
|
// Convert dates from UTC to local TZ
|
|
function utc2Local(date) {
|
|
var newDate = new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
|
|
var offset = date.getTimezoneOffset() / 60;
|
|
var hours = date.getHours();
|
|
newDate.setHours(hours - offset);
|
|
return newDate;
|
|
}
|
|
|
|
// Temporarily suspend a button, prevent overloading the backend
|
|
// if someone start clicking quickly
|
|
function suspendButton(el){
|
|
$(el).attr('disabled', true);
|
|
setTimeout(function(){
|
|
$(el).attr('disabled', false);
|
|
}, 1000);
|
|
}
|
|
|
|
// get max height for the main video and the preview div
|
|
function maxHeight(){
|
|
// Which is the window height, minus toolbar, and a margin of 25px
|
|
return $(window).height()-$('#toolbar').height()-25;
|
|
}
|
|
|
|
// Create a new email input field
|
|
function addEmailInputField(form, val){
|
|
var parentEl = $('#' + form),
|
|
currentEntry = parentEl.find('.email-entry:last'),
|
|
newEntry = $(currentEntry.clone()).appendTo(parentEl);
|
|
newEntry.find('input').val(val);
|
|
newEntry.removeClass('has-error');
|
|
adjustAddRemoveEmailButtons(form);
|
|
}
|
|
|
|
// Adjust add and remove buttons foir email notifications
|
|
function adjustAddRemoveEmailButtons(form){
|
|
$('#' + form).find('.email-entry:not(:last) .btn-add-email')
|
|
.removeClass('btn-primary').removeClass('btn-add-email')
|
|
.addClass('btn-danger').addClass('btn-remove-email')
|
|
.html('<span class="glyphicon glyphicon-minus"></span>');
|
|
$('#' + form).find('.email-entry:last .btn-remove-email')
|
|
.removeClass('btn-danger').removeClass('btn-remove-email')
|
|
.addClass('btn-primary').addClass('btn-add-email')
|
|
.html('<span class="glyphicon glyphicon-plus"></span>');
|
|
}
|
|
// Add emails input field
|
|
$(document).on('click','button.btn-add-email',function(e){
|
|
e.preventDefault();
|
|
addEmailInputField($(this).parents('.email-list:first').attr('id'), '');
|
|
});
|
|
// Remove email input field
|
|
$(document).on('click','button.btn-remove-email',function(e){
|
|
e.preventDefault();
|
|
el = $(this).parents('.email-entry:first');
|
|
el.remove();
|
|
});
|
|
|
|
// Update the displayName of the peer
|
|
// and its screen if any
|
|
function updateDisplayName(id){
|
|
// We might receive the screen before the peer itself
|
|
// so check if the object exists before using it, or fallback with empty values
|
|
var display = (peers[id] && peers[id].hasName) ? stringEscape(peers[id].displayName) : '';
|
|
var color = (peers[id] && peers[id].color) ? peers[id].color : chooseColor();
|
|
var screenName = (peers[id] && peers[id].hasName) ? sprintf(localize('SCREEN_s'), stringEscape(peers[id].displayName)) : '';
|
|
$('#name_' + id).html(display).css('background-color', color);
|
|
$('#name_' + id + '_screen').html(screenName).css('background-color', color);
|
|
}
|
|
|
|
// Handle owner/join password confirm
|
|
$('#ownerPassConfirm').on('input', function() {
|
|
if ($('#ownerPassConfirm').val() == $('#ownerPass').val() &&
|
|
$('#ownerPassConfirm').val() != ''){
|
|
$('#ownerPassConfirm').parent().removeClass('has-error');
|
|
}
|
|
else{
|
|
$('#ownerPassConfirm').parent().addClass('has-error');
|
|
}
|
|
});
|
|
$('#joinPassConfirm').on('input', function() {
|
|
if ($('#joinPass').val() == $('#joinPassConfirm').val() &&
|
|
$('#joinPass').val() != ''){
|
|
$('#joinPassConfirm').parent().removeClass('has-error');
|
|
}
|
|
else{
|
|
$('#joinPassConfirm').parent().addClass('has-error');
|
|
}
|
|
});
|
|
|
|
// Hide or show password fields
|
|
// Depending on their corresponding switch
|
|
$('#joinPassSet').on('switchChange.bootstrapSwitch', function(event, state) {
|
|
if (state){
|
|
$('#joinPassFields').show(200);
|
|
}
|
|
else{
|
|
$('#joinPassFields').hide(200);
|
|
}
|
|
});
|
|
$('#ownerPassSet').on('switchChange.bootstrapSwitch', function(event, state) {
|
|
if (state){
|
|
$('#ownerPassFields').show(200);
|
|
}
|
|
else{
|
|
$('#ownerPassFields').hide(200);
|
|
}
|
|
});
|
|
|
|
|
|
// Submit the configuration form
|
|
$('#configureRoomForm').submit(function(e){
|
|
e.preventDefault();
|
|
// check join password
|
|
if ($('#joinPassSet').bootstrapSwitch('state')){
|
|
// Both must match
|
|
if ($('#joinPass').val() !== $('#joinPassConfirm').val()){
|
|
$('#joinPassConfirm').notify(localize('PASSWORDS_DO_NOT_MATCH'), 'error');
|
|
return false;
|
|
}
|
|
// Empty password are not valid. We can't use the switch state, cause when editing a room
|
|
// with a password already set, those field are empty
|
|
if ($('#joinPassFields').css('display') !== 'none' && $('#joinPass').val() === ''){
|
|
$('#joinPass').notify(localize('PASSWORDS_CANNOT_BE_EMPTY'), 'error');
|
|
return false;
|
|
}
|
|
}
|
|
// Same for the owner password
|
|
if ($('#ownerPassSet').bootstrapSwitch('state')){
|
|
if ($('#ownerPass').val() !== $('#ownerPassConfirm').val()){
|
|
$('#ownerPassConfirm').notify(localize('PASSWORDS_DO_NOT_MATCH'), 'error');
|
|
return false;
|
|
}
|
|
if ($('#ownerPassFields').css('display') !== 'none' && $('#ownerPass').val() === ''){
|
|
$('#ownerPass').notify(localize('PASSWORDS_CANNOT_BE_EMPTY'), 'error');
|
|
return false;
|
|
}
|
|
}
|
|
var validEmail = true;
|
|
$('.email-list').find('input').each(function(index, input){
|
|
if (!$(input).val().match(/\S+@\S+\.\S+/) && $(input).val() !== ''){
|
|
$(input).parent().addClass('has-error');
|
|
// TODO: find how to display correctly the error
|
|
//$(input).parent().notify(localize('ERROR_MAIL_INVALID'), 'error');
|
|
validEmail = false;
|
|
// Break the each loop and return an error now, we can't submit as is
|
|
return false;
|
|
}
|
|
else{
|
|
$(input).parent().removeClass('has-error');
|
|
}
|
|
});
|
|
if (!validEmail){
|
|
return false;
|
|
}
|
|
var locked = $('#lockedSet').bootstrapSwitch('state'),
|
|
askForName = $('#askForNameSet').bootstrapSwitch('state'),
|
|
joinPass = ($('#joinPassSet').bootstrapSwitch('state')) ?
|
|
$('#joinPass').val() : false,
|
|
ownerPass = ($('#ownerPassSet').bootstrapSwitch('state')) ?
|
|
$('#ownerPass').val() : false,
|
|
persist = ($('#persistentSet').length > 0) ?
|
|
$('#persistentSet').bootstrapSwitch('state') : '',
|
|
members = ($('#maxMembers').length > 0) ?
|
|
$('#maxMembers').val() : 0,
|
|
emails = [];
|
|
$('input[name="emails[]"]').each(function(){
|
|
if ($(this).val() !== ''){
|
|
emails.push($(this).val());
|
|
}
|
|
});
|
|
vroomApi(
|
|
'update_room_conf',
|
|
{
|
|
room: roomName,
|
|
locked: locked,
|
|
ask_for_name: askForName,
|
|
join_password: joinPass,
|
|
owner_password: ownerPass,
|
|
persistent: persist,
|
|
max_members: members,
|
|
emails: emails
|
|
},
|
|
function(data){
|
|
// On success, reset the input fields, collapse the password inputs
|
|
// and close the configuration modal
|
|
$('#ownerPass,#ownerPassConfirm,#joinPass,#joinPassConfirm').val('');
|
|
$('#configureModal').modal('hide');
|
|
$('#joinPassFields,#ownerPassFields').hide();
|
|
$.notify(data.msg, 'info');
|
|
$('#configureRoomForm').trigger('room_conf_updated');
|
|
}
|
|
);
|
|
});
|
|
|
|
// Get our role and other room settings from the server
|
|
function getRoomInfo(cb){
|
|
vroomApi(
|
|
'get_room_conf',
|
|
{
|
|
room: roomName,
|
|
},
|
|
function(data){
|
|
roomInfo = data;
|
|
// Reset the list of email displayed, so first remove every input field but the last one
|
|
// We keep it so we can clone it again
|
|
$('.email-list').find('.email-entry:not(:last)').remove();
|
|
// Now add one input per email
|
|
$.each(data.notif, function(index, obj){
|
|
addEmailInputField('email-list-notification', obj.email);
|
|
});
|
|
// Now, remove the first one if the list is not empty
|
|
if (Object.keys(data.notif).length > 0){
|
|
$('.email-list').find('.email-entry:first').remove();
|
|
}
|
|
else{
|
|
$('.email-list').find('.email-entry:first').find('input:first').val('');
|
|
}
|
|
adjustAddRemoveEmailButtons();
|
|
// Update config switches
|
|
$('#lockedSet').bootstrapSwitch('state', data.locked);
|
|
$('#askForNameSet').bootstrapSwitch('state', data.ask_for_name);
|
|
$('#joinPassSet').bootstrapSwitch('state', data.join_auth);
|
|
$('#ownerPassSet').bootstrapSwitch('state', data.owner_auth);
|
|
// exec a callback if needed
|
|
if (typeof cb === 'function'){
|
|
cb();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// Used on the index page
|
|
function initIndex(){
|
|
var room;
|
|
// Submit the home form to create a room
|
|
$('#createRoom').submit(function(e){
|
|
e.preventDefault();
|
|
// Do not submit if we know the name is invalid
|
|
if (!$('#roomName').val().match(/^[\w\-]{0,49}$/)){
|
|
$('#roomName').parent().parent().notify(localize('ERROR_NAME_INVALID'), {
|
|
class: 'error',
|
|
position: 'bottom center'
|
|
});
|
|
}
|
|
else{
|
|
vroomApi(
|
|
'create_room',
|
|
{
|
|
room: $('#roomName').val()
|
|
},
|
|
function(data) {
|
|
// On success, the room has been created, lets join it
|
|
room = data.room;
|
|
window.location.assign(rootUrl + data.room);
|
|
},
|
|
function(data){
|
|
data = data.responseJSON;
|
|
if (data.err && data.err === 'ERROR_NAME_CONFLICT'){
|
|
room = data.room;
|
|
$('#conflictModal').modal('show');
|
|
}
|
|
else if (data.msg){
|
|
$('#roomName').parent().parent().notify(data.msg, {
|
|
class: 'error',
|
|
position: 'bottom center'
|
|
});
|
|
}
|
|
else{
|
|
$.notify(localize('ERROR_OCCURRED'), 'error');
|
|
}
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
// Handle join confirmation
|
|
$('#confirmJoinButton').click(function(){
|
|
window.location.assign(rootUrl + room);
|
|
});
|
|
// Handle cancel/choose another name
|
|
$('#chooseAnotherNameButton').click(function(){
|
|
$('#roomName').val('');
|
|
$('#conflictModal').modal('hide');
|
|
});
|
|
// Check for invalid room name as user is typing
|
|
$('#roomName').on('input', function(){
|
|
if (!$('#roomName').val().match(/^[\w\-]{0,49}$/)){
|
|
$('#roomName').parent().addClass('has-error');
|
|
}
|
|
else{
|
|
$('#roomName').parent().removeClass('has-error');
|
|
}
|
|
});
|
|
}
|
|
|
|
// The documentation page
|
|
function initDoc(){
|
|
|
|
window.onresize = function (){
|
|
$('#toc').width($('#toc').parents().width());
|
|
$('#toc').css('max-height', maxHeight() - 100 + 'px');
|
|
};
|
|
$('#toc').width($('#toc').parents().width());
|
|
$('#toc').css('max-height', maxHeight() - 100 + 'px');
|
|
|
|
$('#toc').toc({
|
|
elementClass: 'toc',
|
|
ulClass: 'nav',
|
|
heading: 'Table of content',
|
|
indexingFormats: 'number'
|
|
});
|
|
|
|
// Scroll to the table of content section when user scroll the mouse
|
|
$('body').scrollspy({
|
|
target: '#toc',
|
|
offset: $('#headerNav').outerHeight(true) + 40
|
|
});
|
|
|
|
// Small delay so the affix can start when everything is loaded
|
|
setTimeout(function() {
|
|
var $sideBar = $('#toc');
|
|
$sideBar.affix({
|
|
offset: {
|
|
top: function() {
|
|
var offsetTop = $sideBar.offset().top,
|
|
sideBarMargin = parseInt($sideBar.children(0).css('margin-top'), 10),
|
|
navOuterHeight = $('#headerNav').height();
|
|
return (this.top = offsetTop - navOuterHeight - sideBarMargin);
|
|
},
|
|
bottom: function() {
|
|
return (this.bottom = $('footer').outerHeight(true));
|
|
}
|
|
}
|
|
});
|
|
}, 200);
|
|
}
|
|
|
|
// Used on the room admin page
|
|
function initAdminRooms(){
|
|
var roomList = {};
|
|
var matches = 0;
|
|
|
|
// Update display of room list
|
|
function updateRoomList(filter, min, max){
|
|
$('#roomList').html('');
|
|
var filterRe = new RegExp(filter, "gi");
|
|
var i = 0;
|
|
matches = 0;
|
|
$.each(roomList, function (index, obj){
|
|
if (filter === '' || obj.name.match(filterRe)){
|
|
matches++;
|
|
if (i >= min && i < max){
|
|
var t = obj.create_date.split(/[- :]/);
|
|
var create = utc2Local(new Date(t[0], t[1]-1, t[2], t[3], t[4], t[5])).toLocaleString();
|
|
t = obj.last_activity.split(/[- :]/);
|
|
var activity = utc2Local(new Date(t[0], t[1]-1, t[2], t[3], t[4], t[5])).toLocaleString();
|
|
$('#roomList').append($('<tr>')
|
|
.append($('<td>').html(stringEscape(obj.name)))
|
|
.append($('<td>').html(stringEscape(create)).addClass('hidden-xs'))
|
|
.append($('<td>').html(stringEscape(activity)).addClass('hidden-xs'))
|
|
.append($('<td>').html(obj.members).addClass('hidden-xs hidden-sm'))
|
|
.append($('<td>')
|
|
.append($('<div>').addClass('btn-group')
|
|
.append($('<a>').addClass('btn btn-default btn-lg').attr('href',rootUrl + obj.name)
|
|
.html(
|
|
$('<span>').addClass('glyphicon glyphicon-log-in')
|
|
)
|
|
)
|
|
.append($('<button>').addClass('btn btn-default btn-lg btn-configure').data('room', obj.name)
|
|
.html(
|
|
$('<span>').addClass('glyphicon glyphicon-cog')
|
|
)
|
|
)
|
|
.append($('<a>').addClass('btn btn-default btn-lg btn-remove').data('room', obj.name)
|
|
.html(
|
|
$('<span>').addClass('glyphicon glyphicon-trash')
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
i++;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update pagination
|
|
function updatePagination(){
|
|
if (matches <= itemPerPage){
|
|
$('#pagination').hide(200);
|
|
return;
|
|
}
|
|
var total = Math.ceil(matches / itemPerPage);
|
|
if (total === 0){
|
|
total = 1;
|
|
}
|
|
$('#pagination').bootpag({
|
|
total: total,
|
|
maxVisible: 10,
|
|
page: 1
|
|
}).on('page', function(e, page){
|
|
var min = itemPerPage * (page - 1);
|
|
var max = min + itemPerPage;
|
|
updateRoomList($('#searchRoom').val(), min, max);
|
|
});
|
|
$('#pagination').show(200);
|
|
}
|
|
|
|
// Request the list of existing rooms to the server
|
|
function getRooms(){
|
|
vroomApi(
|
|
'get_room_list',
|
|
{},
|
|
function(data){
|
|
roomList = data.rooms;
|
|
matches = Object.keys(roomList).length;
|
|
updateRoomList($('#searchRoom').val(), 0, itemPerPage);
|
|
updatePagination();
|
|
}
|
|
);
|
|
}
|
|
|
|
function getRoomConf(roomName){
|
|
vroomApi(
|
|
'get_room_conf',
|
|
{
|
|
room: roomName,
|
|
},
|
|
function(data){
|
|
// Reset the list of email displayed, so first remove evry input field but the last one
|
|
// We keep it so we can clone it again
|
|
$('.email-list').find('.email-entry:not(:last)').remove();
|
|
$.each(data.notif, function(index, obj){
|
|
addEmailInputField('email-list-notification', obj.email);
|
|
});
|
|
// Now, remove the first one if the list is not empty
|
|
if (Object.keys(data.notif).length > 0){
|
|
$('.email-list').find('.email-entry:first').remove();
|
|
}
|
|
else{
|
|
$('.email-list').find('.email-entry:first').find('input:first').val('');
|
|
}
|
|
adjustAddRemoveEmailButtons();
|
|
// Update config switches
|
|
$('#lockedSet').bootstrapSwitch('state', data.locked);
|
|
$('#askForNameSet').bootstrapSwitch('state', data.ask_for_name);
|
|
$('#joinPassSet').bootstrapSwitch('state', data.join_auth);
|
|
$('#ownerPassSet').bootstrapSwitch('state', data.owner_auth);
|
|
$('#persistentSet').bootstrapSwitch('state', data.persistent);
|
|
$('#maxMembers').val(data.max_members);
|
|
// Hide the password inputs
|
|
$('#joinPassFields,#ownerPassFields').hide();
|
|
// And display the config modal dialog
|
|
$('#configureModal').modal('show');
|
|
}
|
|
);
|
|
}
|
|
|
|
// Handle submiting the configuration form
|
|
$(document).on('click', '.btn-configure', function(){
|
|
roomName = $(this).data('room');
|
|
getRoomConf(roomName);
|
|
});
|
|
|
|
// Submitting the delete form
|
|
$(document).on('click', '.btn-remove', function(){
|
|
roomName = $(this).data('room');
|
|
$('#deleteRoomModal').modal('show');
|
|
});
|
|
|
|
// Get room list right after loading the page
|
|
getRooms();
|
|
|
|
// Delete room form
|
|
$('#deleteRoomForm').submit(function(e){
|
|
e.preventDefault();
|
|
vroomApi(
|
|
'delete_room',
|
|
{
|
|
room: roomName,
|
|
},
|
|
function(data){
|
|
$.notify(data.msg, 'success');
|
|
getRooms();
|
|
$('#deleteRoomModal').modal('hide');
|
|
}
|
|
);
|
|
});
|
|
|
|
// Update room list when searching
|
|
$('#searchRoom').on('input', function(){
|
|
var lastInput = +new Date;
|
|
setTimeout(function(){
|
|
if (lastInput + 500 < +new Date){
|
|
updateRoomList($('#searchRoom').val(), 0, itemPerPage);
|
|
updatePagination();
|
|
}
|
|
}, 600);
|
|
});
|
|
}
|
|
|
|
// Audit page
|
|
function initAdminAudit(){
|
|
var eventList = {};
|
|
var matches = 0;
|
|
|
|
// Update display of event list
|
|
function updateEventList(filter, min, max){
|
|
$('#loadingIcon').show();
|
|
$('#eventList').html('');
|
|
var filterRe = new RegExp(filter, "gi");
|
|
var i = 0;
|
|
matches = 0;
|
|
$.each(eventList, function (index, obj){
|
|
if (filter === '' ||
|
|
( obj.event.match(filterRe) ||
|
|
obj.from_ip.match(filterRe) ||
|
|
obj.user.match(filterRe) ||
|
|
obj.message.match(filterRe))
|
|
){
|
|
matches ++;
|
|
if (i >= min && i < max){
|
|
var t = obj.date.split(/[- :]/);
|
|
var date = utc2Local(new Date(t[0], t[1]-1, t[2], t[3], t[4], t[5])).toLocaleString();
|
|
$('#eventList').append($('<tr>')
|
|
.append($('<td>').html(stringEscape(obj.id)).addClass('hidden-xs'))
|
|
.append($('<td>').html(stringEscape(date)))
|
|
.append($('<td>').html(stringEscape(obj.from_ip)))
|
|
.append($('<td>').html(stringEscape(obj.event)).addClass('hidden-xs'))
|
|
.append($('<td>').html(stringEscape(obj.user)).addClass('hidden-xs'))
|
|
.append($('<td>').html(stringEscape(obj.message)))
|
|
);
|
|
}
|
|
i++;
|
|
}
|
|
});
|
|
$('#loadingIcon').hide();
|
|
}
|
|
|
|
function updatePagination(){
|
|
if (matches <= itemPerPage){
|
|
$('#pagination').hide(200);
|
|
return;
|
|
}
|
|
var total = Math.ceil(matches / itemPerPage);
|
|
if (total === 0){
|
|
total = 1;
|
|
}
|
|
$('#pagination').bootpag({
|
|
total: total,
|
|
maxVisible: 10,
|
|
page: 1
|
|
}).on('page', function(e, page){
|
|
var min = itemPerPage * (page - 1);
|
|
var max = min + itemPerPage;
|
|
updateEventList($('#searchEvent').val(), min, max);
|
|
});
|
|
$('#pagination').show(200);
|
|
}
|
|
|
|
function reloadEvents(start,end){
|
|
vroomApi(
|
|
'get_event_list',
|
|
{
|
|
start: start,
|
|
end: end
|
|
},
|
|
function(data){
|
|
eventList = data.events;
|
|
matches = Object.keys(eventList).length;
|
|
updateEventList($('#eventSearch').val(), 0, itemPerPage);
|
|
updatePagination();
|
|
}
|
|
);
|
|
}
|
|
|
|
// When we click on either refresh or export buttons, check dates are valid
|
|
$('#events_refresh,#events_export').click(function(e){
|
|
e.preventDefault();
|
|
var startObj = new Date($('#dateStart').val());
|
|
var endObj = new Date($('#dateEnd').val());
|
|
if (!$('#dateStart').val().match(dateRe)){
|
|
$('#dateStart').notify(localize('ERROR_DATE_INVALID'), {
|
|
class: 'error',
|
|
position: 'bottom center'
|
|
});
|
|
return false;
|
|
}
|
|
else if (!$('#dateEnd').val().match(dateRe)){
|
|
$('#dateEnd').notify(localize('ERROR_DATE_INVALID'), {
|
|
class: 'error',
|
|
position: 'bottom center'
|
|
});
|
|
return false;
|
|
}
|
|
else if (startObj > endObj){
|
|
$('#dateEnd').notify(localize('ERROR_END_MUST_BE_AFTER_START'), {
|
|
class: 'error',
|
|
position: 'bottom center'
|
|
});
|
|
return false;
|
|
}
|
|
else{
|
|
// Now, either refresh the current view
|
|
if ($(this).attr('id') === 'events_refresh'){
|
|
reloadEvents($('#dateStart').val(),$('#dateEnd').val());
|
|
return;
|
|
}
|
|
// Or export in XLSX as requested
|
|
else if ($(this).attr('id') === 'events_export'){
|
|
window.location.assign('export_events?from=' + $('#dateStart').val() + '&to=' + $('#dateEnd').val());
|
|
}
|
|
}
|
|
});
|
|
|
|
// Filter as you type, but with a small delay
|
|
$('#searchEvent').on('input', function(){
|
|
$('#loadingIcon').show();
|
|
var lastInput = +new Date;
|
|
setTimeout(function(){
|
|
if (lastInput + 500 < +new Date){
|
|
$('#loadingIcon').show();
|
|
$('#pagination').html('');
|
|
updateEventList($('#searchEvent').val(), 0, itemPerPage);
|
|
updatePagination();
|
|
$('#searchEvent').val();
|
|
}
|
|
}, 600);
|
|
});
|
|
|
|
// When loading the page, display events from the current day
|
|
reloadEvents($('#dateStart').val(),$('#dateEnd').val());
|
|
}
|
|
|
|
// Started when entering a room
|
|
function initJoin(room){
|
|
// Auth input if access is protected
|
|
$('#authBeforeJoinPass').on('input', function() {
|
|
var pass = $('#authBeforeJoinPass').val();
|
|
if (pass.length > 0 && pass.length < 200){
|
|
$('#authBeforeJoinButton').removeClass('disabled');
|
|
$('#authBeforeJoinPass').parent().removeClass('has-error');
|
|
}
|
|
else{
|
|
$('#authBeforeJoinButton').addClass('disabled');
|
|
$('#authBeforeJoinPass').parent().addClass('has-error');
|
|
$('#authBeforeJoinPass').notify(pass.length < 1 ? localize('PASSWORD_REQUIRED') : localize('PASSWORD_TOO_LONG'), 'error');
|
|
}
|
|
});
|
|
// Submit the join password form
|
|
$('#authBeforeJoinForm').submit(function(event){
|
|
event.preventDefault();
|
|
var pass = $('#authBeforeJoinPass').val();
|
|
if (pass.length > 0 && pass.length < 200){
|
|
$('#auth-before-join').slideUp();
|
|
try_auth(pass, true);
|
|
}
|
|
});
|
|
|
|
// This is the displayName input before joining the room
|
|
$('#displayNamePre').on('input', function() {
|
|
var name = $('#displayNamePre').val();
|
|
if (name.length > 0 && name.length < 50){
|
|
$('#displayNamePreButton').removeClass('disabled');
|
|
$('#displayNamePre').parent().removeClass('has-error');
|
|
}
|
|
else{
|
|
$('#displayNamePreButton').addClass('disabled');
|
|
$('#displayNamePre').parent().addClass('has-error');
|
|
$('#displayNamePre').notify(name.length < 1 ? localize('DISPLAY_NAME_REQUIRED') : localize('DISPLAY_NAME_TOO_LONG'), 'error');
|
|
}
|
|
});
|
|
|
|
// Intercept form submission
|
|
$('#displayNamePreForm').submit(function(event){
|
|
event.preventDefault();
|
|
var name = $('#displayNamePre').val();
|
|
if (name.length > 0 && name.length < 50){
|
|
$('#display-name-pre').slideUp();
|
|
$('#displayName').val(name);
|
|
peers.local.hasName = true;
|
|
peers.local.displayName = name;
|
|
updateDisplayName('local');
|
|
$('#chatBox').removeAttr('disabled').removeAttr('placeholder');
|
|
init_webrtc(room);
|
|
}
|
|
});
|
|
|
|
|
|
function try_auth(pass, showErrorMsg){
|
|
vroomApi(
|
|
'authenticate',
|
|
{
|
|
room: room,
|
|
password: pass
|
|
},
|
|
function(data){
|
|
$('.connecting-err-reason').hide();
|
|
// Once auth is passed, we retrieve room configuration
|
|
vroomApi(
|
|
'get_room_conf',
|
|
{
|
|
room: room,
|
|
},
|
|
function(data){
|
|
roomInfo = data;
|
|
// If our name is asked before joining the room, display the corresponding modal
|
|
// Else, just continue (with webrtc initialization
|
|
if (roomInfo.ask_for_name){
|
|
$('#display-name-pre').slideDown();
|
|
}
|
|
else{
|
|
init_webrtc(roomName);
|
|
}
|
|
}
|
|
);
|
|
},
|
|
function(data){
|
|
// 401 means password is needed
|
|
if (data.status === 401){
|
|
data = data.responseJSON;
|
|
$('.connecting-err-reason').text(data.msg);
|
|
$('#auth-before-join').slideDown();
|
|
}
|
|
// 403 is when the room is locked, and there's no owner pass
|
|
// So there's nothing to do, the access will be denied
|
|
else if (data.status === 403){
|
|
data = data.responseJSON;
|
|
$('.connecting-err-reason').text(data.msg);
|
|
$('.connecting-msg').not('#room-is-locked').remove();
|
|
$('#room-is-locked').slideDown();
|
|
}
|
|
if (showErrorMsg){
|
|
data = data.responseJSON;
|
|
if (data.msg){
|
|
$.notify(data.msg, 'error');
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
// Always start by trying with an empty password.
|
|
// Only if one is required the modal will appear
|
|
try_auth('', false);
|
|
}
|
|
|
|
// This just creates the webrtc object, and then continues
|
|
function init_webrtc(room){
|
|
// First get SimpleWebRTC config from the server
|
|
vroomApi(
|
|
'get_rtc_conf',
|
|
{
|
|
room: room,
|
|
},
|
|
function(data){
|
|
if (!video){
|
|
data.config.media.video = false;
|
|
}
|
|
data.config.localVideoEl = 'webRTCVideoLocal';
|
|
webrtc = new SimpleWebRTC(data.config);
|
|
// Handle the readyToCall event: join the room
|
|
webrtc.once('readyToCall', function (){
|
|
peers.local.id = webrtc.connection.connection.socket.sessionid;
|
|
webrtc.joinRoom(roomName);
|
|
});
|
|
|
|
// If browser doesn't support webRTC or dataChannels
|
|
if (!webrtc.capabilities.support || !webrtc.capabilities.supportGetUserMedia || !webrtc.capabilities.supportDataChannel){
|
|
$('.connecting-msg').not('#no-webrtc-msg').remove();
|
|
$('#no-webrtc-msg').slideDown();
|
|
}
|
|
else{
|
|
// Hide screen sharing btn if not supported, or when connecting from a mobile
|
|
if (!webrtc.capabilities.supportScreenSharing || !$.browser.desktop){
|
|
$('.btn-share-screen').remove();
|
|
}
|
|
initVroom(room);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// This is the main function called when you join a room (after auth and all the basic stuff ready)
|
|
function initVroom(room) {
|
|
|
|
var mainVid = false,
|
|
chatHistory = {},
|
|
chatIndex = 0,
|
|
maxVol = -100,
|
|
colorChanged = false;
|
|
|
|
$('#name_local').css('background-color', peers.local.color);
|
|
|
|
// Return the number of peers in the room
|
|
function countPeers(){
|
|
var count = Object.keys(peers).length;
|
|
// Do not count ourself
|
|
count--;
|
|
// and do not count our local screen
|
|
if (peers.local.screenShared){
|
|
count--;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// Get the role of a peer, by Socket.IO peer ID
|
|
function getPeerRole(id){
|
|
vroomApi(
|
|
'get_peer_role',
|
|
{
|
|
room: roomName,
|
|
peer_id: id
|
|
},
|
|
function(data){
|
|
// If its our own role, check if it chagned
|
|
if (id === peers.local.id){
|
|
if (data.role != peers.local.role){
|
|
// In which case, we refresh room config
|
|
// and tell all other peers we've changed
|
|
getRoomInfo();
|
|
webrtc.sendToAll('role_change', {});
|
|
}
|
|
peers.local.role = data.role;
|
|
if (data.role === 'owner'){
|
|
$('.unauthEl').hide(500);
|
|
$('.ownerEl').show(500);
|
|
}
|
|
else {
|
|
if (roomInfo.owner_auth){
|
|
$('.unauthEl').show(500);
|
|
}
|
|
$('.ownerEl').hide(500);
|
|
}
|
|
}
|
|
else if (peers[id]){
|
|
peers[id].role = data.role;
|
|
if (data.role === 'owner'){
|
|
// If this peer is an owner, we add the mark on its preview
|
|
$('#overlay_' + id).append($('<div>').attr('id', 'owner_' + id).addClass('owner hidden-xs'));
|
|
// And we disable owner's action for him
|
|
$('#ownerActions_' + id).remove();
|
|
}
|
|
else{
|
|
$('#owner_' + id).remove();
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// Put a video on the mainVideo div, called when you click on a video preview
|
|
function handlePreviewClick(el, id){
|
|
var wait = 1;
|
|
// There's already a main video, let's hide it
|
|
// and delay the new one so the fade out has time to complete
|
|
if ($('#mainVideo video').length > 0){
|
|
$('#mainVideo').hide(200);
|
|
wait = 200;
|
|
// Play all previews
|
|
// the one in the mainVid was muted
|
|
$('#webRTCVideo video').each(function(){
|
|
if ($(this).get(0).volume == 0 && $(this).attr('id') !== 'webRTCVideoLocal'){
|
|
$(this).get(0).volume= .7;
|
|
}
|
|
});
|
|
}
|
|
setTimeout(function(){
|
|
// To prevent a freeze, change the src before removing the video from the DOM
|
|
// See https://bugzilla-dev.allizom.org/show_bug.cgi?id=937110
|
|
if ($.browser.mozilla && $('#mainVideo video').length > 0){
|
|
$($('#mainVideo video').get(0)).attr('src', $($('#mohPlayer').get(0)).attr('src'));
|
|
$('#mainVideo video').get(0).pause();
|
|
}
|
|
$('#mainVideo').html('');
|
|
if (el.hasClass('selected')){
|
|
el.removeClass('selected');
|
|
}
|
|
else{
|
|
// Clone the clicked preview and put it in the main div
|
|
$('#mainVideo').html(el.clone().dblclick(function(){
|
|
fullScreen(this);
|
|
})
|
|
.bind('contextmenu', function(){
|
|
return false;
|
|
})
|
|
.css('max-height', maxHeight())
|
|
.removeClass('latencyUnknown latencyGood latencyMedium latencyWarn latencyPoor')
|
|
.attr('id', el.attr('id') + '_main')
|
|
);
|
|
$('.selected').removeClass('selected');
|
|
el.addClass('selected');
|
|
mainVid = el.attr('id');
|
|
// Cut the volume of the corresponding preview
|
|
$('#webRTCVideo video').each(function(){
|
|
if ($(this).get(0).paused){
|
|
$(this).get(0).play();
|
|
}
|
|
});
|
|
if (el.attr('id') !== 'webRTCVideoLocal'){
|
|
el.get(0).volume = 0;
|
|
$('#mainVideo video').get(0).volume = 1;
|
|
}
|
|
else {
|
|
// If we put our own video in the main div, we need to mute it
|
|
$('#mainVideo video').get(0).volume = 0;
|
|
}
|
|
$('#mainVideo').show(200);
|
|
}
|
|
}, wait);
|
|
}
|
|
|
|
// Logout
|
|
function hangupCall(){
|
|
webrtc.leaveRoom();
|
|
}
|
|
|
|
// Handle a new video (either another peer, or a screen
|
|
// including our own local screen)
|
|
function addVideo(video, peer){
|
|
playSound('join.mp3');
|
|
// The div container of this new video
|
|
// will contain the video preview plus all other info like displayName, overlay and volume bar
|
|
var div = $('<div>').addClass('col-xs-3 col-sm-12 col-lg-6 previewContainer').append(video).appendTo('#webRTCVideo');
|
|
var id;
|
|
// Peer isn't defined ? it's our own local screen
|
|
if (!peer){
|
|
id = 'local';
|
|
$('<div>').addClass('displayName').attr('id', 'name_local_screen').appendTo(div);
|
|
updateDisplayName(id);
|
|
$(video).addClass('latencyUnknown');
|
|
}
|
|
// video id contains screen ? it's a peer sharing it's screen
|
|
else if (video.id.match(/screen/)){
|
|
id = peer.id + '_screen';
|
|
var peer_id = video.id.replace('_screen_incoming', '');
|
|
$('<div>').addClass('displayName').attr('id', 'name_' + peer_id + '_screen').appendTo(div);
|
|
updateDisplayName(peer_id);
|
|
}
|
|
// It's the webcam of a peer
|
|
// add the volume bar and the mute/pause overlay
|
|
else{
|
|
id = peer.id;
|
|
// Create 4 divs which will contains the volume bar, the displayName, the muted/paused and
|
|
// the owner actions (overlay)
|
|
$('<div>').addClass('volumeBar').attr('id', 'volume_' + id).appendTo(div);
|
|
$('<div>').addClass('displayName').attr('id', 'name_' + id).appendTo(div);
|
|
$('<div>').attr('id', 'overlay_' + id).addClass('hidden-xs').appendTo(div);
|
|
// Will contain per peer action menu (mute/pause/kick), but will only be displayed
|
|
// on owner's screen
|
|
$('<div>').addClass('ownerActions hidden-xs').attr('id', 'ownerActions_' + id).appendTo(div)
|
|
.append($('<div>',{
|
|
class: 'btn-group'
|
|
})
|
|
.append($('<button>', {
|
|
class: 'actionMute btn btn-default btn-sm',
|
|
id: 'actionMute_' + id,
|
|
click: function() { mutePeer(id) },
|
|
}).prop('title', localize('MUTE_PEER')))
|
|
.append($('<button>', {
|
|
class: 'actionPause btn btn-default btn-sm',
|
|
id: 'actionPause_' + id,
|
|
click: function() { pausePeer(id) },
|
|
}).prop('title', localize('SUSPEND_PEER')))
|
|
.append($('<button>', {
|
|
class: 'actionPromote btn btn-default btn-sm',
|
|
id: 'actionPromote_' + id,
|
|
click: function() { promotePeer(id) },
|
|
}).prop('title', localize('PROMOTE_PEER')))
|
|
.append($('<button>', {
|
|
class: 'actionKick btn btn-default btn-sm',
|
|
id: 'actionKick_' + id,
|
|
click: function() { kickPeer(id) },
|
|
}).prop('title', localize('KICK_PEER'))));
|
|
// Display the menu now if we're an owner, but add some delay
|
|
// as those actions won't work until all the channels are ready
|
|
setTimeout (function(){
|
|
$(div).hover(
|
|
function(){
|
|
if (peers.local.role === 'owner' || peers.local.role === 'admin'){
|
|
$('#ownerActions_' + id).show(200);
|
|
}
|
|
},
|
|
function(){
|
|
$('#ownerActions_' + id).hide(200);
|
|
}
|
|
);
|
|
}, 3500);
|
|
// Choose a random color. If everything is OK
|
|
// this peer will send us its own color in a few seconds
|
|
var color = chooseColor();
|
|
// Now store this new peer in our object
|
|
peers[peer.id] = {
|
|
displayName: peer.id,
|
|
color: color,
|
|
micMuted: false,
|
|
videoPaused: false,
|
|
hasVideo: true,
|
|
lastPong: +new Date,
|
|
dc: peer.getDataChannel('vroom'),
|
|
obj: peer
|
|
};
|
|
// Send our info to this peer (displayName/color)
|
|
// but wait a bit so the "vroom" dataChannel created earlier is fully setup (or have more chances to be)
|
|
// before we send
|
|
setTimeout(function(){
|
|
// displayName is private data, lets send it through dataChannel
|
|
if ($('#displayName').val() !== ''){
|
|
peer.sendDirectly('vroom','set_display_name', $('#displayName').val());
|
|
}
|
|
// color can be sent through the signaling channel
|
|
peer.send('peer_color', {color: peers.local.color});
|
|
// if we don't have a video, just signal it to this peer
|
|
peer.send('media_info', {video: !!video});
|
|
// We don't have chat history yet ? Lets ask to this new peer
|
|
if(!peers.local.hasHistory && chatIndex == 0){
|
|
peer.sendDirectly('vroom', 'chat_history_request', '');
|
|
}
|
|
// Get the role of this peer
|
|
getPeerRole(peer.id);
|
|
}, 3000);
|
|
video.volume = .7;
|
|
$(video).addClass('latencyUnknown');
|
|
// Stop moh, we're not alone anymore
|
|
$('#mohPlayer')[0].pause();
|
|
$('.aloneEl').hide(300);
|
|
}
|
|
$(div).attr('id', 'peer_' + id);
|
|
// Disable context menu on the video
|
|
$(video).bind('contextmenu', function(){
|
|
return false;
|
|
});
|
|
// And go full screen on double click
|
|
// TODO: also handle double tap
|
|
$(video).dblclick(function(){
|
|
fullScreen(this);
|
|
});
|
|
// Simple click put this preview in the mainVideo div
|
|
$(video).click(function(){
|
|
handlePreviewClick($(this), id);
|
|
});
|
|
// Now display the div
|
|
div.show(200);
|
|
}
|
|
|
|
// Update volume of the corresponding peer
|
|
// Taken from SimpleWebRTC demo
|
|
function showVolume(el, volume) {
|
|
if (!el){
|
|
return;
|
|
}
|
|
if (volume < -45){ // vary between -45 and -20
|
|
el.css('height', '0px');
|
|
}
|
|
else if (volume > -20){
|
|
el.css('height', '100%');
|
|
}
|
|
else {
|
|
el.css('height', Math.floor((volume + 100) * 100 / 25 - 220) + '%');
|
|
}
|
|
}
|
|
|
|
// Graphical indicator for the latency between you and a peer
|
|
function updatePeerLatency(id,time){
|
|
if (!peers[id]){
|
|
return;
|
|
}
|
|
var cl = 'latencyPoor';
|
|
if (time < 60){
|
|
cl = 'latencyGood';
|
|
}
|
|
else if (time < 120){
|
|
cl = 'latencyMedium';
|
|
}
|
|
else if (time < 250){
|
|
cl = 'latencyWarn';
|
|
}
|
|
$('#' + id + '_video_incoming').removeClass('latencyUnknown latencyGood latencyMedium latencyWarn latencyPoor').addClass(cl);
|
|
}
|
|
|
|
// Add a new message to the chat history
|
|
function newChatMessage(from, message, time, color){
|
|
var cl = (from === 'local') ? 'chatMsgSelf' : 'chatMsgOthers';
|
|
// We need to check time format as it can be sent from another
|
|
// peer if we asked for its history. If it's not define or doesn't look
|
|
// correct, we'll get time locally
|
|
if (!time || !time.match(/^\d{1,2}:\d{1,2}:\d{1,2}$/)){
|
|
time = getTime();
|
|
}
|
|
// It's a message sent from a connected peer, we should have its info
|
|
if (peers[from] && peers[from].color){
|
|
var color = peers[from].color;
|
|
var displayName = peers[from].displayName;
|
|
}
|
|
// this peer might not be defined if we're importing chat history
|
|
// So just use the from as the displayName and the provided color
|
|
else{
|
|
var color = (color && color.match(/#[\da-f]{6}/i)) ? color : chooseColor();
|
|
var displayName = from;
|
|
}
|
|
// Create the new msg
|
|
var newmsg = $('<div>').addClass('chatMsg ' + cl)
|
|
.append($('<b>' + time + ' ' + stringEscape(displayName) + '</b><p>' + linkify(stringEscape(message)) + '</p>'))
|
|
.css('background-color', color);
|
|
// Add it in the history
|
|
$('<div>').addClass('row chatMsgContainer').append(newmsg).appendTo('#chatHistory');
|
|
// Move the scroller down
|
|
$('#chatHistory').scrollTop($('#chatHistory').prop('scrollHeight'));
|
|
// Record this message in the history object
|
|
// so we can send it to other peers asking for it
|
|
chatHistory[chatIndex] = {
|
|
time: time,
|
|
from: displayName,
|
|
color: color,
|
|
message: message
|
|
}
|
|
chatIndex++;
|
|
}
|
|
|
|
// Mute a peer
|
|
function mutePeer(id, globalAction){
|
|
if (peers[id] && peers[id].role !== 'owner'){
|
|
if (!globalAction ||
|
|
(!peers[id].micMuted && globalAction === 'mute') ||
|
|
(peers[id].micMuted && globalAction === 'unmute')){
|
|
var msg = localize('YOU_HAVE_MUTED_s');
|
|
var who = (peers[id].hasName) ? peers[id].displayName : localize('A_PARTICIPANT');
|
|
if (peers[id].micMuted){
|
|
msg = localize('YOU_HAVE_UNMUTED_s')
|
|
}
|
|
// notify everyone that we have muted this peer
|
|
webrtc.sendToAll('owner_toggle_mute', {peer: id});
|
|
$.notify(sprintf(msg, who), 'info');
|
|
}
|
|
}
|
|
// We cannot mute another owner
|
|
else if (!globalAction){
|
|
$.notify(localize('CANT_MUTE_OWNER'), 'error');
|
|
}
|
|
}
|
|
|
|
// Pause a peer's webcam
|
|
function pausePeer(id, globalAction){
|
|
if (peers[id] && peers[id].role !== 'owner'){
|
|
if (!globalAction ||
|
|
(!peers[id].videoPaused && globalAction === 'pause') ||
|
|
(peers[id].videoPaused && globalAction === 'resume')){
|
|
var msg = localize('YOU_HAVE_SUSPENDED_s');
|
|
var who = (peers[id].hasName) ? peers[id].displayName : localize('A_PARTICIPANT');
|
|
if (peers[id].videoPaused){
|
|
msg = localize('YOU_HAVE_RESUMED_s');
|
|
}
|
|
webrtc.sendToAll('owner_toggle_pause', {peer: id});
|
|
$.notify(sprintf(msg, who), 'info');
|
|
}
|
|
}
|
|
else if (!globalAction){
|
|
$.notify(localize('CANT_SUSPEND_OWNER'), 'error');
|
|
}
|
|
}
|
|
|
|
// Promote a peer (he will be owner)
|
|
function promotePeer(id){
|
|
if (peers[id] && peers[id].role !== 'owner'){
|
|
vroomApi(
|
|
'promote_peer',
|
|
{
|
|
room: roomName,
|
|
peer_id: id
|
|
},
|
|
function(data){
|
|
webrtc.sendToAll('owner_promoted', {peer: id});
|
|
$.notify(data.msg, 'success');
|
|
}
|
|
);
|
|
suspendButton($('#actionPromote_' + id));
|
|
}
|
|
else if (peers[id]){
|
|
$.notify(localize('CANT_PROMOTE_OWNER'), 'error');
|
|
}
|
|
}
|
|
|
|
// Kick a peer out of the room
|
|
function kickPeer(id){
|
|
if (peers[id] && peers[id].role !== 'owner'){
|
|
webrtc.sendToAll('owner_kick', {peer: id});
|
|
// Wait a bit for the peer to leave, but end connection if it's still here
|
|
// after 2 seconds
|
|
setTimeout(function(){
|
|
if (peers[id]){
|
|
peers[id].obj.end();
|
|
}
|
|
}, 2000);
|
|
var who = (peers[id].hasName) ? peers[id].displayName : localize('A_PARTICIPANT');
|
|
$.notify(sprintf(localize('YOU_HAVE_KICKED_s'), who), 'info');
|
|
}
|
|
else{
|
|
$.notify(localize('CANT_KICK_OWNER'), 'error');
|
|
}
|
|
}
|
|
|
|
// Mute our own mic
|
|
function muteMic(){
|
|
webrtc.mute();
|
|
peers.local.micMuted = true;
|
|
showVolume($('#localVolume'), -45);
|
|
}
|
|
|
|
// Unmute our own mic
|
|
function unmuteMic(){
|
|
webrtc.unmute();
|
|
peers.local.micMuted = false;
|
|
}
|
|
|
|
// Suspend or webcam
|
|
function suspendCam(){
|
|
webrtc.pauseVideo();
|
|
peers.local.videoPaused = true;
|
|
}
|
|
|
|
// Resume webcam
|
|
function resumeCam(){
|
|
webrtc.resumeVideo();
|
|
peers.local.videoPaused = false;
|
|
}
|
|
|
|
// Check if MoH is needed
|
|
function checkMoh(){
|
|
setTimeout(function(){
|
|
if (countPeers() < 1){
|
|
if ($('.btn-moh').first().hasClass('btn-danger')){
|
|
$('#mohPlayer').get(0).volume = .25;
|
|
$('#mohPlayer').get(0).play();
|
|
}
|
|
$('.aloneEl').show(200);
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
// Load etherpad in its iFrame
|
|
function loadEtherpadIframe(){
|
|
$('#etherpadContainer').pad({
|
|
host: etherpad.host,
|
|
baseUrl: etherpad.path,
|
|
padId: etherpad.group + '$' + room,
|
|
showControls: true,
|
|
showLineNumbers: true,
|
|
height: maxHeight()-7 + 'px',
|
|
border: 2,
|
|
userColor: peers.local.color,
|
|
userName: peers.local.displayName
|
|
});
|
|
}
|
|
|
|
// An owner is muting/unmuting someone
|
|
webrtc.on('owner_toggle_mute', function(data){
|
|
// Ignore this if the remote peer isn't an owner of the room
|
|
// or if the peer receiving it is our local screen sharing
|
|
if (peers[data.id].role !== 'owner' || data.roomType === 'screen'){
|
|
return;
|
|
}
|
|
// We are the one being (un)muted, and we're not owner
|
|
// Be nice and obey
|
|
if (data.payload.peer && data.payload.peer == peers.local.id && peers.local.role !== 'owner'){
|
|
var who = (peers[data.id].hasName) ? peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
if (!peers.local.micMuted){
|
|
muteMic();
|
|
$.notify(sprintf(localize('s_IS_MUTING_YOU'), who), 'info');
|
|
}
|
|
else {
|
|
unmuteMic();
|
|
$.notify(sprintf(localize('s_IS_UNMUTING_YOU'), who), 'info');
|
|
}
|
|
$('.btn-mute-mic').toggleClass('btn-danger').button('toggle');
|
|
}
|
|
// It's another peer of the room
|
|
else if (data.payload.peer != peers.local.id && peers[data.payload.peer]){
|
|
var who = (peers[data.id].hasName) ?
|
|
peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
var target = (peers[data.payload.peer].hasName) ?
|
|
peers[data.payload.peer].displayName : localize('A_PARTICIPANT');
|
|
if (peers[data.payload.peer].micMuted){
|
|
$.notify(sprintf(localize('s_IS_UNMUTING_s'), who, target), 'info');
|
|
}
|
|
else{
|
|
$.notify(sprintf(localize('s_IS_MUTING_s'), who, target), 'info');
|
|
}
|
|
}
|
|
});
|
|
|
|
// An owner is pausing/resuming a webcam.
|
|
// More or less the same dance than mute/unmute. Both fonctions should be merged
|
|
webrtc.on('owner_toggle_pause', function(data){
|
|
if (peers[data.id].role != 'owner' || data.roomType == 'screen'){
|
|
return;
|
|
}
|
|
if (data.payload.peer && data.payload.peer == peers.local.id && peers.local.role != 'owner'){
|
|
var who = (peers[data.id].hasName) ? peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
if (!peers.local.videoPaused){
|
|
suspendCam();
|
|
$.notify(sprintf(localize('s_IS_SUSPENDING_YOU'), who), 'info');
|
|
}
|
|
else{
|
|
resumeCam();
|
|
$.notify(sprintf(localize('s_IS_RESUMING_YOU'), who), 'info');
|
|
}
|
|
$('.btn-suspend-cam').toggleClass('btn-danger').button('toggle');
|
|
}
|
|
else if (data.payload.peer != peers.local.id && peers[data.payload.peer]){
|
|
var who = (peers[data.id].hasName) ?
|
|
peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
var target = (peers[data.payload.peer].hasName) ?
|
|
peers[data.payload.peer].displayName : localize('A_PARTICIPANT');
|
|
if (peers[data.payload.peer].videoPaused){
|
|
$.notify(sprintf(localize('s_IS_RESUMING_s'), who, target), 'info');
|
|
}
|
|
else{
|
|
$.notify(sprintf(localize('s_IS_SUSPENDING_s'), who, target), 'info');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Room config has been updated
|
|
webrtc.on('room_conf_updated', function(data){
|
|
var who = (peers[data.id].hasName) ? peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
getRoomInfo();
|
|
$.notify(sprintf(localize('s_CHANGED_ROOM_CONFIG'), who), 'success');
|
|
});
|
|
|
|
webrtc.on('media_info', function(data){
|
|
// This peer indicates he has no webcam
|
|
if (!data.payload.video){
|
|
$('#overlay_' + data.id).append($('<div>').attr('id', 'noWebcam_' + data.id).addClass('noWebcam'));
|
|
$('#actionPause_' + data.id).addClass('disabled');
|
|
peers[data.id].hasVideo = false;
|
|
}
|
|
});
|
|
|
|
// An owner has just promoted a participant of the room to the owner role
|
|
webrtc.on('owner_promoted', function(data){
|
|
// Ignore if the emitter is not an owner, or is a screen
|
|
if (peers[data.id].role !== 'owner' || data.roomType === 'screen'){
|
|
return;
|
|
}
|
|
// Are we the one being promoted ?
|
|
if (data.payload.peer && data.payload.peer === peers.local.id && peers.local.role !== 'owner'){
|
|
var who = (peers[data.id].hasName) ?
|
|
peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
getPeerRole(peers.local.id);
|
|
$.notify(sprintf(localize('s_IS_PROMOTING_YOU'), who), 'success');
|
|
}
|
|
else if (data.payload.peer !== peers.local.id && peers[data.payload.peer]){
|
|
var who = (peers[data.id].hasName) ?
|
|
peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
var target = (peers[data.payload.peer].hasName) ?
|
|
peers[data.payload.peer].displayName : localize('A_PARTICIPANT');
|
|
$.notify(sprintf(localize('s_IS_PROMOTING_s'), who, target), 'info');
|
|
}
|
|
});
|
|
|
|
// An owner is kicking someone out of the room
|
|
webrtc.on('owner_kick', function(data){
|
|
if (peers[data.id].role !== 'owner' || data.roomType === 'screen'){
|
|
return;
|
|
}
|
|
if (data.payload.peer && data.payload.peer === peers.local.id && peers.local.role !== 'owner'){
|
|
hangupCall;
|
|
window.location.assign(rootUrl + 'kicked/' + roomName);
|
|
}
|
|
else if (data.payload.peer !== peers.local.id && peers[data.payload.peer] && peers[data.payload.peer].role !== 'owner'){
|
|
var who = (peers[data.id].hasName) ?
|
|
peers[data.id].displayName : localize('A_ROOM_ADMIN');
|
|
var target = (peers[data.payload.peer].hasName) ?
|
|
peers[data.payload.peer].displayName : localize('A_PARTICIPANT');
|
|
$.notify(sprintf(localize('s_IS_KICKING_s'), who, target), 'info');
|
|
// Wait a bit for the peer to leave, but end connection if it's still here
|
|
// after 2 seconds
|
|
setTimeout(function(){
|
|
if (peers[data.payload.id]){
|
|
peers[data.payload.id].obj.end();
|
|
}
|
|
}, 2000);
|
|
}
|
|
});
|
|
|
|
// An owner is terminating the call, obey and leave the room now
|
|
webrtc.on('terminate_room', function(data){
|
|
if (peers[data.id].role != 'owner' || data.roomType == 'screen'){
|
|
return;
|
|
}
|
|
hangupCall;
|
|
window.location.assign(rootUrl + 'goodbye/' + roomName);
|
|
});
|
|
|
|
// Handle volume changes from our own mic
|
|
webrtc.on('volumeChange', function (volume, treshold){
|
|
// Record the highest level (used for the "no sound" detection)
|
|
if (volume > maxVol){
|
|
maxVol = volume;
|
|
}
|
|
// Do nothing if we're muted
|
|
if (peers.local.micMuted) {
|
|
return;
|
|
}
|
|
showVolume($('#localVolume'), volume);
|
|
});
|
|
|
|
// Handle dataChannel messages (incoming)
|
|
webrtc.on('channelMessage', function (peer, label, data){
|
|
// Handle volume changes from remote peers
|
|
if (data.type === 'volume'){
|
|
showVolume($('#volume_' + peer.id), data.volume);
|
|
}
|
|
// We only want to act on data received from the vroom channel
|
|
if (label !== 'vroom'){
|
|
return;
|
|
}
|
|
// Ping from the peer, lets just respond with a pong
|
|
// used to measure latency
|
|
else if (data.type === 'ping'){
|
|
peers[peer.id].obj.sendDirectly('vroom', 'pong', data.payload);
|
|
}
|
|
// Pong from the peer, lets compute reponse time
|
|
else if (data.type === 'pong'){
|
|
var diff = +new Date - parseInt(data.payload);
|
|
peers[peer.id].lastPong = +new Date;
|
|
updatePeerLatency(peer.id,diff);
|
|
}
|
|
// The peer sets a displayName, record this in our peers object
|
|
else if (data.type === 'set_display_name'){
|
|
var name = data.payload;
|
|
// Ignore it if it's too long
|
|
if (name.length > 50){
|
|
return;
|
|
}
|
|
// Set display name under the video
|
|
peers[peer.id].displayName = name;
|
|
if (name !== ''){
|
|
peers[peer.id].hasName = true;
|
|
}
|
|
else {
|
|
peers[peer.id].hasName = false;
|
|
}
|
|
updateDisplayName(peer.id);
|
|
}
|
|
// This peer asked for our chat history, lets send him
|
|
else if (data.type === 'chat_history_request'){
|
|
peer.sendDirectly('vroom', 'chat_history_response', JSON.stringify(chatHistory));
|
|
}
|
|
// This peer is sending its chat history (and we don't have it yet)
|
|
else if (data.type === 'chat_history_response' && !peers.local.hasHistory){
|
|
peers.local.hasHistory = true;
|
|
var history = JSON.parse(data.payload);
|
|
for (var i = 0; i < Object.keys(history).length; i++){
|
|
newChatMessage(history[i].from,history[i].message,history[i].time,history[i].color);
|
|
}
|
|
}
|
|
// One peer just sent a text chat message
|
|
else if (data.type === 'text_chat'){
|
|
// Notify if the chat menu is collapsed
|
|
if ($('#chatDropdown').hasClass('collapsed')){
|
|
$('#chatDropdown').addClass('btn-danger');
|
|
playSound('newmsg.mp3');
|
|
$('#unreadMsg').text(parseInt($('#unreadMsg').text())+1).show(1000);
|
|
}
|
|
newChatMessage(peer.id, data.payload);
|
|
}
|
|
});
|
|
|
|
// A peer is sending its color
|
|
webrtc.on('peer_color', function(data){
|
|
var color = data.payload.color;
|
|
if (!color.match(/#[\da-f]{6}/i)){
|
|
return;
|
|
}
|
|
peers[data.id].color = color;
|
|
$('#name_' + data.id).css('background-color', color);
|
|
$('#name_' + data.id + '_screen').css('background-color', color);
|
|
// Update the displayName but only if it has been set (no need to display a cryptic peer ID)
|
|
if (peers[data.id].hasName){
|
|
updateDisplayName(data.id);
|
|
}
|
|
});
|
|
|
|
// Received when a peer mute his mic, or pause the video
|
|
webrtc.on('mute', function(data){
|
|
// Muting
|
|
if (data.name === 'audio'){
|
|
// put the volume bar at the minimum
|
|
showVolume($('#volume_' + data.id), -46);
|
|
var div = 'mute_' + data.id,
|
|
cl = 'muted';
|
|
peers[data.id].micMuted = true;
|
|
$('#actionMute_' + data.id).addClass('btn-danger');
|
|
}
|
|
// Pausing webcam
|
|
else if (data.name === 'video'){
|
|
var div = 'pause_' + data.id,
|
|
cl = 'paused';
|
|
peers[data.id].videoPaused = true;
|
|
$('#actionPause_' + data.id).addClass('btn-danger');
|
|
}
|
|
else{
|
|
return;
|
|
}
|
|
$('#overlay_' + data.id).append($('<div>').attr('id', div).addClass(cl));
|
|
});
|
|
|
|
// Handle unmute/resume
|
|
webrtc.on('unmute', function(data){
|
|
if (data.name === 'audio'){
|
|
var el = '#mute_' + data.id;
|
|
peers[data.id].micMuted = false;
|
|
$('#actionMute_' + data.id).removeClass('btn-danger');
|
|
}
|
|
else if (data.name === 'video'){
|
|
var el = '#pause_' + data.id;
|
|
peers[data.id].videoPaused = false;
|
|
$('#actionPause_' + data.id).removeClass('btn-danger');
|
|
}
|
|
else{
|
|
return;
|
|
}
|
|
$(el).remove();
|
|
});
|
|
|
|
// This peer claims he changed its role (usually from participant to owner)
|
|
// Lets check this
|
|
webrtc.on('role_change', function(data){
|
|
if (data.roomType === 'screen'){
|
|
return;
|
|
}
|
|
getPeerRole(data.id);
|
|
});
|
|
|
|
// Notification from the server
|
|
webrtc.connection.connection.on('notification', function(data) {
|
|
if (!data.payload.class || !data.payload.class.match(/^(success)|(info)|(warning)|(error)$/)){
|
|
data.payload.class = 'info';
|
|
}
|
|
$.notify(data.payload.msg, {
|
|
className: data.payload.class,
|
|
autoHide: false
|
|
});
|
|
});
|
|
|
|
// When we joined the room
|
|
webrtc.on('joinedRoom', function(){
|
|
// Check if sound is detected and warn if it hasn't
|
|
if (window.AudioContext){
|
|
setTimeout(function (){
|
|
if (maxVol < -80){
|
|
$.notify(localize('NO_SOUND_DETECTED'), {
|
|
className: 'error',
|
|
autoHide: false
|
|
});
|
|
}
|
|
}, 10000);
|
|
}
|
|
// Notify the server a new participant has joined (ourself)
|
|
// If we were prompted for our display name before joining
|
|
// we send it. Not that I like sending this kind of data to the server
|
|
// but it's needed for email notifications
|
|
vroomApi(
|
|
'join',
|
|
{
|
|
room: roomName,
|
|
name: (peers.local.hasName) ? peers.local.displayName : '',
|
|
peer_id: peers.local.id
|
|
},
|
|
function(data){
|
|
if (data.msg){
|
|
$.notify(data.msg, 'success');
|
|
}
|
|
$('#videoLocalContainer').show(200);
|
|
$('#timeCounterXs,#timeCounter').tinyTimer({ from: new Date });
|
|
getPeerRole(peers.local.id);
|
|
setTimeout(function(){
|
|
$('#connecting').modal('hide');
|
|
}, 200);
|
|
}
|
|
);
|
|
checkMoh();
|
|
});
|
|
|
|
// Handle new video stream added: someone joined the room
|
|
webrtc.on('videoAdded', function(video, peer){
|
|
addVideo(video, peer);
|
|
});
|
|
|
|
// We share our screen
|
|
webrtc.on('localScreenAdded', function(video){
|
|
addVideo(video);
|
|
});
|
|
|
|
// error opening the webcam or mic stream
|
|
webrtc.on('localMediaError', function(){
|
|
$('#no-media-msg').slideDown();
|
|
// If video is disabled but we still have an error
|
|
// it means mic isn't available
|
|
if (!video){
|
|
$('#connecting-msg').slideUp();
|
|
}
|
|
});
|
|
|
|
// Handle video stream removed: someone leaved the room
|
|
webrtc.on('videoRemoved', function(video, peer){
|
|
playSound('leave.mp3');
|
|
var id = (peer) ? peer.id : 'local';
|
|
// Is the screen sharing of a peer being removed ?
|
|
if (video.id.match(/_screen_/)){
|
|
id = id + '_screen';
|
|
}
|
|
// Or the peer itself
|
|
else if (peer && peers[peer.id] && id !== 'local'){
|
|
delete peers[peer.id];
|
|
}
|
|
$('#peer_' + id).hide(300);
|
|
// Remove the div, but wait for the fade out to complete
|
|
setTimeout(function(){
|
|
$('#peer_' + id).remove();
|
|
}, 300);
|
|
if (mainVid === id + '_video_incoming' || mainVid === id + '_incoming' || mainVid === id + 'Screen'){
|
|
$('#mainVideo').hide(500);
|
|
setTimeout(function(){
|
|
if ($.browser.mozilla && $('#mainVideo video').length > 0){
|
|
$($('#mainVideo video').get(0)).attr('src', $($('#mohPlayer').get(0)).attr('src'));
|
|
$('#mainVideo video').get(0).pause();
|
|
}
|
|
$('#mainVideo').html('');
|
|
}, 500);
|
|
mainVid = false;
|
|
}
|
|
if (id != 'local'){
|
|
checkMoh();
|
|
}
|
|
});
|
|
|
|
// Display an message if an error occures during p2p connection
|
|
function p2pFailure(peer){
|
|
peer.end();
|
|
$.notify(localize('ERROR_ESTABLISHING_P2P'));
|
|
}
|
|
webrtc.on('iceFailed', function(peer){
|
|
p2pFailure(peer);
|
|
});
|
|
webrtc.on('connectivityError', function(peer){
|
|
p2pFailure(peer);
|
|
});
|
|
|
|
// Detect connection lost and display an error msg
|
|
// TODO: we should try to reconnect
|
|
webrtc.connection.connection.socket.on('disconnect', function(){
|
|
setTimeout(function(){
|
|
$('#disconnected').modal('show');
|
|
}, 2000);
|
|
});
|
|
|
|
// Handle Email Invitation
|
|
$('#inviteEmail').submit(function(event) {
|
|
event.preventDefault();
|
|
var rcpts = [],
|
|
message = $('#message').val();
|
|
$('input[name="invitation-recipients[]"]').each(function(){
|
|
rcpts.push($(this).val());
|
|
});
|
|
// Simple email address verification
|
|
// not fullproof, but email validation is a real nightmare
|
|
var validEmail = true;
|
|
$('.email-list').find('input').each(function(index, input){
|
|
if (!$(input).val().match(/\S+@\S+\.\S+/) && $(input).val() !== ''){
|
|
$(input).parent().addClass('has-error');
|
|
//$(input).parent().notify(localize('ERROR_MAIL_INVALID'), 'error');
|
|
validEmail = false;
|
|
// Break the each loop
|
|
return false;
|
|
}
|
|
else{
|
|
$(input).parent().removeClass('has-error');
|
|
}
|
|
});
|
|
if (!validEmail){
|
|
return false;
|
|
}
|
|
vroomApi(
|
|
'invite_email',
|
|
{
|
|
rcpts: rcpts,
|
|
message: message,
|
|
room: roomName
|
|
},
|
|
function(data){
|
|
$('#recipient').val('');
|
|
$('#inviteModal').modal('hide');
|
|
$('#email-list-invite').find('.email-entry:not(:last)').remove();
|
|
$('#email-list-invite').find('input').val('');
|
|
$('#message').val('');
|
|
$.notify(data.msg, 'success');
|
|
}
|
|
);
|
|
});
|
|
|
|
// Set your DisplayName
|
|
$('#displayName').on('input', function(){
|
|
var name = $('#displayName').val();
|
|
if (name.length > 50){
|
|
$('#displayName').parent().addClass('has-error');
|
|
$('#displayName').notify(localize('DISPLAY_NAME_TOO_LONG'), 'error');
|
|
return;
|
|
}
|
|
else{
|
|
$('#displayName').parent().removeClass('has-error');
|
|
}
|
|
// Enable chat input when you set your disaplay name
|
|
if (name !== '' && $('#chatBox').attr('disabled')){
|
|
$('#chatBox').removeAttr('disabled').removeAttr('placeholder');
|
|
peers.local.hasName = true;
|
|
}
|
|
// And disable it again if you remove your display name
|
|
else if (name === ''){
|
|
$('#chatBox').attr('disabled', true).attr('placeholder', localize('SET_YOUR_NAME_TO_CHAT'));
|
|
peers.local.hasName = false;
|
|
}
|
|
peers.local.displayName = name;
|
|
updateDisplayName('local');
|
|
webrtc.sendDirectlyToAll('vroom', 'set_display_name', name);
|
|
lastNameChange = +new Date;
|
|
// Should we reload etherpad iFrame with this new name ?
|
|
if (etherpad.enabled){
|
|
// Wait ~3 sec and reload etherpad
|
|
setTimeout(function(){
|
|
if (lastNameChange && lastNameChange + 3000 < +new Date && $('#etherpadContainer').html() !== ''){
|
|
loadEtherpadIframe();
|
|
}
|
|
}, 3100);
|
|
}
|
|
});
|
|
|
|
// ScreenSharing
|
|
$('.btn-share-screen').click(function() {
|
|
var action = ($(this).hasClass('btn-danger')) ? 'unshare' : 'share';
|
|
function cantShare(err){
|
|
$.notify(err, 'error');
|
|
return;
|
|
}
|
|
if (!peers.local.screenShared && action === 'share'){
|
|
webrtc.shareScreen(function(err){
|
|
// An error occured while sharing our screen
|
|
if(err){
|
|
if (err.name === 'EXTENSION_UNAVAILABLE' && $.browser.webkit){
|
|
$('#chromeExtMessage').modal('show');
|
|
}
|
|
// This error usually means you have denied access (old flag way)
|
|
// or you cancelled screen sharing (new extension way)
|
|
// or you select no window (in Firefox)
|
|
else if (err.name === 'PermissionDeniedError' && $.browser.mozilla){
|
|
$('#firefoxShareScreenModal').modal('show');
|
|
}
|
|
else if (err.name === 'PERMISSION_DENIED' ||
|
|
err.name === 'PermissionDeniedError' ||
|
|
err.name === 'ConstraintNotSatisfiedError'){
|
|
cantShare(localize('SCREEN_SHARING_CANCELLED'));
|
|
}
|
|
else{
|
|
cantShare(localize('CANT_SHARE_SCREEN'));
|
|
}
|
|
$('.btn-share-screen').removeClass('active');
|
|
return;
|
|
}
|
|
// Screen sharing worked, warn that everyone can see it
|
|
else{
|
|
$('.btn-share-screen').addClass('btn-danger').button('toggle');
|
|
peers.local.screenShared = true;
|
|
$.notify(localize('EVERYONE_CAN_SEE_YOUR_SCREEN'), 'info');
|
|
}
|
|
});
|
|
}
|
|
else{
|
|
peers.local.screenShared = false;
|
|
webrtc.stopScreenShare();
|
|
$('.btn-share-screen').removeClass('btn-danger').button('toggle');
|
|
$.notify(localize('SCREEN_UNSHARED'), 'info');
|
|
}
|
|
});
|
|
|
|
// Handle microphone mute/unmute
|
|
$('.btn-mute-mic').click(function() {
|
|
var action = ($(this).hasClass('btn-danger')) ? 'unmute' : 'mute';
|
|
if (action === 'mute'){
|
|
muteMic();
|
|
$.notify(localize('MIC_MUTED'), 'info');
|
|
}
|
|
else{
|
|
unmuteMic();
|
|
$.notify(localize('MIC_UNMUTED'), 'info');
|
|
}
|
|
$('.btn-mute-mic').toggleClass('btn-danger').button('toggle');
|
|
});
|
|
|
|
// Disable suspend webcam button if no webcam
|
|
if (!video){
|
|
$('.btn-suspend-cam').addClass('disabled').attr('disabled', true);
|
|
}
|
|
|
|
// Suspend the webcam
|
|
$('.btn-suspend-cam').click(function() {
|
|
var action = ($(this).hasClass('btn-danger')) ? 'resume' : 'pause';
|
|
if (action === 'pause'){
|
|
suspendCam();
|
|
$.notify(localize('CAM_SUSPENDED'), 'info');
|
|
}
|
|
else{
|
|
resumeCam();
|
|
$.notify(localize('CAM_RESUMED'), 'info');
|
|
}
|
|
$('.btn-suspend-cam').toggleClass('btn-danger').button('toggle');
|
|
});
|
|
|
|
// Handle auth to become room owner
|
|
$('#authPass').on('input', function() {
|
|
if ($('#authPass').val() === ''){
|
|
$('#authPassButton').addClass('disabled');
|
|
}
|
|
else{
|
|
$('#authPassButton').removeClass('disabled');
|
|
}
|
|
});
|
|
$('#ownerAuthForm').submit(function(event) {
|
|
event.preventDefault();
|
|
var pass = $('#ownerAuthPass').val();
|
|
vroomApi(
|
|
'authenticate',
|
|
{
|
|
password: pass,
|
|
room: roomName
|
|
},
|
|
function(data){
|
|
$('#authPass').val('');
|
|
$('#ownerAuthModal').modal('hide');
|
|
if (data.role === 'owner'){
|
|
getPeerRole(peers.local.id);
|
|
$('#joinPassFields,#ownerPassFields').hide();
|
|
$.notify(data.msg, 'success');
|
|
}
|
|
else{
|
|
$.notify(localize('WRONG_PASSWORD'), 'error');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
// The configuration form has been submited successfuly
|
|
// Lets announce it to the other peers
|
|
$('#configureRoomForm').on('room_conf_updated', function(){
|
|
webrtc.sendToAll('room_conf_updated');
|
|
});
|
|
|
|
// Choose another color. Useful if two peers have the same
|
|
$('#changeColorButton').click(function(){
|
|
peers.local.color = chooseColor();
|
|
// Reload etherpad iframe if required to sync the new color
|
|
if (etherpad.enabled){
|
|
loadEtherpadIframe();
|
|
}
|
|
webrtc.sendToAll('peer_color', {color: peers.local.color});
|
|
updateDisplayName('local');
|
|
});
|
|
|
|
// Download chat history in a file
|
|
$('#saveChat').click(function(){
|
|
var d = new Date;
|
|
downloadContent('VROOM Tchat (' + room + ') ' + d.toLocaleString() + '.html', 'text/html;charset=utf-8', $('#chatHistory').html());
|
|
});
|
|
|
|
// Suspend/Play MoH
|
|
$('.btn-moh').click(function(){
|
|
if ($(this).hasClass('btn-danger')){
|
|
$('#mohPlayer')[0].pause();
|
|
}
|
|
else{
|
|
$('#mohPlayer')[0].play();
|
|
}
|
|
$('.btn-moh').toggleClass('btn-danger').button('toggle');
|
|
});
|
|
|
|
// Handle the logout button: display a confirmation dialog
|
|
// and suspend mic and webcam
|
|
$('.btn-logout').click(function(){
|
|
$('#quitModal').modal('show');
|
|
if (!peers.local.micMuted){
|
|
muteMic();
|
|
}
|
|
if (!peers.local.videoPaused){
|
|
suspendCam();
|
|
}
|
|
});
|
|
// Remove the active class on the logout button if
|
|
// the modal is closed
|
|
$('#quitModal').on('hide.bs.modal',function(){
|
|
$('.btn-logout').removeClass('active');
|
|
// Unmute the mic only if it wasn't manually muted
|
|
if (!$('.btn-mute-mic:first').hasClass('btn-danger')){
|
|
unmuteMic();
|
|
}
|
|
// Same for the camera
|
|
if (!$('.btn-suspend-cam:first').hasClass('btn-danger')){
|
|
resumeCam();
|
|
}
|
|
});
|
|
$('#confirmQuitButton').click(function() {
|
|
hangupCall;
|
|
window.location.assign(rootUrl + 'goodbye/' + roomName);
|
|
});
|
|
window.onunload = window.onbeforeunload = hangupCall;
|
|
|
|
// Go fullscreen on double click of our own preview
|
|
$('#webRTCVideoLocal').dblclick(function(){
|
|
fullScreen(this);
|
|
});
|
|
// And put it in the main div on simple click
|
|
$('#webRTCVideoLocal').click(function(){
|
|
handlePreviewClick($(this), 'self');
|
|
});
|
|
|
|
// On click on the chat button, remove the red label
|
|
// and reset the unread msg counter
|
|
$('#chatDropdown').click(function (){
|
|
$('#chatDropdown').removeClass('btn-danger');
|
|
$('#unreadMsg').text('0').hide(1000);
|
|
});
|
|
// The input is a textarea, trigger a submit
|
|
// when the user hit enter, unless shift is pressed
|
|
$('#chatForm').keypress(function(e) {
|
|
if (e.which === 13 && !e.shiftKey){
|
|
// Do not add \n
|
|
e.preventDefault();
|
|
$(this).trigger('submit');
|
|
}
|
|
});
|
|
|
|
// Fix height in Firefox
|
|
if ($.browser.mozilla){
|
|
$('#saveChat,#sendChat').css('height', '47px');
|
|
}
|
|
// Adapt the chat input (textarea) size
|
|
$('#chatBox').on('input', function(){
|
|
var h = parseInt($(this).css('height'));
|
|
var mh = parseInt($(this).css('max-height'));
|
|
// Scrollbar, we need to add a row
|
|
if ($(this).prop('scrollHeight') > $(this).prop('clientHeight') && h < mh){
|
|
// Do a loop so that we can adapt to copy/pastes of several lines
|
|
// But do not add more than 10 rows at a time
|
|
for (var i = 0; $(this).prop('scrollHeight') > $(this).prop('clientHeight') && h < mh && i<10; i++){
|
|
$(this).prop('rows', $(this).prop('rows')+1);
|
|
}
|
|
}
|
|
// Check if we have empty lines in our textarea
|
|
// If we do, remove one row
|
|
// TODO: we should only check for the last row and don't remove a row if we have empty lines in the middle
|
|
else{
|
|
lines = $('#chatBox').val().split(/\r?\n/);
|
|
for(var i=0; i<lines.length && $(this).prop('rows')>1; i++) {
|
|
var val = lines[i].replace(/^\s+|\s+$/, '');
|
|
if (val.length == 0){
|
|
$(this).prop('rows', $(this).prop('rows')-1);
|
|
}
|
|
}
|
|
}
|
|
// In any case, adapt height of the buttons
|
|
$('#saveChat,#sendChat').css('height', $(this).css('height'));
|
|
});
|
|
|
|
// Chat: send to other peers
|
|
$('#chatForm').submit(function (e){
|
|
e.preventDefault();
|
|
if ($('#chatBox').val()){
|
|
webrtc.sendDirectlyToAll('vroom', 'text_chat', $('#chatBox').val());
|
|
// Local echo of our own message
|
|
newChatMessage('local',$('#chatBox').val());
|
|
// reset the input box
|
|
$('#chatBox').val('').prop('rows', 1);
|
|
$('#saveChat,#sendChat').css('height', $('#chatBox').css('height'));
|
|
}
|
|
});
|
|
|
|
|
|
if (etherpad.enabled){
|
|
$('.btn-etherpad').click(function(){
|
|
var action = ($(this).hasClass('btn-danger')) ? 'hide' : 'show';
|
|
if (action === 'show'){
|
|
// If not already loaded, load etherpad in the iFrame
|
|
if ($('#etherpadContainer').html() === ''){
|
|
loadEtherpadIframe();
|
|
}
|
|
$('#etherpadContainer').slideDown('200');
|
|
}
|
|
else{
|
|
$('#etherpadContainer').slideUp('200');
|
|
}
|
|
$('.btn-etherpad').toggleClass('btn-danger').button('toggle');
|
|
});
|
|
}
|
|
|
|
// Ping every peer each 5 sec to measure latency
|
|
// Do this through dataChannel to get a real idea of the latency
|
|
setInterval(function(){
|
|
$.each(peers, function(id){
|
|
// No response from last ping ? mark latency as poor
|
|
if (parseInt(peers[id].lastPong)+5000 > +new Date){
|
|
$('#' + id + '_video_incoming').removeClass('latencyUnknown latencyGood latencyMedium latencyWarn').addClass('latencyPoor');
|
|
}
|
|
});
|
|
webrtc.sendDirectlyToAll('vroom', 'ping', +new Date);
|
|
}, 5000);
|
|
|
|
window.onresize = function (){
|
|
$('#webRTCVideo').css('max-height', maxHeight());
|
|
$('#mainVideo>video').css('max-height', maxHeight());
|
|
$('#etherpadContainer').css('max-height', maxHeight());
|
|
};
|
|
// Preview heigh is limited to the windows heigh, minus the navbar, minus 25px
|
|
$('#webRTCVideo').css('max-height', maxHeight());
|
|
$('#etherpadContainer').css('max-height', maxHeight());
|
|
|
|
};
|
|
|