Got all the single-line comments back.

This commit is contained in:
Maxim Khlobystov 2016-01-15 00:46:41 -05:00
parent a844cb9356
commit cc6ffe15df
36 changed files with 1277 additions and 133 deletions

View File

@ -1,49 +1,56 @@
// Methods return a reference to itself to allow chaining
this.NotificationControl = (() => { this.NotificationControl = (() => {
let container, notifications; let container, notifications;
container = ''; container = ''; // holds where the alerts will go
notifications = {}; notifications = {};
class NotificationControl { class NotificationControl {
constructor(c) { constructor(c) {
container = c[0] === '#' ? c.substr(1) : c; container = c[0] === '#' ? c.substr(1) : c; // prepend '#' to the identifier
$("#whiteboard").prepend( $("#whiteboard").prepend( // create container for notifications
`<!-- Drawing area for notifications. Must have "data-alert" atrribute, I do not know why, typically only for actual notifications -->${"<div id=\"" + container + "\" data-alert></div>"}` `<!-- Drawing area for notifications. Must have "data-alert" atrribute, I do not know why, typically only for actual notifications -->${"<div id=\"" + container + "\" data-alert></div>"}`
); );
} }
// id: name of the notification
// type: optional style classes
// content: the notification's message
// nDuration: how many milliseconds the notification will stay (less than 1 implies permanent)
// nFadeTime: how many milliseconds it takes the notification to be removed
create(id, type, content, nDuration, nFadeTime) { create(id, type, content, nDuration, nFadeTime) {
let elementId; let elementId;
elementId = id[0] === '#' ? id.substr(1) : id; elementId = id[0] === '#' ? id.substr(1) : id; // remove prepended '#' from the identifier
notifications[elementId] = {}; notifications[elementId] = {};
notifications[elementId].element = ''; notifications[elementId].element = '';
notifications[elementId].element += `<div id="${elementId}" data-alert class='bbbNotification alert-box ${type}' tabindex='0' aria-live='assertive' role='dialogalert'>`; notifications[elementId].element += `<div id="${elementId}" data-alert class='bbbNotification alert-box ${type}' tabindex='0' aria-live='assertive' role='dialogalert'>`;
notifications[elementId].element += `${content}`; notifications[elementId].element += `${content}`;
notifications[elementId].element += '<button href="#" tabindex="0" class="close" aria-label="Close Alert">&times;</button>'; notifications[elementId].element += '<button href="#" tabindex="0" class="close" aria-label="Close Alert">&times;</button>';
notifications[elementId].element += '</div>'; notifications[elementId].element += '</div>';
notifications[elementId].duration = nDuration || -1; notifications[elementId].duration = nDuration || -1; // if no time is specified, it must be dismissed by the user
notifications[elementId].fadeTime = nFadeTime || 1000; notifications[elementId].fadeTime = nFadeTime || 1000;
return this; return this;
} }
registerShow(elementId, nShowNotification) { registerShow(elementId, nShowNotification) { // register the method to be called when showing the notification
notifications[elementId].showNotification = nShowNotification; notifications[elementId].showNotification = nShowNotification;
return this; return this;
} }
registerHide(elementId, nHideNotification) { registerHide(elementId, nHideNotification) { // register the method called when hiding the notification
notifications[elementId].hideNotification = nHideNotification; notifications[elementId].hideNotification = nHideNotification;
return this; return this;
} }
display(elementId) { display(elementId) { // called the registered methods
let base; let base;
$(`#${container}`).append(notifications[elementId].element); $(`#${container}`).append(notifications[elementId].element); // display the notification
if(typeof (base = notifications[elementId]).showNotification === "function") { if(typeof (base = notifications[elementId]).showNotification === "function") {
base.showNotification(); base.showNotification();
} }
setTimeout((_this => { setTimeout((_this => {
// remove the notification if the user selected to
return function() { return function() {
let base1; let base1;
if(notifications[elementId].duration > 0) { if(notifications[elementId].duration > 0) {
@ -52,12 +59,13 @@ this.NotificationControl = (() => {
if(typeof (base1 = notifications[elementId]).hideNotification === "function") { if(typeof (base1 = notifications[elementId]).hideNotification === "function") {
base1.hideNotification(); base1.hideNotification();
} }
return notifications[elementId] = {}; return notifications[elementId] = {}; // delete all notification data
}; };
})(this), notifications[elementId].duration); })(this), notifications[elementId].duration);
return this; return this;
} }
// hides a notification that may have been left over
hideANotification(elementId) { hideANotification(elementId) {
let base; let base;
$(`#${elementId}`).fadeOut(notifications[elementId].fadeTime, () => { $(`#${elementId}`).fadeOut(notifications[elementId].fadeTime, () => {
@ -70,24 +78,30 @@ this.NotificationControl = (() => {
} }
} }
// static icon members
NotificationControl.icons = { NotificationControl.icons = {
// RaphaelJS "settings" icon
'settings_IconPath': 'M17.41,20.395l-0.778-2.723c0.228-0.2,0.442-0.414,0.644-0.643l2.721,0.778c0.287-0.418,0.534-0.862,0.755-1.323l-2.025-1.96c0.097-0.288,0.181-0.581,0.241-0.883l2.729-0.684c0.02-0.252,0.039-0.505,0.039-0.763s-0.02-0.51-0.039-0.762l-2.729-0.684c-0.061-0.302-0.145-0.595-0.241-0.883l2.026-1.96c-0.222-0.46-0.469-0.905-0.756-1.323l-2.721,0.777c-0.201-0.228-0.416-0.442-0.644-0.643l0.778-2.722c-0.418-0.286-0.863-0.534-1.324-0.755l-1.96,2.026c-0.287-0.097-0.581-0.18-0.883-0.241l-0.683-2.73c-0.253-0.019-0.505-0.039-0.763-0.039s-0.51,0.02-0.762,0.039l-0.684,2.73c-0.302,0.061-0.595,0.144-0.883,0.241l-1.96-2.026C7.048,3.463,6.604,3.71,6.186,3.997l0.778,2.722C6.736,6.919,6.521,7.134,6.321,7.361L3.599,6.583C3.312,7.001,3.065,7.446,2.844,7.907l2.026,1.96c-0.096,0.288-0.18,0.581-0.241,0.883l-2.73,0.684c-0.019,0.252-0.039,0.505-0.039,0.762s0.02,0.51,0.039,0.763l2.73,0.684c0.061,0.302,0.145,0.595,0.241,0.883l-2.026,1.96c0.221,0.46,0.468,0.905,0.755,1.323l2.722-0.778c0.2,0.229,0.415,0.442,0.643,0.643l-0.778,2.723c0.418,0.286,0.863,0.533,1.323,0.755l1.96-2.026c0.288,0.097,0.581,0.181,0.883,0.241l0.684,2.729c0.252,0.02,0.505,0.039,0.763,0.039s0.51-0.02,0.763-0.039l0.683-2.729c0.302-0.061,0.596-0.145,0.883-0.241l1.96,2.026C16.547,20.928,16.992,20.681,17.41,20.395zM11.798,15.594c-1.877,0-3.399-1.522-3.399-3.399s1.522-3.398,3.399-3.398s3.398,1.521,3.398,3.398S13.675,15.594,11.798,15.594zM27.29,22.699c0.019-0.547-0.06-1.104-0.23-1.654l1.244-1.773c-0.188-0.35-0.4-0.682-0.641-0.984l-2.122,0.445c-0.428-0.364-0.915-0.648-1.436-0.851l-0.611-2.079c-0.386-0.068-0.777-0.105-1.173-0.106l-0.974,1.936c-0.279,0.054-0.558,0.128-0.832,0.233c-0.257,0.098-0.497,0.22-0.727,0.353L17.782,17.4c-0.297,0.262-0.568,0.545-0.813,0.852l0.907,1.968c-0.259,0.495-0.437,1.028-0.519,1.585l-1.891,1.06c0.019,0.388,0.076,0.776,0.164,1.165l2.104,0.519c0.231,0.524,0.541,0.993,0.916,1.393l-0.352,2.138c0.32,0.23,0.66,0.428,1.013,0.6l1.715-1.32c0.536,0.141,1.097,0.195,1.662,0.15l1.452,1.607c0.2-0.057,0.399-0.118,0.596-0.193c0.175-0.066,0.34-0.144,0.505-0.223l0.037-2.165c0.455-0.339,0.843-0.747,1.152-1.206l2.161-0.134c0.152-0.359,0.279-0.732,0.368-1.115L27.29,22.699zM23.127,24.706c-1.201,0.458-2.545-0.144-3.004-1.345s0.143-2.546,1.344-3.005c1.201-0.458,2.547,0.144,3.006,1.345C24.931,22.902,24.328,24.247,23.127,24.706z', 'settings_IconPath': 'M17.41,20.395l-0.778-2.723c0.228-0.2,0.442-0.414,0.644-0.643l2.721,0.778c0.287-0.418,0.534-0.862,0.755-1.323l-2.025-1.96c0.097-0.288,0.181-0.581,0.241-0.883l2.729-0.684c0.02-0.252,0.039-0.505,0.039-0.763s-0.02-0.51-0.039-0.762l-2.729-0.684c-0.061-0.302-0.145-0.595-0.241-0.883l2.026-1.96c-0.222-0.46-0.469-0.905-0.756-1.323l-2.721,0.777c-0.201-0.228-0.416-0.442-0.644-0.643l0.778-2.722c-0.418-0.286-0.863-0.534-1.324-0.755l-1.96,2.026c-0.287-0.097-0.581-0.18-0.883-0.241l-0.683-2.73c-0.253-0.019-0.505-0.039-0.763-0.039s-0.51,0.02-0.762,0.039l-0.684,2.73c-0.302,0.061-0.595,0.144-0.883,0.241l-1.96-2.026C7.048,3.463,6.604,3.71,6.186,3.997l0.778,2.722C6.736,6.919,6.521,7.134,6.321,7.361L3.599,6.583C3.312,7.001,3.065,7.446,2.844,7.907l2.026,1.96c-0.096,0.288-0.18,0.581-0.241,0.883l-2.73,0.684c-0.019,0.252-0.039,0.505-0.039,0.762s0.02,0.51,0.039,0.763l2.73,0.684c0.061,0.302,0.145,0.595,0.241,0.883l-2.026,1.96c0.221,0.46,0.468,0.905,0.755,1.323l2.722-0.778c0.2,0.229,0.415,0.442,0.643,0.643l-0.778,2.723c0.418,0.286,0.863,0.533,1.323,0.755l1.96-2.026c0.288,0.097,0.581,0.181,0.883,0.241l0.684,2.729c0.252,0.02,0.505,0.039,0.763,0.039s0.51-0.02,0.763-0.039l0.683-2.729c0.302-0.061,0.596-0.145,0.883-0.241l1.96,2.026C16.547,20.928,16.992,20.681,17.41,20.395zM11.798,15.594c-1.877,0-3.399-1.522-3.399-3.399s1.522-3.398,3.399-3.398s3.398,1.521,3.398,3.398S13.675,15.594,11.798,15.594zM27.29,22.699c0.019-0.547-0.06-1.104-0.23-1.654l1.244-1.773c-0.188-0.35-0.4-0.682-0.641-0.984l-2.122,0.445c-0.428-0.364-0.915-0.648-1.436-0.851l-0.611-2.079c-0.386-0.068-0.777-0.105-1.173-0.106l-0.974,1.936c-0.279,0.054-0.558,0.128-0.832,0.233c-0.257,0.098-0.497,0.22-0.727,0.353L17.782,17.4c-0.297,0.262-0.568,0.545-0.813,0.852l0.907,1.968c-0.259,0.495-0.437,1.028-0.519,1.585l-1.891,1.06c0.019,0.388,0.076,0.776,0.164,1.165l2.104,0.519c0.231,0.524,0.541,0.993,0.916,1.393l-0.352,2.138c0.32,0.23,0.66,0.428,1.013,0.6l1.715-1.32c0.536,0.141,1.097,0.195,1.662,0.15l1.452,1.607c0.2-0.057,0.399-0.118,0.596-0.193c0.175-0.066,0.34-0.144,0.505-0.223l0.037-2.165c0.455-0.339,0.843-0.747,1.152-1.206l2.161-0.134c0.152-0.359,0.279-0.732,0.368-1.115L27.29,22.699zM23.127,24.706c-1.201,0.458-2.545-0.144-3.004-1.345s0.143-2.546,1.344-3.005c1.201-0.458,2.547,0.144,3.006,1.345C24.931,22.902,24.328,24.247,23.127,24.706z',
// RaphaelJS "Safari" icon
'Safari_IconPath': 'M16.154,5.135c-0.504,0-1,0.031-1.488,0.089l-0.036-0.18c-0.021-0.104-0.06-0.198-0.112-0.283c0.381-0.308,0.625-0.778,0.625-1.306c0-0.927-0.751-1.678-1.678-1.678s-1.678,0.751-1.678,1.678c0,0.745,0.485,1.376,1.157,1.595c-0.021,0.105-0.021,0.216,0,0.328l0.033,0.167C7.645,6.95,3.712,11.804,3.712,17.578c0,6.871,5.571,12.441,12.442,12.441c6.871,0,12.441-5.57,12.441-12.441C28.596,10.706,23.025,5.135,16.154,5.135zM16.369,8.1c4.455,0,8.183,3.116,9.123,7.287l-0.576,0.234c-0.148-0.681-0.755-1.191-1.48-1.191c-0.837,0-1.516,0.679-1.516,1.516c0,0.075,0.008,0.148,0.018,0.221l-2.771-0.028c-0.054-0.115-0.114-0.226-0.182-0.333l3.399-5.11l0.055-0.083l-4.766,4.059c-0.352-0.157-0.74-0.248-1.148-0.256l0.086-0.018l-1.177-2.585c0.64-0.177,1.111-0.763,1.111-1.459c0-0.837-0.678-1.515-1.516-1.515c-0.075,0-0.147,0.007-0.219,0.018l0.058-0.634C15.357,8.141,15.858,8.1,16.369,8.1zM12.146,3.455c0-0.727,0.591-1.318,1.318-1.318c0.727,0,1.318,0.591,1.318,1.318c0,0.425-0.203,0.802-0.516,1.043c-0.183-0.123-0.413-0.176-0.647-0.13c-0.226,0.045-0.413,0.174-0.535,0.349C12.542,4.553,12.146,4.049,12.146,3.455zM7.017,17.452c0-4.443,3.098-8.163,7.252-9.116l0.297,0.573c-0.61,0.196-1.051,0.768-1.051,1.442c0,0.837,0.678,1.516,1.515,1.516c0.068,0,0.135-0.006,0.2-0.015l-0.058,2.845l0.052-0.011c-0.442,0.204-0.824,0.513-1.116,0.895l0.093-0.147l-1.574-0.603l1.172,1.239l0.026-0.042c-0.19,0.371-0.306,0.788-0.324,1.229l-0.003-0.016l-2.623,1.209c-0.199-0.604-0.767-1.041-1.438-1.041c-0.837,0-1.516,0.678-1.516,1.516c0,0.064,0.005,0.128,0.013,0.191l-0.783-0.076C7.063,18.524,7.017,17.994,7.017,17.452zM16.369,26.805c-4.429,0-8.138-3.078-9.106-7.211l0.691-0.353c0.146,0.686,0.753,1.2,1.482,1.2c0.837,0,1.515-0.679,1.515-1.516c0-0.105-0.011-0.207-0.031-0.307l2.858,0.03c0.045,0.095,0.096,0.187,0.15,0.276l-3.45,5.277l0.227-0.195l4.529-3.92c0.336,0.153,0.705,0.248,1.094,0.266l-0.019,0.004l1.226,2.627c-0.655,0.166-1.142,0.76-1.142,1.468c0,0.837,0.678,1.515,1.516,1.515c0.076,0,0.151-0.007,0.225-0.018l0.004,0.688C17.566,26.746,16.975,26.805,16.369,26.805zM18.662,26.521l-0.389-0.6c0.661-0.164,1.152-0.759,1.152-1.47c0-0.837-0.68-1.516-1.516-1.516c-0.066,0-0.13,0.005-0.193,0.014v-2.86l-0.025,0.004c0.409-0.185,0.77-0.459,1.055-0.798l1.516,0.659l-1.104-1.304c0.158-0.335,0.256-0.704,0.278-1.095l2.552-1.164c0.19,0.618,0.766,1.068,1.447,1.068c0.838,0,1.516-0.679,1.516-1.516c0-0.069-0.006-0.137-0.016-0.204l0.65,0.12c0.089,0.517,0.136,1.049,0.136,1.591C25.722,21.826,22.719,25.499,18.662,26.521z', 'Safari_IconPath': 'M16.154,5.135c-0.504,0-1,0.031-1.488,0.089l-0.036-0.18c-0.021-0.104-0.06-0.198-0.112-0.283c0.381-0.308,0.625-0.778,0.625-1.306c0-0.927-0.751-1.678-1.678-1.678s-1.678,0.751-1.678,1.678c0,0.745,0.485,1.376,1.157,1.595c-0.021,0.105-0.021,0.216,0,0.328l0.033,0.167C7.645,6.95,3.712,11.804,3.712,17.578c0,6.871,5.571,12.441,12.442,12.441c6.871,0,12.441-5.57,12.441-12.441C28.596,10.706,23.025,5.135,16.154,5.135zM16.369,8.1c4.455,0,8.183,3.116,9.123,7.287l-0.576,0.234c-0.148-0.681-0.755-1.191-1.48-1.191c-0.837,0-1.516,0.679-1.516,1.516c0,0.075,0.008,0.148,0.018,0.221l-2.771-0.028c-0.054-0.115-0.114-0.226-0.182-0.333l3.399-5.11l0.055-0.083l-4.766,4.059c-0.352-0.157-0.74-0.248-1.148-0.256l0.086-0.018l-1.177-2.585c0.64-0.177,1.111-0.763,1.111-1.459c0-0.837-0.678-1.515-1.516-1.515c-0.075,0-0.147,0.007-0.219,0.018l0.058-0.634C15.357,8.141,15.858,8.1,16.369,8.1zM12.146,3.455c0-0.727,0.591-1.318,1.318-1.318c0.727,0,1.318,0.591,1.318,1.318c0,0.425-0.203,0.802-0.516,1.043c-0.183-0.123-0.413-0.176-0.647-0.13c-0.226,0.045-0.413,0.174-0.535,0.349C12.542,4.553,12.146,4.049,12.146,3.455zM7.017,17.452c0-4.443,3.098-8.163,7.252-9.116l0.297,0.573c-0.61,0.196-1.051,0.768-1.051,1.442c0,0.837,0.678,1.516,1.515,1.516c0.068,0,0.135-0.006,0.2-0.015l-0.058,2.845l0.052-0.011c-0.442,0.204-0.824,0.513-1.116,0.895l0.093-0.147l-1.574-0.603l1.172,1.239l0.026-0.042c-0.19,0.371-0.306,0.788-0.324,1.229l-0.003-0.016l-2.623,1.209c-0.199-0.604-0.767-1.041-1.438-1.041c-0.837,0-1.516,0.678-1.516,1.516c0,0.064,0.005,0.128,0.013,0.191l-0.783-0.076C7.063,18.524,7.017,17.994,7.017,17.452zM16.369,26.805c-4.429,0-8.138-3.078-9.106-7.211l0.691-0.353c0.146,0.686,0.753,1.2,1.482,1.2c0.837,0,1.515-0.679,1.515-1.516c0-0.105-0.011-0.207-0.031-0.307l2.858,0.03c0.045,0.095,0.096,0.187,0.15,0.276l-3.45,5.277l0.227-0.195l4.529-3.92c0.336,0.153,0.705,0.248,1.094,0.266l-0.019,0.004l1.226,2.627c-0.655,0.166-1.142,0.76-1.142,1.468c0,0.837,0.678,1.515,1.516,1.515c0.076,0,0.151-0.007,0.225-0.018l0.004,0.688C17.566,26.746,16.975,26.805,16.369,26.805zM18.662,26.521l-0.389-0.6c0.661-0.164,1.152-0.759,1.152-1.47c0-0.837-0.68-1.516-1.516-1.516c-0.066,0-0.13,0.005-0.193,0.014v-2.86l-0.025,0.004c0.409-0.185,0.77-0.459,1.055-0.798l1.516,0.659l-1.104-1.304c0.158-0.335,0.256-0.704,0.278-1.095l2.552-1.164c0.19,0.618,0.766,1.068,1.447,1.068c0.838,0,1.516-0.679,1.516-1.516c0-0.069-0.006-0.137-0.016-0.204l0.65,0.12c0.089,0.517,0.136,1.049,0.136,1.591C25.722,21.826,22.719,25.499,18.662,26.521z',
// RaphaelJS "Internet Explorer" icon
'IE_IconPath': 'M27.998,2.266c-2.12-1.91-6.925,0.382-9.575,1.93c-0.76-0.12-1.557-0.185-2.388-0.185c-3.349,0-6.052,0.985-8.106,2.843c-2.336,2.139-3.631,4.94-3.631,8.177c0,0.028,0.001,0.056,0.001,0.084c3.287-5.15,8.342-7.79,9.682-8.487c0.212-0.099,0.338,0.155,0.141,0.253c-0.015,0.042-0.015,0,0,0c-2.254,1.35-6.434,5.259-9.146,10.886l-0.003-0.007c-1.717,3.547-3.167,8.529-0.267,10.358c2.197,1.382,6.13-0.248,9.295-2.318c0.764,0.108,1.567,0.165,2.415,0.165c5.84,0,9.937-3.223,11.399-7.924l-8.022-0.014c-0.337,1.661-1.464,2.548-3.223,2.548c-2.21,0-3.729-1.211-3.828-4.012l15.228-0.014c0.028-0.578-0.042-0.985-0.042-1.436c0-5.251-3.143-9.355-8.255-10.663c2.081-1.294,5.974-3.209,7.848-1.681c1.407,1.14,0.633,3.533,0.295,4.518c-0.056,0.254,0.24,0.296,0.296,0.057C28.814,5.573,29.026,3.194,27.998,2.266zM13.272,25.676c-2.469,1.475-5.873,2.539-7.539,1.289c-1.243-0.935-0.696-3.468,0.398-5.938c0.664,0.992,1.495,1.886,2.473,2.63C9.926,24.651,11.479,25.324,13.272,25.676zM12.714,13.046c0.042-2.435,1.787-3.49,3.617-3.49c1.928,0,3.49,1.112,3.49,3.49H12.714z' 'IE_IconPath': 'M27.998,2.266c-2.12-1.91-6.925,0.382-9.575,1.93c-0.76-0.12-1.557-0.185-2.388-0.185c-3.349,0-6.052,0.985-8.106,2.843c-2.336,2.139-3.631,4.94-3.631,8.177c0,0.028,0.001,0.056,0.001,0.084c3.287-5.15,8.342-7.79,9.682-8.487c0.212-0.099,0.338,0.155,0.141,0.253c-0.015,0.042-0.015,0,0,0c-2.254,1.35-6.434,5.259-9.146,10.886l-0.003-0.007c-1.717,3.547-3.167,8.529-0.267,10.358c2.197,1.382,6.13-0.248,9.295-2.318c0.764,0.108,1.567,0.165,2.415,0.165c5.84,0,9.937-3.223,11.399-7.924l-8.022-0.014c-0.337,1.661-1.464,2.548-3.223,2.548c-2.21,0-3.729-1.211-3.828-4.012l15.228-0.014c0.028-0.578-0.042-0.985-0.042-1.436c0-5.251-3.143-9.355-8.255-10.663c2.081-1.294,5.974-3.209,7.848-1.681c1.407,1.14,0.633,3.533,0.295,4.518c-0.056,0.254,0.24,0.296,0.296,0.057C28.814,5.573,29.026,3.194,27.998,2.266zM13.272,25.676c-2.469,1.475-5.873,2.539-7.539,1.289c-1.243-0.935-0.696-3.468,0.398-5.938c0.664,0.992,1.495,1.886,2.473,2.63C9.926,24.651,11.479,25.324,13.272,25.676zM12.714,13.046c0.042-2.435,1.787-3.49,3.617-3.49c1.928,0,3.49,1.112,3.49,3.49H12.714z'
}; };
return NotificationControl; return NotificationControl;
})(); })();
this.notification_WebRTCAudioExited = function() { this.notification_WebRTCAudioExited = function() { // used when the user can join audio
return Meteor.NotificationControl.create("webRTC_AudioExited", ' ', 'You have exited audio', 2500).display("webRTC_AudioExited"); return Meteor.NotificationControl.create("webRTC_AudioExited", ' ', 'You have exited audio', 2500).display("webRTC_AudioExited");
}; };
this.notification_WebRTCAudioJoining = function() { this.notification_WebRTCAudioJoining = function() { // used when the user can join audio
// display joining notification
Meteor.NotificationControl.create("webRTC_AudioJoining", '', 'Connecting to the audio call...', -1).registerShow("webRTC_AudioJoining", () => {}).display("webRTC_AudioJoining"); Meteor.NotificationControl.create("webRTC_AudioJoining", '', 'Connecting to the audio call...', -1).registerShow("webRTC_AudioJoining", () => {}).display("webRTC_AudioJoining");
return Tracker.autorun(comp => { // joined. Displayed joined notification and hide the joining notification
if(BBB.amIInAudio()) { return Tracker.autorun(comp => { // wait until user is in
comp.stop(); if(BBB.amIInAudio()) { // display notification when you are in audio
comp.stop(); // prevents computation from running twice (which can happen occassionally)
return Meteor.NotificationControl.create("webRTC_AudioJoined", 'success ', '', 2500).registerShow("webRTC_AudioJoined", () => { return Meteor.NotificationControl.create("webRTC_AudioJoined", 'success ', '', 2500).registerShow("webRTC_AudioJoined", () => {
Meteor.NotificationControl.hideANotification('webRTC_AudioJoining'); Meteor.NotificationControl.hideANotification('webRTC_AudioJoining');
return $("#webRTC_AudioJoined").prepend(`You've joined the ${BBB.amIListenOnlyAudio() ? 'Listen Only' : ''} audio`); return $("#webRTC_AudioJoined").prepend(`You've joined the ${BBB.amIListenOnlyAudio() ? 'Listen Only' : ''} audio`);
@ -96,10 +110,11 @@ this.notification_WebRTCAudioJoining = function() {
}); });
}; };
this.notification_WebRTCNotSupported = function() { this.notification_WebRTCNotSupported = function() { // shown when the user's browser does not support WebRTC
// create a new notification at the audio button they clicked to trigger the event
return Meteor.NotificationControl.create("webRTC_NotSupported", 'alert', '', -1).registerShow("webRTC_NotSupported", () => { return Meteor.NotificationControl.create("webRTC_NotSupported", 'alert', '', -1).registerShow("webRTC_NotSupported", () => {
let browserName, ref; let browserName, ref;
if(((ref = (browserName = getBrowserName())) === 'Safari' || ref === 'IE') || (browserName = "settings")) { if(((ref = (browserName = getBrowserName())) === 'Safari' || ref === 'IE') || (browserName = "settings")) { // show either the browser icon or cog gears
$("#webRTC_NotSupported").prepend( $("#webRTC_NotSupported").prepend(
`<div id="browser-icon-container"></div>${"Sorry,<br/>" + (browserName !== 'settings' ? browserName : 'your browser') + " doesn't support WebRTC"}` `<div id="browser-icon-container"></div>${"Sorry,<br/>" + (browserName !== 'settings' ? browserName : 'your browser') + " doesn't support WebRTC"}`
); );

View File

@ -14,6 +14,7 @@ this.getBuildInformation = function() {
}; };
}; };
// Convert a color `value` as integer to a hex color (e.g. 255 to #0000ff)
this.colourToHex = function(value) { this.colourToHex = function(value) {
let hex; let hex;
hex = parseInt(value).toString(16); hex = parseInt(value).toString(16);
@ -23,9 +24,10 @@ this.colourToHex = function(value) {
return `#${hex}`; return `#${hex}`;
}; };
// color can be a number (a hex converted to int) or a string (e.g. "#ffff00")
this.formatColor = function(color) { this.formatColor = function(color) {
if(color == null) { if(color == null) {
color = "0"; color = "0"; // default value
} }
if(!color.toString().match(/\#.*/)) { if(!color.toString().match(/\#.*/)) {
color = colourToHex(color); color = colourToHex(color);
@ -37,22 +39,27 @@ this.getInSession = function(k) {
return SessionAmplify.get(k); return SessionAmplify.get(k);
}; };
// returns epoch in ms
this.getTime = function() { this.getTime = function() {
return (new Date).valueOf(); return (new Date).valueOf();
}; };
// checks if the pan gesture is mostly horizontal
this.isPanHorizontal = function(event) { this.isPanHorizontal = function(event) {
return Math.abs(event.deltaX) > Math.abs(event.deltaY); return Math.abs(event.deltaX) > Math.abs(event.deltaY);
}; };
// helper to determine whether user has joined any type of audio
Handlebars.registerHelper("amIInAudio", () => { Handlebars.registerHelper("amIInAudio", () => {
return BBB.amIInAudio(); return BBB.amIInAudio();
}); });
// helper to determine whether the user is in the listen only audio stream
Handlebars.registerHelper("amIListenOnlyAudio", () => { Handlebars.registerHelper("amIListenOnlyAudio", () => {
return BBB.amIListenOnlyAudio(); return BBB.amIListenOnlyAudio();
}); });
// helper to determine whether the user is in the listen only audio stream
Handlebars.registerHelper("isMyMicLocked", () => { Handlebars.registerHelper("isMyMicLocked", () => {
return BBB.isMyMicLocked(); return BBB.isMyMicLocked();
}); });
@ -63,7 +70,7 @@ Handlebars.registerHelper("colourToHex", (_this => {
}; };
})(this)); })(this));
Handlebars.registerHelper('equals', (a, b) => { Handlebars.registerHelper('equals', (a, b) => { // equals operator was dropped in Meteor's migration from Handlebars to Spacebars
return a === b; return a === b;
}); });
@ -74,9 +81,11 @@ Handlebars.registerHelper("getCurrentMeeting", () => {
Handlebars.registerHelper("getCurrentSlide", () => { Handlebars.registerHelper("getCurrentSlide", () => {
let result; let result;
result = BBB.getCurrentSlide("helper getCurrentSlide"); result = BBB.getCurrentSlide("helper getCurrentSlide");
// console.log "result=#{JSON.stringify result}"
return result; return result;
}); });
// Allow access through all templates
Handlebars.registerHelper("getInSession", k => { Handlebars.registerHelper("getInSession", k => {
return SessionAmplify.get(k); return SessionAmplify.get(k);
}); });
@ -88,11 +97,14 @@ Handlebars.registerHelper("getMeetingName", () => {
Handlebars.registerHelper("getShapesForSlide", () => { Handlebars.registerHelper("getShapesForSlide", () => {
let currentSlide, ref; let currentSlide, ref;
currentSlide = BBB.getCurrentSlide("helper getShapesForSlide"); currentSlide = BBB.getCurrentSlide("helper getShapesForSlide");
// try to reuse the lines above
return Meteor.Shapes.find({ return Meteor.Shapes.find({
whiteboardId: currentSlide != null ? (ref = currentSlide.slide) != null ? ref.id : void 0 : void 0 whiteboardId: currentSlide != null ? (ref = currentSlide.slide) != null ? ref.id : void 0 : void 0
}); });
}); });
// retrieves all users in the meeting
Handlebars.registerHelper("getUsersInMeeting", () => { Handlebars.registerHelper("getUsersInMeeting", () => {
let users; let users;
users = Meteor.Users.find().fetch(); users = Meteor.Users.find().fetch();
@ -121,6 +133,7 @@ Handlebars.registerHelper("isCurrentUserMuted", () => {
return BBB.amIMuted(); return BBB.amIMuted();
}); });
//Retreives a username for a private chat tab from the database if it exists
Handlebars.registerHelper("privateChatName", () => { Handlebars.registerHelper("privateChatName", () => {
let obj, ref; let obj, ref;
obj = Meteor.Users.findOne({ obj = Meteor.Users.findOne({
@ -249,6 +262,7 @@ Handlebars.registerHelper('containerPosition', section => {
} }
}); });
// vertically shrinks the whiteboard if the slide navigation controllers are present
Handlebars.registerHelper('whiteboardSize', section => { Handlebars.registerHelper('whiteboardSize', section => {
if(BBB.isUserPresenter(getInSession('userId'))) { if(BBB.isUserPresenter(getInSession('userId'))) {
return 'presenter-whiteboard'; return 'presenter-whiteboard';
@ -319,6 +333,11 @@ this.getSortedUserList = function(users) {
} else if(!b.user.phone_user) { } else if(!b.user.phone_user) {
return 1; return 1;
} }
//Check name (case-insensitive) in the event of a tie up above. If the name
//is the same then use userID which should be unique making the order the same
//across all clients.
if(a.user._sort_name < b.user._sort_name) { if(a.user._sort_name < b.user._sort_name) {
return -1; return -1;
} else if(a.user._sort_name > b.user._sort_name) { } else if(a.user._sort_name > b.user._sort_name) {
@ -333,6 +352,7 @@ this.getSortedUserList = function(users) {
return users; return users;
}; };
// transform plain text links into HTML tags compatible with Flash client
this.linkify = function(str) { this.linkify = function(str) {
return str = str.replace(re_weburl, "<a href='event:$&'><u>$&</u></a>"); return str = str.replace(re_weburl, "<a href='event:$&'><u>$&</u></a>");
}; };
@ -347,7 +367,10 @@ this.safeString = function(str) {
} }
}; };
this.toggleCam = function(event) {}; this.toggleCam = function(event) {
// Meteor.Users.update {_id: context._id} , {$set:{"user.sharingVideo": !context.sharingVideo}}
// Meteor.call('userToggleCam', context._id, !context.sharingVideo)
};
this.toggleChatbar = function() { this.toggleChatbar = function() {
setInSession("display_chatbar", !getInSession("display_chatbar")); setInSession("display_chatbar", !getInSession("display_chatbar"));
@ -378,6 +401,8 @@ this.populateNotifications = function(msg) {
let chat, chats, initChats, j, l, len, len1, myPrivateChats, myUserId, new_msg_userid, results, u, uniqueArray, users; let chat, chats, initChats, j, l, len, len1, myPrivateChats, myUserId, new_msg_userid, results, u, uniqueArray, users;
myUserId = getInSession("userId"); myUserId = getInSession("userId");
users = Meteor.Users.find().fetch(); users = Meteor.Users.find().fetch();
// assuming that I only have access only to private messages where I am the sender or the recipient
myPrivateChats = Meteor.Chat.find({ myPrivateChats = Meteor.Chat.find({
'message.chat_type': 'PRIVATE_CHAT' 'message.chat_type': 'PRIVATE_CHAT'
}).fetch(); }).fetch();
@ -397,6 +422,8 @@ this.populateNotifications = function(msg) {
}); });
} }
} }
//keep unique entries only
uniqueArray = uniqueArray.filter((itm, i, a) => { uniqueArray = uniqueArray.filter((itm, i, a) => {
return i === a.indexOf(itm); return i === a.indexOf(itm);
}); });
@ -418,6 +445,7 @@ this.populateNotifications = function(msg) {
setInSession('chats', initChats); setInSession('chats', initChats);
} }
results = []; results = [];
//insert the unique entries in the collection
for(l = 0, len1 = uniqueArray.length; l < len1; l++) { for(l = 0, len1 = uniqueArray.length; l < len1; l++) {
u = uniqueArray[l]; u = uniqueArray[l];
chats = getInSession('chats'); chats = getInSession('chats');
@ -510,13 +538,23 @@ this.closeMenus = function() {
} }
}; };
// Periodically check the status of the WebRTC call, when a call has been established attempt to hangup,
// retry if a call is in progress, send the leave voice conference message to BBB
this.exitVoiceCall = function(event, afterExitCall) { this.exitVoiceCall = function(event, afterExitCall) {
let checkToHangupCall, hangupCallback; let checkToHangupCall, hangupCallback;
// To be called when the hangup is initiated
hangupCallback = function() { hangupCallback = function() {
return console.log("Exiting Voice Conference"); return console.log("Exiting Voice Conference");
}; };
// Checks periodically until a call is established so we can successfully end the call
// clean state
getInSession("triedHangup", false); getInSession("triedHangup", false);
// function to initiate call
(checkToHangupCall = function(context) { (checkToHangupCall = function(context) {
// if an attempt to hang up the call is made when the current session is not yet finished, the request has no effect
// keep track in the session if we haven't tried a hangup
if(BBB.getCallStatus() !== null && !getInSession("triedHangup")) { if(BBB.getCallStatus() !== null && !getInSession("triedHangup")) {
console.log("Attempting to hangup on WebRTC call"); console.log("Attempting to hangup on WebRTC call");
if(BBB.amIListenOnlyAudio()) { if(BBB.amIListenOnlyAudio()) {
@ -540,10 +578,11 @@ this.exitVoiceCall = function(event, afterExitCall) {
); );
return setTimeout(checkToHangupCall, Meteor.config.app.WebRTCHangupRetryInterval); return setTimeout(checkToHangupCall, Meteor.config.app.WebRTCHangupRetryInterval);
} }
})(this); })(this); // automatically run function
return false; return false;
}; };
// close the daudio UI, then join the conference. If listen only send the request to the server
this.joinVoiceCall = function(event, arg) { this.joinVoiceCall = function(event, arg) {
let isListenOnly, joinCallback; let isListenOnly, joinCallback;
isListenOnly = (arg != null ? arg : {}).isListenOnly; isListenOnly = (arg != null ? arg : {}).isListenOnly;
@ -554,6 +593,8 @@ this.joinVoiceCall = function(event, arg) {
if(isListenOnly == null) { if(isListenOnly == null) {
isListenOnly = true; isListenOnly = true;
} }
// create voice call params
joinCallback = function(message) { joinCallback = function(message) {
return console.log("Beginning WebRTC Conference Call"); return console.log("Beginning WebRTC Conference Call");
}; };
@ -567,14 +608,17 @@ this.joinVoiceCall = function(event, arg) {
true true
); );
} }
BBB.joinVoiceConference(joinCallback, isListenOnly); BBB.joinVoiceConference(joinCallback, isListenOnly); // make the call //TODO should we apply role permissions to this action?
return false; return false;
}; };
// Starts the entire logout procedure.
// meeting: the meeting the user is in
// the user's userId
this.userLogout = function(meeting, user) { this.userLogout = function(meeting, user) {
Meteor.call("userLogout", meeting, user, getInSession("authToken")); Meteor.call("userLogout", meeting, user, getInSession("authToken"));
console.log("logging out"); console.log("logging out");
return clearSessionVar(document.location = getInSession('logoutURL')); return clearSessionVar(document.location = getInSession('logoutURL')); // navigate to logout
}; };
this.kickUser = function(meetingId, toKickUserId, requesterUserId, authToken) { this.kickUser = function(meetingId, toKickUserId, requesterUserId, authToken) {
@ -590,9 +634,10 @@ this.setUserPresenter = function(
return Meteor.call("setUserPresenter", meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken); return Meteor.call("setUserPresenter", meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken);
}; };
// Clear the local user session
this.clearSessionVar = function(callback) { this.clearSessionVar = function(callback) {
amplify.store('authToken', null); amplify.store('authToken', null);
amplify.store('bbbServerVersion', null); amplify.store('bbbServerVer1sion', null);
amplify.store('chats', null); amplify.store('chats', null);
amplify.store('dateOfBuild', null); amplify.store('dateOfBuild', null);
amplify.store('display_chatPane', null); amplify.store('display_chatPane', null);
@ -612,15 +657,18 @@ this.clearSessionVar = function(callback) {
} }
}; };
// assign the default values for the Session vars
this.setDefaultSettings = function() { this.setDefaultSettings = function() {
let initChats; let initChats;
setInSession("display_navbar", true); setInSession("display_navbar", true);
setInSession("display_chatbar", true); setInSession("display_chatbar", true);
setInSession("display_whiteboard", true); setInSession("display_whiteboard", true);
setInSession("display_chatPane", true); setInSession("display_chatPane", true);
//if it is a desktop version of the client
if(isPortraitMobile() || isLandscapeMobile()) { if(isPortraitMobile() || isLandscapeMobile()) {
setInSession("messageFontSize", Meteor.config.app.mobileFont); setInSession("messageFontSize", Meteor.config.app.mobileFont);
} else { } else { //if this is a mobile version of the client
setInSession("messageFontSize", Meteor.config.app.desktopFont); setInSession("messageFontSize", Meteor.config.app.desktopFont);
} }
setInSession('display_slidingMenu', false); setInSession('display_slidingMenu', false);
@ -632,6 +680,9 @@ this.setDefaultSettings = function() {
} }
setInSession('display_menu', false); setInSession('display_menu', false);
setInSession('chatInputMinHeight', 0); setInSession('chatInputMinHeight', 0);
//keep notifications and an opened private chat tab if page was refreshed
//reset to default if that's a new user
if(loginOrRefresh()) { if(loginOrRefresh()) {
initChats = [ initChats = [
{ {
@ -643,9 +694,10 @@ this.setDefaultSettings = function() {
setInSession('chats', initChats); setInSession('chats', initChats);
setInSession("inChatWith", 'PUBLIC_CHAT'); setInSession("inChatWith", 'PUBLIC_CHAT');
} }
return TimeSync.loggingEnabled = false; return TimeSync.loggingEnabled = false; // suppresses the log messages from timesync
}; };
//true if it is a new user, false if the client was just refreshed
this.loginOrRefresh = function() { this.loginOrRefresh = function() {
let checkId, userId; let checkId, userId;
userId = getInSession('userId'); userId = getInSession('userId');
@ -681,50 +733,82 @@ this.onLoadComplete = function() {
}); });
}; };
// Detects a mobile device
this.isMobile = function() { this.isMobile = function() {
return navigator.userAgent.match(/Android/i) || navigator.userAgent.match(/iPhone|iPad|iPod/i) || navigator.userAgent.match(/BlackBerry/i) || navigator.userAgent.match(/Windows Phone/i) || navigator.userAgent.match(/IEMobile/i) || navigator.userAgent.match(/BlackBerry/i) || navigator.userAgent.match(/webOS/i); return navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/iPhone|iPad|iPod/i) ||
navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/Windows Phone/i) ||
navigator.userAgent.match(/IEMobile/i) ||
navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/webOS/i);
}; };
this.isLandscape = function() { this.isLandscape = function() {
return !isMobile() && window.matchMedia('(orientation: landscape)').matches && window.matchMedia('(min-device-aspect-ratio: 1/1)').matches; return !isMobile() &&
window.matchMedia('(orientation: landscape)').matches && // browser is landscape
window.matchMedia('(min-device-aspect-ratio: 1/1)').matches; // device is landscape
}; };
this.isPortrait = function() { this.isPortrait = function() {
return !isMobile() && window.matchMedia('(orientation: portrait)').matches && window.matchMedia('(min-device-aspect-ratio: 1/1)').matches; return !isMobile() &&
window.matchMedia('(orientation: portrait)').matches && // browser is portrait
window.matchMedia('(min-device-aspect-ratio: 1/1)').matches; // device is landscape
}; };
// Checks if the view is portrait and a mobile device is being used
this.isPortraitMobile = function() { this.isPortraitMobile = function() {
return isMobile() && window.matchMedia('(orientation: portrait)').matches && window.matchMedia('(max-device-aspect-ratio: 1/1)').matches; return isMobile() &&
window.matchMedia('(orientation: portrait)').matches && // browser is portrait
window.matchMedia('(max-device-aspect-ratio: 1/1)').matches; // device is portrait
}; };
// Checks if the view is landscape and mobile device is being used
this.isLandscapeMobile = function() { this.isLandscapeMobile = function() {
return isMobile() && window.matchMedia('(orientation: landscape)').matches && window.matchMedia('(min-device-aspect-ratio: 1/1)').matches; return isMobile() &&
window.matchMedia('(orientation: landscape)').matches && // browser is landscape
window.matchMedia('(min-device-aspect-ratio: 1/1)').matches; // device is landscape
}; };
this.isLandscapePhone = function() { this.isLandscapePhone = function() {
return window.matchMedia('(orientation: landscape)').matches && window.matchMedia('(min-device-aspect-ratio: 1/1)').matches && window.matchMedia('(max-device-width: 959px)').matches; // @phone-landscape media query:
return window.matchMedia('(orientation: landscape)').matches &&
window.matchMedia('(min-device-aspect-ratio: 1/1)').matches &&
window.matchMedia('(max-device-width: 959px)').matches;
}; };
this.isPortraitPhone = function() { this.isPortraitPhone = function() {
return (window.matchMedia('(orientation: portrait)').matches && window.matchMedia('(max-device-aspect-ratio: 1/1)').matches && window.matchMedia('(max-device-width: 480px)').matches) || (window.matchMedia('(orientation: landscape)').matches && window.matchMedia('(max-device-aspect-ratio: 1/1)').matches && window.matchMedia('(max-device-width: 480px)').matches); // @phone-portrait media query:
return (window.matchMedia('(orientation: portrait)').matches &&
window.matchMedia('(max-device-aspect-ratio: 1/1)').matches &&
window.matchMedia('(max-device-width: 480px)').matches) ||
// @phone-portrait-with-keyboard media query:
(window.matchMedia('(orientation: landscape)').matches &&
window.matchMedia('(max-device-aspect-ratio: 1/1)').matches &&
window.matchMedia('(max-device-width: 480px)').matches);
}; };
this.isPhone = function() { this.isPhone = function() {
return isLandscapePhone() || isPortraitPhone(); return isLandscapePhone() || isPortraitPhone();
}; };
// The webpage orientation is now landscape
this.orientationBecameLandscape = function() { this.orientationBecameLandscape = function() {
return adjustChatInputHeight(); return adjustChatInputHeight();
}; };
// The webpage orientation is now portrait
this.orientationBecamePortrait = function() { this.orientationBecamePortrait = function() {
return adjustChatInputHeight(); return adjustChatInputHeight();
}; };
// Checks if only one panel (userlist/whiteboard/chatbar) is currently open
this.isOnlyOnePanelOpen = function() { this.isOnlyOnePanelOpen = function() {
//return (getInSession("display_usersList") ? 1 : 0) + (getInSession("display_whiteboard") ? 1 : 0) + (getInSession("display_chatbar") ? 1 : 0) === 1
return getInSession("display_usersList") + getInSession("display_whiteboard") + getInSession("display_chatbar") === 1; return getInSession("display_usersList") + getInSession("display_whiteboard") + getInSession("display_chatbar") === 1;
}; };
// determines which browser is being used
this.getBrowserName = function() { this.getBrowserName = function() {
if(navigator.userAgent.match(/Chrome/i)) { if(navigator.userAgent.match(/Chrome/i)) {
return 'Chrome'; return 'Chrome';
@ -744,16 +828,22 @@ this.scrollChatDown = function() {
return $('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0); return $('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
}; };
// changes the height of the chat input area if needed (based on the textarea content)
this.adjustChatInputHeight = function() { this.adjustChatInputHeight = function() {
let projectedHeight, ref; let projectedHeight, ref;
if(isLandscape()) { if(isLandscape()) {
$('#newMessageInput').css('height', 'auto'); $('#newMessageInput').css('height', 'auto');
projectedHeight = $('#newMessageInput')[0].scrollHeight + 23; projectedHeight = $('#newMessageInput')[0].scrollHeight + 23;
if(projectedHeight !== $('.panel-footer').height() && projectedHeight >= getInSession('chatInputMinHeight')) { if(projectedHeight !== $('.panel-footer').height() && projectedHeight >= getInSession('chatInputMinHeight')) {
$('#newMessageInput').css('overflow', 'hidden'); $('#newMessageInput').css('overflow', 'hidden'); // prevents a scroll bar
// resizes the chat input area
$('.panel-footer').css('top', `${-(projectedHeight - 70)}px`); $('.panel-footer').css('top', `${-(projectedHeight - 70)}px`);
$('.panel-footer').css('height', `${projectedHeight}px`); $('.panel-footer').css('height', `${projectedHeight}px`);
$('#newMessageInput').height($('#newMessageInput')[0].scrollHeight); $('#newMessageInput').height($('#newMessageInput')[0].scrollHeight);
// resizes the chat messages container
$('#chatbody').height($('#chat').height() - projectedHeight - 45); $('#chatbody').height($('#chat').height() - projectedHeight - 45);
$('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0); $('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
} }
@ -788,11 +878,14 @@ this.toggleEmojisFAB = function() {
}; };
this.toggleUserlistMenu = function() { this.toggleUserlistMenu = function() {
// menu
if($('.userlistMenu').hasClass('menuOut')) { if($('.userlistMenu').hasClass('menuOut')) {
$('.userlistMenu').removeClass('menuOut'); $('.userlistMenu').removeClass('menuOut');
} else { } else {
$('.userlistMenu').addClass('menuOut'); $('.userlistMenu').addClass('menuOut');
} }
// icon
if($('.toggleUserlistButton').hasClass('menuToggledOn')) { if($('.toggleUserlistButton').hasClass('menuToggledOn')) {
return $('.toggleUserlistButton').removeClass('menuToggledOn'); return $('.toggleUserlistButton').removeClass('menuToggledOn');
} else { } else {
@ -801,11 +894,14 @@ this.toggleUserlistMenu = function() {
}; };
this.toggleSettingsMenu = function() { this.toggleSettingsMenu = function() {
// menu
if($('.settingsMenu').hasClass('menuOut')) { if($('.settingsMenu').hasClass('menuOut')) {
$('.settingsMenu').removeClass('menuOut'); $('.settingsMenu').removeClass('menuOut');
} else { } else {
$('.settingsMenu').addClass('menuOut'); $('.settingsMenu').addClass('menuOut');
} }
// icon
if($('.toggleMenuButton').hasClass('menuToggledOn')) { if($('.toggleMenuButton').hasClass('menuToggledOn')) {
return $('.toggleMenuButton').removeClass('menuToggledOn'); return $('.toggleMenuButton').removeClass('menuToggledOn');
} else { } else {

View File

@ -72,6 +72,7 @@ this.BBB = (function() {
for AM_I_SHARING_CAM_RESP (see below). for AM_I_SHARING_CAM_RESP (see below).
*/ */
BBB.amISharingWebcam = function(callback) { BBB.amISharingWebcam = function(callback) {
// BBB.isUserSharingWebcam BBB.getCurrentUser()?.userId
return false; return false;
}; };
@ -86,21 +87,30 @@ this.BBB = (function() {
IS_USER_PUBLISHING_CAM_RESP (see below). IS_USER_PUBLISHING_CAM_RESP (see below).
*/ */
BBB.isUserSharingWebcam = function(userId, callback) { BBB.isUserSharingWebcam = function(userId, callback) {
// BBB.getUser(userId)?.user?.webcam_stream?.length isnt 0
return false; return false;
}; };
// returns whether the user has joined any type of audio
BBB.amIInAudio = function(callback) { BBB.amIInAudio = function(callback) {
let ref, ref1, ref2, user; let ref, ref1, ref2, user;
user = BBB.getCurrentUser(); user = BBB.getCurrentUser();
return (user != null ? (ref = user.user) != null ? ref.listenOnly : void 0 : void 0) || (user != null ? (ref1 = user.user) != null ? (ref2 = ref1.voiceUser) != null ? ref2.joined : void 0 : void 0 : void 0); return (user != null ? (ref = user.user) != null ? ref.listenOnly : void 0 : void 0) || (user != null ? (ref1 = user.user) != null ? (ref2 = ref1.voiceUser) != null ? ref2.joined : void 0 : void 0 : void 0);
}; };
// returns true if the user has joined the listen only audio stream
BBB.amIListenOnlyAudio = function(callback) { BBB.amIListenOnlyAudio = function(callback) {
let ref, ref1; let ref, ref1;
return (ref = BBB.getCurrentUser()) != null ? (ref1 = ref.user) != null ? ref1.listenOnly : void 0 : void 0; return (ref = BBB.getCurrentUser()) != null ? (ref1 = ref.user) != null ? ref1.listenOnly : void 0 : void 0;
}; };
// returns whether the user has joined the voice conference and is sharing audio through a microphone
BBB.amISharingAudio = function(callback) { BBB.amISharingAudio = function(callback) {
let ref; let ref;
return BBB.isUserSharingAudio((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0); return BBB.isUserSharingAudio((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0);
}; };
// returns whether the user is currently talking
BBB.amITalking = function(callback) { BBB.amITalking = function(callback) {
let ref; let ref;
return BBB.isUserTalking((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0); return BBB.isUserTalking((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0);
@ -126,13 +136,20 @@ this.BBB = (function() {
let ref, ref1; let ref, ref1;
return (ref = BBB.getUser(userId)) != null ? (ref1 = ref.user) != null ? ref1.presenter : void 0 : void 0; return (ref = BBB.getUser(userId)) != null ? (ref1 = ref.user) != null ? ref1.presenter : void 0 : void 0;
}; };
// returns true if the current user is marked as locked
BBB.amILocked = function() { BBB.amILocked = function() {
let ref; let ref;
return (ref = BBB.getCurrentUser()) != null ? ref.user.locked : void 0; return (ref = BBB.getCurrentUser()) != null ? ref.user.locked : void 0;
}; };
// check whether the user is locked AND the current lock settings for the room
// includes locking the microphone of viewers (listenOnly is still alowed)
BBB.isMyMicLocked = function() { BBB.isMyMicLocked = function() {
let lockedMicForRoom, ref; let lockedMicForRoom, ref;
lockedMicForRoom = (ref = Meteor.Meetings.findOne()) != null ? ref.roomLockSettings.disableMic : void 0; lockedMicForRoom = (ref = Meteor.Meetings.findOne()) != null ? ref.roomLockSettings.disableMic : void 0;
// note that voiceUser.locked is not used in BigBlueButton at this stage (April 2015)
return lockedMicForRoom && BBB.amILocked(); return lockedMicForRoom && BBB.amILocked();
}; };
BBB.getCurrentSlide = function(callingLocaton) { BBB.getCurrentSlide = function(callingLocaton) {
@ -145,6 +162,7 @@ this.BBB = (function() {
"presentationId": presentationId, "presentationId": presentationId,
"slide.current": true "slide.current": true
}); });
// console.log "trigger:#{callingLocaton} currentSlideId=#{currentSlide?._id}"
return currentSlide; return currentSlide;
}; };
BBB.getMeetingName = function() { BBB.getMeetingName = function() {
@ -461,9 +479,13 @@ this.BBB = (function() {
presentationID - the presentation to delete presentationID - the presentation to delete
*/ */
BBB.deletePresentation = function(presentationID) {}; BBB.deletePresentation = function(presentationID) {};
// Request to switch the presentation to the previous slide
BBB.goToPreviousPage = function() { BBB.goToPreviousPage = function() {
return Meteor.call('publishSwitchToPreviousSlideMessage', getInSession('meetingId'), getInSession('userId'), getInSession('authToken')); return Meteor.call('publishSwitchToPreviousSlideMessage', getInSession('meetingId'), getInSession('userId'), getInSession('authToken'));
}; };
// Request to switch the presentation to the next slide
BBB.goToNextPage = function() { BBB.goToNextPage = function() {
return Meteor.call('publishSwitchToNextSlideMessage', getInSession('meetingId'), getInSession('userId'), getInSession('authToken')); return Meteor.call('publishSwitchToNextSlideMessage', getInSession('meetingId'), getInSession('userId'), getInSession('authToken'));
}; };
@ -485,6 +507,12 @@ this.BBB = (function() {
BBB.webRTCWebcamRequestSuccess = function() {}; BBB.webRTCWebcamRequestSuccess = function() {};
BBB.webRTCWebcamRequestFail = function(reason) {}; BBB.webRTCWebcamRequestFail = function(reason) {};
// Third-party JS apps should use this to query if the BBB SWF file is ready to handle calls.
// ***********************************************************************************
// * Broadcasting of events to 3rd-party apps.
// ************************************************************************************
/* /*
Stores the 3rd-party app event listeners ** Stores the 3rd-party app event listeners **
*/ */

View File

@ -1,15 +1,19 @@
let loadLib; let loadLib;
// Helper to load javascript libraries from the BBB server
loadLib = function(libname) { loadLib = function(libname) {
let retryMessageCallback, successCallback; let retryMessageCallback, successCallback;
successCallback = function() {}; successCallback = function() {};
retryMessageCallback = function(param) { retryMessageCallback = function(param) {
//return(Meteor.log.info("Failed to load library"), param);
return console.log("Failed to load library", param); return console.log("Failed to load library", param);
}; };
return Meteor.Loader.loadJs(`${window.location.origin}/client/lib/${libname}`, successCallback, 10000).fail(retryMessageCallback); return Meteor.Loader.loadJs(`${window.location.origin}/client/lib/${libname}`, successCallback, 10000).fail(retryMessageCallback);
}; };
// These settings can just be stored locally in session, created at start up
Meteor.startup(() => { Meteor.startup(() => {
// Load SIP libraries before the application starts
loadLib('sip.js'); loadLib('sip.js');
loadLib('bbb_webrtc_bridge_sip.js'); loadLib('bbb_webrtc_bridge_sip.js');
loadLib('bbblogger.js'); loadLib('bbblogger.js');
@ -24,6 +28,7 @@ Meteor.startup(() => {
}); });
}); });
//
Template.header.events({ Template.header.events({
"click .chatBarIcon"(event) { "click .chatBarIcon"(event) {
$(".tooltip").hide(); $(".tooltip").hide();
@ -126,6 +131,8 @@ Template.main.rendered = function() {
lastOrientationWasLandscape = isLandscape(); lastOrientationWasLandscape = isLandscape();
$(window).resize(() => { $(window).resize(() => {
$('#dialog').dialog('close'); $('#dialog').dialog('close');
// when the orientation switches call the handler
if(isLandscape() && !lastOrientationWasLandscape) { if(isLandscape() && !lastOrientationWasLandscape) {
orientationBecameLandscape(); orientationBecameLandscape();
return lastOrientationWasLandscape = true; return lastOrientationWasLandscape = true;
@ -214,7 +221,7 @@ Template.main.gestures({
$('.right-drawer').removeClass('menuOut'); $('.right-drawer').removeClass('menuOut');
$('.right-drawer').css('transform', ''); $('.right-drawer').css('transform', '');
$('.toggleMenuButton').removeClass('menuToggledOn'); $('.toggleMenuButton').removeClass('menuToggledOn');
$('.shield').removeClass('darken'); $('.shield').removeClass('darken'); // in case it was opened by clicking a button
} else { } else {
$('.shield').css('opacity', 0.5); $('.shield').css('opacity', 0.5);
$('.right-drawer').css('transform', `translateX(${screenWidth - $('.right-drawer').width()}px)`); $('.right-drawer').css('transform', `translateX(${screenWidth - $('.right-drawer').width()}px)`);
@ -232,16 +239,26 @@ Template.main.gestures({
'panright #container, panleft #container'(event, template) { 'panright #container, panleft #container'(event, template) {
let initTransformValue, leftDrawerWidth, menuPanned, panIsValid, rightDrawerWidth, screenWidth; let initTransformValue, leftDrawerWidth, menuPanned, panIsValid, rightDrawerWidth, screenWidth;
if(isPortraitMobile() && isPanHorizontal(event)) { if(isPortraitMobile() && isPanHorizontal(event)) {
// panright/panleft is always triggered once right before panstart
if(!getInSession('panStarted')) { if(!getInSession('panStarted')) {
// opening the left-hand menu
if(event.type === 'panright' && event.center.x <= $('#container').width() * 0.1) { if(event.type === 'panright' && event.center.x <= $('#container').width() * 0.1) {
setInSession('panIsValid', true); setInSession('panIsValid', true);
setInSession('menuPanned', 'left'); setInSession('menuPanned', 'left');
// closing the left-hand menu
} else if(event.type === 'panleft' && event.center.x < $('#container').width() * 0.9) { } else if(event.type === 'panleft' && event.center.x < $('#container').width() * 0.9) {
setInSession('panIsValid', true); setInSession('panIsValid', true);
setInSession('menuPanned', 'left'); setInSession('menuPanned', 'left');
// opening the right-hand menu
} else if(event.type === 'panleft' && event.center.x >= $('#container').width() * 0.9) { } else if(event.type === 'panleft' && event.center.x >= $('#container').width() * 0.9) {
setInSession('panIsValid', true); setInSession('panIsValid', true);
setInSession('menuPanned', 'right'); setInSession('menuPanned', 'right');
// closing the right-hand menu
} else if(event.type === 'panright' && event.center.x > $('#container').width() * 0.1) { } else if(event.type === 'panright' && event.center.x > $('#container').width() * 0.1) {
setInSession('panIsValid', true); setInSession('panIsValid', true);
setInSession('menuPanned', 'right'); setInSession('menuPanned', 'right');
@ -250,11 +267,11 @@ Template.main.gestures({
} }
setInSession('eventType', event.type); setInSession('eventType', event.type);
if(getInSession('menuPanned') === 'left') { if(getInSession('menuPanned') === 'left') {
if($('.userlistMenu').css('transform') !== 'none') { if($('.userlistMenu').css('transform') !== 'none') { // menu is already transformed
setInSession( setInSession(
'initTransform', 'initTransform',
parseInt($('.userlistMenu').css('transform').split(',')[4]) parseInt($('.userlistMenu').css('transform').split(',')[4])
); ); // translateX value
} else if($('.userlistMenu').hasClass('menuOut')) { } else if($('.userlistMenu').hasClass('menuOut')) {
setInSession('initTransform', $('.userlistMenu').width()); setInSession('initTransform', $('.userlistMenu').width());
} else { } else {
@ -263,11 +280,11 @@ Template.main.gestures({
$('.userlistMenu').addClass('left-drawer'); $('.userlistMenu').addClass('left-drawer');
$('.left-drawer').removeClass('userlistMenu'); $('.left-drawer').removeClass('userlistMenu');
} else if(getInSession('menuPanned') === 'right') { } else if(getInSession('menuPanned') === 'right') {
if($('.settingsMenu').css('transform') !== 'none') { if($('.settingsMenu').css('transform') !== 'none') { // menu is already transformed
setInSession( setInSession(
'initTransform', 'initTransform',
parseInt($('.settingsMenu').css('transform').split(',')[4]) parseInt($('.settingsMenu').css('transform').split(',')[4])
); ); // translateX value
} else if($('.settingsMenu').hasClass('menuOut')) { } else if($('.settingsMenu').hasClass('menuOut')) {
setInSession('initTransform', $('.settingsMenu').width()); setInSession('initTransform', $('.settingsMenu').width());
} else { } else {
@ -283,7 +300,12 @@ Template.main.gestures({
leftDrawerWidth = $('.left-drawer').width(); leftDrawerWidth = $('.left-drawer').width();
rightDrawerWidth = $('.right-drawer').width(); rightDrawerWidth = $('.right-drawer').width();
screenWidth = $('#container').width(); screenWidth = $('#container').width();
if(panIsValid && menuPanned === 'left' && initTransformValue + event.deltaX >= 0 && initTransformValue + event.deltaX <= leftDrawerWidth) {
// moving the left-hand menu
if(panIsValid &&
menuPanned === 'left' &&
initTransformValue + event.deltaX >= 0 &&
initTransformValue + event.deltaX <= leftDrawerWidth) {
if($('.settingsMenu').hasClass('menuOut')) { if($('.settingsMenu').hasClass('menuOut')) {
toggleSettingsMenu(); toggleSettingsMenu();
} }
@ -292,7 +314,10 @@ Template.main.gestures({
$('.shield').addClass('animatedShield'); $('.shield').addClass('animatedShield');
} }
return $('.shield').css('opacity', 0.5 * (initTransformValue + event.deltaX) / leftDrawerWidth); return $('.shield').css('opacity', 0.5 * (initTransformValue + event.deltaX) / leftDrawerWidth);
} else if(panIsValid && menuPanned === 'right' && initTransformValue + event.deltaX >= screenWidth - rightDrawerWidth && initTransformValue + event.deltaX <= screenWidth) { } else if(panIsValid &&
menuPanned === 'right' &&
initTransformValue + event.deltaX >= screenWidth - rightDrawerWidth &&
initTransformValue + event.deltaX <= screenWidth) { // moving the right-hand menu
if($('.userlistMenu').hasClass('menuOut')) { if($('.userlistMenu').hasClass('menuOut')) {
toggleUserlistMenu(); toggleUserlistMenu();
} }

View File

@ -1,12 +1,21 @@
// --------------------------------------------------------------------------------------------------------------------
// If a function's last line is the statement false that represents the function returning false
// A function such as a click handler will continue along with the propogation and default behaivour if not stopped
// Returning false stops propogation/prevents default. You cannot always use the event object to call these methods
// Because most Meteor event handlers set the event object to the exact context of the event which does not
// allow you to simply call these methods.
// --------------------------------------------------------------------------------------------------------------------
this.activateBreakLines = function(str) { this.activateBreakLines = function(str) {
let res; let res;
if(typeof str === 'string') { if(typeof str === 'string') { // turn '\r' carriage return characters into '<br/>' break lines
res = str.replace(new RegExp(CARRIAGE_RETURN, 'g'), BREAK_LINE); res = str.replace(new RegExp(CARRIAGE_RETURN, 'g'), BREAK_LINE);
return res; return res;
} }
}; };
this.detectUnreadChat = function() { this.detectUnreadChat = function() {
//if the current tab is not the same as the tab we just published in
return Meteor.Chat.find({}).observe({ return Meteor.Chat.find({}).observe({
added: (_this => { added: (_this => {
return function(chatMessage) { return function(chatMessage) {
@ -23,7 +32,7 @@ this.detectUnreadChat = function() {
let destinationTab, tabsTime; let destinationTab, tabsTime;
tabsTime = getInSession('userListRenderedTime'); tabsTime = getInSession('userListRenderedTime');
if((tabsTime != null) && chatMessage.message.from_userid !== "SYSTEM_MESSAGE" && chatMessage.message.from_time - tabsTime > 0) { if((tabsTime != null) && chatMessage.message.from_userid !== "SYSTEM_MESSAGE" && chatMessage.message.from_time - tabsTime > 0) {
populateNotifications(chatMessage); populateNotifications(chatMessage); // check if we need to show a new notification
destinationTab = findDestinationTab(); destinationTab = findDestinationTab();
if(destinationTab !== getInSession("inChatWith")) { if(destinationTab !== getInSession("inChatWith")) {
setInSession('chats', getInSession('chats').map(tab => { setInSession('chats', getInSession('chats').map(tab => {
@ -42,10 +51,12 @@ this.detectUnreadChat = function() {
}); });
}; };
// This method returns all messages for the user. It looks at the session to determine whether the user is in
// private or public chat. If true is passed, messages returned are from before the user joined. Else, the messages are from after the user joined
this.getFormattedMessagesForChat = function() { this.getFormattedMessagesForChat = function() {
let chattingWith; let chattingWith;
chattingWith = getInSession('inChatWith'); chattingWith = getInSession('inChatWith');
if(chattingWith === 'PUBLIC_CHAT') { if(chattingWith === 'PUBLIC_CHAT') { // find all public and system messages
return Meteor.Chat.find({ return Meteor.Chat.find({
'message.chat_type': { 'message.chat_type': {
$in: ["SYSTEM_MESSAGE", "PUBLIC_CHAT"] $in: ["SYSTEM_MESSAGE", "PUBLIC_CHAT"]
@ -69,12 +80,13 @@ this.getFormattedMessagesForChat = function() {
} }
}; };
Handlebars.registerHelper("autoscroll", () => { Handlebars.registerHelper("autoscroll", () => { // Scrolls the message container to the bottom. The number of pixels to scroll down is the height of the container
let ref; let ref;
$('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0); $('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
return false; return false;
}); });
// true if the lock settings limit public chat and the current user is locked
Handlebars.registerHelper("publicChatDisabled", () => { Handlebars.registerHelper("publicChatDisabled", () => {
let presenter, publicChatIsDisabled, ref, ref1, ref2, userIsLocked; let presenter, publicChatIsDisabled, ref, ref1, ref2, userIsLocked;
userIsLocked = (ref = Meteor.Users.findOne({ userIsLocked = (ref = Meteor.Users.findOne({
@ -87,6 +99,7 @@ Handlebars.registerHelper("publicChatDisabled", () => {
return userIsLocked && publicChatIsDisabled && !presenter; return userIsLocked && publicChatIsDisabled && !presenter;
}); });
// true if the lock settings limit private chat and the current user is locked
Handlebars.registerHelper("privateChatDisabled", () => { Handlebars.registerHelper("privateChatDisabled", () => {
let presenter, privateChatIsDisabled, ref, ref1, ref2, userIsLocked; let presenter, privateChatIsDisabled, ref, ref1, ref2, userIsLocked;
userIsLocked = (ref = Meteor.Users.findOne({ userIsLocked = (ref = Meteor.Users.findOne({
@ -99,17 +112,18 @@ Handlebars.registerHelper("privateChatDisabled", () => {
return userIsLocked && privateChatIsDisabled && !presenter; return userIsLocked && privateChatIsDisabled && !presenter;
}); });
// return whether the user's chat pane is open in Private chat
Handlebars.registerHelper("inPrivateChat", () => { Handlebars.registerHelper("inPrivateChat", () => {
return (getInSession('inChatWith')) !== 'PUBLIC_CHAT'; return (getInSession('inChatWith')) !== 'PUBLIC_CHAT';
}); });
this.sendMessage = function() { this.sendMessage = function() {
let chattingWith, color, message, ref, toUsername; let chattingWith, color, message, ref, toUsername;
message = linkify($('#newMessageInput').val()); message = linkify($('#newMessageInput').val()); // get the message from the input box
if(!((message != null ? message.length : void 0) > 0 && (/\S/.test(message)))) { if(!((message != null ? message.length : void 0) > 0 && (/\S/.test(message)))) { // check the message has content and it is not whitespace
return; return; // do nothing if invalid message
} }
color = "0x000000"; color = "0x000000"; //"0x#{getInSession("messageColor")}"
if((chattingWith = getInSession('inChatWith')) !== "PUBLIC_CHAT") { if((chattingWith = getInSession('inChatWith')) !== "PUBLIC_CHAT") {
toUsername = (ref = Meteor.Users.findOne({ toUsername = (ref = Meteor.Users.findOne({
userId: chattingWith userId: chattingWith
@ -118,33 +132,35 @@ this.sendMessage = function() {
} else { } else {
BBB.sendPublicChatMessage(color, "en", message); BBB.sendPublicChatMessage(color, "en", message);
} }
return $('#newMessageInput').val(''); return $('#newMessageInput').val(''); // Clear message box
}; };
Template.chatbar.helpers({ Template.chatbar.helpers({
getCombinedMessagesForChat() { getCombinedMessagesForChat() {
let deleted, i, j, len, msgs; let deleted, i, j, len, msgs;
msgs = getFormattedMessagesForChat(); msgs = getFormattedMessagesForChat();
len = msgs != null ? msgs.length : void 0; len = msgs != null ? msgs.length : void 0; // get length of messages
i = 0; i = 0;
while(i < len) { while(i < len) { // Must be a do while, for loop compiles and stores the length of array which can change inside the loop!
if(msgs[i].message.from_userid !== 'System') { if(msgs[i].message.from_userid !== 'System') { // skip system messages
j = i + 1; j = i + 1; // Start looking at messages right after the current one
while(j < len) { while(j < len) {
deleted = false; deleted = false;
if(msgs[j].message.from_userid !== 'System') { if(msgs[j].message.from_userid !== 'System') { // Ignore system messages
if((parseFloat(msgs[j].message.from_time) - parseFloat(msgs[i].message.from_time)) >= 60000) { // Check if the time discrepancy between the two messages exceeds window for grouping
break; if((parseFloat(msgs[j].message.from_time) - parseFloat(msgs[i].message.from_time)) >= 60000) { // 60 seconds/1 minute
break; // Messages are too far between, so them seperated and stop joining here
} }
if(msgs[i].message.from_userid === msgs[j].message.from_userid) { if(msgs[i].message.from_userid === msgs[j].message.from_userid) { // Both messages are from the same user
msgs[i].message.message += `${CARRIAGE_RETURN}${msgs[j].message.message}`; // insert a '\r' carriage return character between messages to put them on a new line
msgs.splice(j, 1); msgs[i].message.message += `${CARRIAGE_RETURN}${msgs[j].message.message}`; // Combine the messages
msgs.splice(j, 1); // Delete the message from the collection
deleted = true; deleted = true;
} else { } else {
break; break; // Messages are from different people, move on
} }
} else { } else {
break; break; // This is the break point in the chat, don't merge
} }
len = msgs.length; len = msgs.length;
if(!deleted) { if(!deleted) {
@ -168,10 +184,12 @@ Template.chatbar.helpers({
} }
}); });
// When chatbar gets rendered, launch the auto-check for unread chat
Template.chatbar.rendered = function() { Template.chatbar.rendered = function() {
return detectUnreadChat(); return detectUnreadChat();
}; };
// When "< Public" is clicked, go to public chat
Template.chatbar.events({ Template.chatbar.events({
'click .toPublic'(event) { 'click .toPublic'(event) {
setInSession('inChatWith', 'PUBLIC_CHAT'); setInSession('inChatWith', 'PUBLIC_CHAT');
@ -191,6 +209,7 @@ Template.privateChatTab.rendered = function() {
} }
}; };
// When message gets rendered, scroll to the bottom
Template.message.rendered = function() { Template.message.rendered = function() {
let ref; let ref;
$('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0); $('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
@ -204,7 +223,7 @@ Template.chatInput.rendered = function() {
resize(event, ui) { resize(event, ui) {
let ref; let ref;
if($('.panel-footer').css('top') === '0px') { if($('.panel-footer').css('top') === '0px') {
$('.panel-footer').height(70); $('.panel-footer').height(70); // prevents the element from shrinking vertically for 1-2 px
} else { } else {
$('.panel-footer').css('top', `${parseInt($('.panel-footer').css('top'))}${1}px`); $('.panel-footer').css('top', `${parseInt($('.panel-footer').css('top'))}${1}px`);
} }
@ -227,15 +246,16 @@ Template.chatInput.events({
sendMessage(); sendMessage();
return adjustChatInputHeight(); return adjustChatInputHeight();
}, },
'keypress #newMessageInput'(event) { 'keypress #newMessageInput'(event) { // user pressed a button inside the chatbox
let key; let key;
key = event.charCode ? event.charCode : (event.keyCode ? event.keyCode : 0); key = event.charCode ? event.charCode : (event.keyCode ? event.keyCode : 0);
if(event.shiftKey && (key === 13)) { if(event.shiftKey && (key === 13)) {
event.preventDefault(); event.preventDefault();
document.getElementById("newMessageInput").value += CARRIAGE_RETURN; // append a '\r' carriage return character to the input box dropping the cursor to a new line
document.getElementById("newMessageInput").value += CARRIAGE_RETURN; // Change newline character
return; return;
} }
if(key === 13) { if(key === 13) { // Check for pressing enter to submit message
event.preventDefault(); event.preventDefault();
sendMessage(); sendMessage();
$('#newMessageInput').val(""); $('#newMessageInput').val("");
@ -255,7 +275,7 @@ Template.chatInputControls.rendered = function() {
Template.message.helpers({ Template.message.helpers({
sanitizeAndFormat(str) { sanitizeAndFormat(str) {
let res; let res;
if(typeof str === 'string') { if(typeof str === 'string') { // First, replace replace all tags with the ascii equivalent (excluding those involved in anchor tags)
res = str.replace(/&/g, '&amp;').replace(/<(?![au\/])/g, '&lt;').replace(/\/([^au])>/g, '$1&gt;').replace(/([^=])"(?!>)/g, '$1&quot;'); res = str.replace(/&/g, '&amp;').replace(/<(?![au\/])/g, '&lt;').replace(/\/([^au])>/g, '$1&gt;').replace(/([^=])"(?!>)/g, '$1&quot;');
res = toClickable(res); res = toClickable(res);
return res = activateBreakLines(res); return res = activateBreakLines(res);
@ -268,7 +288,7 @@ Template.message.helpers({
} }
local = new Date(); local = new Date();
offset = local.getTimezoneOffset(); offset = local.getTimezoneOffset();
epochTime = epochTime - offset * 60000; epochTime = epochTime - offset * 60000; // 1 min = 60 s = 60,000 ms
dateObj = new Date(epochTime); dateObj = new Date(epochTime);
hours = dateObj.getUTCHours(); hours = dateObj.getUTCHours();
minutes = dateObj.getUTCMinutes(); minutes = dateObj.getUTCMinutes();
@ -279,6 +299,7 @@ Template.message.helpers({
} }
}); });
// make links received from Flash client clickable in HTML
this.toClickable = function(str) { this.toClickable = function(str) {
let res; let res;
if(typeof str === 'string') { if(typeof str === 'string') {

View File

@ -45,7 +45,7 @@ Template.settingsModal.events({
Template.optionsFontSize.events({ Template.optionsFontSize.events({
"click #decreaseFontSize"(event) { "click #decreaseFontSize"(event) {
if(getInSession("messageFontSize") === 8) { if(getInSession("messageFontSize") === 8) { // min
$('#decreaseFontSize').disabled = true; $('#decreaseFontSize').disabled = true;
$('#decreaseFontSize').removeClass('icon fi-minus'); $('#decreaseFontSize').removeClass('icon fi-minus');
return $('#decreaseFontSize').html('MIN'); return $('#decreaseFontSize').html('MIN');
@ -60,7 +60,7 @@ Template.optionsFontSize.events({
} }
}, },
"click #increaseFontSize"(event) { "click #increaseFontSize"(event) {
if(getInSession("messageFontSize") === 40) { if(getInSession("messageFontSize") === 40) { // max
$('#increaseFontSize').disabled = true; $('#increaseFontSize').disabled = true;
$('#increaseFontSize').removeClass('icon fi-plus'); $('#increaseFontSize').removeClass('icon fi-plus');
return $('#increaseFontSize').html('MAX'); return $('#increaseFontSize').html('MAX');

View File

@ -3,6 +3,10 @@ Template.displayUserIcons.events({
return toggleMic(this); return toggleMic(this);
}, },
'click .raisedHandIcon'(event) { 'click .raisedHandIcon'(event) {
// the function to call 'userLowerHand'
// the meeting id
// the _id of the person whose land is to be lowered
// the userId of the person who is lowering the hand
return BBB.lowerHand(getInSession("meetingId"), this.userId, getInSession("userId"), getInSession("authToken")); return BBB.lowerHand(getInSession("meetingId"), this.userId, getInSession("userId"), getInSession("authToken"));
}, },
'click .kickUser'(event) { 'click .kickUser'(event) {
@ -12,6 +16,8 @@ Template.displayUserIcons.events({
Template.displayUserIcons.helpers({ Template.displayUserIcons.helpers({
userLockedIconApplicable(userId) { userLockedIconApplicable(userId) {
// the lock settings affect the user (and requiire a lock icon) if
// the user is set to be locked and there is a relevant lock in place
let lockInAction, locked, ref, ref1, settings; let lockInAction, locked, ref, ref1, settings;
locked = (ref = BBB.getUser(userId)) != null ? ref.user.locked : void 0; locked = (ref = BBB.getUser(userId)) != null ? ref.user.locked : void 0;
settings = (ref1 = Meteor.Meetings.findOne()) != null ? ref1.roomLockSettings : void 0; settings = (ref1 = Meteor.Meetings.findOne()) != null ? ref1.roomLockSettings : void 0;
@ -20,6 +26,7 @@ Template.displayUserIcons.helpers({
} }
}); });
// Opens a private chat tab when a username from the userlist is clicked
Template.usernameEntry.events({ Template.usernameEntry.events({
'click .usernameEntry'(event) { 'click .usernameEntry'(event) {
let ref, userIdSelected; let ref, userIdSelected;
@ -35,7 +42,7 @@ Template.usernameEntry.events({
toggleUserlistMenu(); toggleUserlistMenu();
toggleShield(); toggleShield();
} }
return setTimeout(() => { return setTimeout(() => { // waits until the end of execution queue
return $("#newMessageInput").focus(); return $("#newMessageInput").focus();
}, 0); }, 0);
}, },

View File

@ -5,6 +5,7 @@ Template.usersList.helpers({
if (numberUsers > 8) { if (numberUsers > 8) {
return `Users: ${numberUsers}`; return `Users: ${numberUsers}`;
} }
// do not display the label if there are just a few users
} }
}); });

View File

@ -12,7 +12,8 @@ this.reactOnSlideChange = (_this => {
setInSession('slideOriginalWidth', this.width); setInSession('slideOriginalWidth', this.width);
setInSession('slideOriginalHeight', this.height); setInSession('slideOriginalHeight', this.height);
$(window).resize(() => { $(window).resize(() => {
if(!$('.panel-footer').hasClass('ui-resizable-resizing')) { // redraw the whiteboard to adapt to the resized window
if(!$('.panel-footer').hasClass('ui-resizable-resizing')) { // not in the middle of resizing the message input
return scaleWhiteboard(); return scaleWhiteboard();
} }
}); });
@ -28,6 +29,7 @@ this.reactOnSlideChange = (_this => {
})(this); })(this);
this.createWhiteboardPaper = (_this => { this.createWhiteboardPaper = (_this => {
// console.log "CREATING WPM"
return function(callback) { return function(callback) {
_this.whiteboardPaperModel = new Meteor.WhiteboardPaperModel('whiteboard-paper'); _this.whiteboardPaperModel = new Meteor.WhiteboardPaperModel('whiteboard-paper');
return callback(_this.whiteboardPaperModel); return callback(_this.whiteboardPaperModel);
@ -67,7 +69,7 @@ this.manuallyDisplayShapes = function() {
shapeType = shapeInfo != null ? shapeInfo.type : void 0; shapeType = shapeInfo != null ? shapeInfo.type : void 0;
if(shapeType !== "text") { if(shapeType !== "text") {
len = shapeInfo.points.length; len = shapeInfo.points.length;
for(num = j = 0, ref2 = len; 0 <= ref2 ? j <= ref2 : j >= ref2; num = 0 <= ref2 ? ++j : --j) { for(num = j = 0, ref2 = len; 0 <= ref2 ? j <= ref2 : j >= ref2; num = 0 <= ref2 ? ++j : --j) { // the coordinates must be in the range 0 to 1
if(shapeInfo != null) { if(shapeInfo != null) {
shapeInfo.points[num] = (shapeInfo != null ? shapeInfo.points[num] : void 0) / 100; shapeInfo.points[num] = (shapeInfo != null ? shapeInfo.points[num] : void 0) / 100;
} }
@ -81,17 +83,29 @@ this.manuallyDisplayShapes = function() {
return results; return results;
}; };
// calculates and returns the best fitting {width, height} pair
// based on the image's original width and height
this.scaleSlide = function(originalWidth, originalHeight) { this.scaleSlide = function(originalWidth, originalHeight) {
let adjustedHeight, adjustedWidth, boardHeight, boardWidth; let adjustedHeight, adjustedWidth, boardHeight, boardWidth;
// set the size of the whiteboard space (frame) where the slide will be displayed
if(window.matchMedia('(orientation: landscape)').matches) { if(window.matchMedia('(orientation: landscape)').matches) {
// for landscape orientation we want "fit to height" so that we can
// minimize the empty space above and below the slide (for best readability)
boardWidth = $("#whiteboard-container").width(); boardWidth = $("#whiteboard-container").width();
boardHeight = $("#whiteboard-container").height(); boardHeight = $("#whiteboard-container").height();
} else { } else {
// for portrait orientation we want "fit to width" so that we can
// minimize the empty space on the sides of the slide (for best readability)
boardWidth = $("#whiteboard-container").width(); boardWidth = $("#whiteboard-container").width();
boardHeight = 1.4 * $("#whiteboard-container").width(); boardHeight = 1.4 * $("#whiteboard-container").width();
} }
// this is the best fitting pair
adjustedWidth = null; adjustedWidth = null;
adjustedHeight = null; adjustedHeight = null;
// the slide image is in portrait orientation
if(originalWidth <= originalHeight) { if(originalWidth <= originalHeight) {
adjustedWidth = boardHeight * originalWidth / originalHeight; adjustedWidth = boardHeight * originalWidth / originalHeight;
if (boardWidth < adjustedWidth) { if (boardWidth < adjustedWidth) {
@ -100,6 +114,8 @@ this.scaleSlide = function(originalWidth, originalHeight) {
} else { } else {
adjustedHeight = boardHeight; adjustedHeight = boardHeight;
} }
// ths slide image is in landscape orientation
} else { } else {
adjustedHeight = boardWidth * originalHeight / originalWidth; adjustedHeight = boardWidth * originalHeight / originalWidth;
if (boardHeight < adjustedHeight) { if (boardHeight < adjustedHeight) {
@ -123,13 +139,16 @@ Template.slide.helpers({
} }
}); });
//// SHAPE ////
Template.shape.rendered = function() { Template.shape.rendered = function() {
let i, len, num, ref, ref1, shapeInfo, shapeType, wpm; let i, len, num, ref, ref1, shapeInfo, shapeType, wpm;
// @data is the shape object coming from the {{#each}} in the html file
shapeInfo = ((ref = this.data.shape) != null ? ref.shape : void 0) || this.data.shape; shapeInfo = ((ref = this.data.shape) != null ? ref.shape : void 0) || this.data.shape;
shapeType = shapeInfo != null ? shapeInfo.type : void 0; shapeType = shapeInfo != null ? shapeInfo.type : void 0;
if(shapeType !== "text") { if(shapeType !== "text") {
len = shapeInfo.points.length; len = shapeInfo.points.length;
for (num = i = 0, ref1 = len; 0 <= ref1 ? i <= ref1 : i >= ref1; num = 0 <= ref1 ? ++i : --i) { for (num = i = 0, ref1 = len; 0 <= ref1 ? i <= ref1 : i >= ref1; num = 0 <= ref1 ? ++i : --i) { // the coordinates must be in the range 0 to 1
shapeInfo.points[num] = shapeInfo.points[num] / 100; shapeInfo.points[num] = shapeInfo.points[num] / 100;
} }
} }

View File

@ -1,5 +1,6 @@
let fakeUpload; let fakeUpload;
// scale the whiteboard to adapt to the resized window
this.scaleWhiteboard = function(callback) { this.scaleWhiteboard = function(callback) {
let adjustedDimensions; let adjustedDimensions;
adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight')); adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'));
@ -27,9 +28,13 @@ this.scaleWhiteboard = function(callback) {
}, },
clearSlide() { clearSlide() {
let ref; let ref;
//clear the slide
if(typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null) { if(typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null) {
whiteboardPaperModel.removeAllImagesFromPaper(); whiteboardPaperModel.removeAllImagesFromPaper();
} }
// hide the cursor
return typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null ? (ref = whiteboardPaperModel.cursor) != null ? ref.remove() : void 0 : void 0; return typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null ? (ref = whiteboardPaperModel.cursor) != null ? ref.remove() : void 0 : void 0;
} }
}; };
@ -212,25 +217,29 @@ Template.whiteboard.rendered = function() {
return adjustChatInputHeight(); return adjustChatInputHeight();
}, },
start() { start() {
if($('#chat').width() / $('#panels').width() > 0.2) { if($('#chat').width() / $('#panels').width() > 0.2) { // chat shrinking can't make it smaller than one fifth of the whiteboard-chat area
return $('#whiteboard').resizable('option', 'maxWidth', $('#panels').width() - 200); return $('#whiteboard').resizable('option', 'maxWidth', $('#panels').width() - 200); // gives the chat enough space (200px)
} else { } else {
return $('#whiteboard').resizable('option', 'maxWidth', $('#whiteboard').width()); return $('#whiteboard').resizable('option', 'maxWidth', $('#whiteboard').width());
} }
}, },
stop() { stop() {
$('#whiteboard').css('width', `${100 * $('#whiteboard').width() / $('#panels').width()}%`); $('#whiteboard').css('width', `${100 * $('#whiteboard').width() / $('#panels').width()}%`); // transforms width to %
return $('#whiteboard').resizable('option', 'maxWidth', null); return $('#whiteboard').resizable('option', 'maxWidth', null);
} }
}); });
// whiteboard element needs to be available
Meteor.NotificationControl = new NotificationControl('notificationArea'); Meteor.NotificationControl = new NotificationControl('notificationArea');
return $(document).foundation();
return $(document).foundation(); // initialize foundation javascript
}; };
Template.presenterUploaderControl.created = function() { Template.presenterUploaderControl.created = function() {
this.isOpen = new ReactiveVar(false); this.isOpen = new ReactiveVar(false);
this.files = new ReactiveList({ this.files = new ReactiveList({
sort(a, b) { sort(a, b) {
// Put the ones who still uploading first
let ref, ref1; let ref, ref1;
return (ref = a.isUploading === b.isUploading) != null ? ref : { return (ref = a.isUploading === b.isUploading) != null ? ref : {
0: (ref1 = a.isUploading) != null ? ref1 : -{ 0: (ref1 = a.isUploading) != null ? ref1 : -{
@ -261,7 +270,7 @@ fakeUpload = function(file, list) {
if(file.isUploading === true) { if(file.isUploading === true) {
return fakeUpload(file, list); return fakeUpload(file, list);
} else { } else {
return list.remove(file.name); return list.remove(file.name); // TODO: Here we should remove and update te presentation on mongo
} }
}), 200); }), 200);
}; };

View File

@ -1,3 +1,4 @@
// A base class for whiteboard tools
this.WhiteboardToolModel = (function() { this.WhiteboardToolModel = (function() {
class WhiteboardToolModel { class WhiteboardToolModel {
constructor() {} constructor() {}
@ -8,9 +9,13 @@ this.WhiteboardToolModel = (function() {
this.gh = 0; this.gh = 0;
this.gw = 0; this.gw = 0;
this.obj = 0; this.obj = 0;
// the defintion of this shape, kept so we can redraw the shape whenever needed
return this.definition = []; return this.definition = [];
} }
//set the size of the paper
// @param {number} @gh gh parameter
// @param {number} @gw gw parameter
setPaperSize(gh, gw) { setPaperSize(gh, gw) {
this.gh = gh; this.gh = gh;
this.gw = gw; this.gw = gw;
@ -22,6 +27,7 @@ this.WhiteboardToolModel = (function() {
} }
setPaperDimensions(paperWidth, paperHeight) { setPaperDimensions(paperWidth, paperHeight) {
// TODO: can't we simply take the width and the height from `@paper`?
this.paperWidth = paperWidth; this.paperWidth = paperWidth;
this.paperHeight = paperHeight; this.paperHeight = paperHeight;
} }

View File

@ -1,4 +1,11 @@
// General utility methods
Meteor.methods({ Meteor.methods({
// POST request using javascript
// @param {string} path path of submission
// @param {string} params parameters to submit
// @param {string} method method of submission ("post" is default)
// @return {undefined}
postToUrl(path, params, method="post") { postToUrl(path, params, method="post") {
let $hiddenField, form, key; let $hiddenField, form, key;
form = $("<form></form>"); form = $("<form></form>");
@ -22,16 +29,18 @@ Meteor.methods({
} }
}); });
// thickness can be a number (e.g. "2") or a string (e.g. "2px")
this.formatThickness = function(thickness) { this.formatThickness = function(thickness) {
if(thickness == null) { if(thickness == null) {
thickness = "1"; thickness = "1"; // default value
} }
if(!thickness.toString().match(/.*px$/)) { if(!thickness.toString().match(/.*px$/)) {
`#${thickness}px`; `#${thickness}px`; // leading "#" - to be compatible with Firefox
} }
return thickness; return thickness;
}; };
// applies zooming to the stroke thickness
this.zoomStroke = function(thickness) { this.zoomStroke = function(thickness) {
let currentSlide, ratio; let currentSlide, ratio;
currentSlide = BBB.getCurrentSlide("zoomStroke"); currentSlide = BBB.getCurrentSlide("zoomStroke");

View File

@ -4,6 +4,7 @@ const bind = function(fn, me) {
}; };
}; };
// The cursor/pointer in the whiteboard
this.WhiteboardCursorModel = (function() { this.WhiteboardCursorModel = (function() {
class WhiteboardCursorModel { class WhiteboardCursorModel {
constructor(paper, radius, color) { constructor(paper, radius, color) {
@ -15,7 +16,7 @@ this.WhiteboardCursorModel = (function() {
this.radius = 6; this.radius = 6;
} }
if(this.color == null) { if(this.color == null) {
this.color = "#ff6666"; this.color = "#ff6666"; // a pinkish red
} }
this.cursor = null; this.cursor = null;
} }

View File

@ -1,12 +1,23 @@
// An ellipse in the whiteboard
this.WhiteboardEllipseModel = (function() { this.WhiteboardEllipseModel = (function() {
class WhiteboardEllipseModel extends WhiteboardToolModel { class WhiteboardEllipseModel extends WhiteboardToolModel {
constructor(paper) { constructor(paper) {
super(paper); super(paper);
this.paper = paper; this.paper = paper;
// the defintion of this shape, kept so we can redraw the shape whenever needed
// format: top left x, top left y, bottom right x, bottom right y, stroke color, thickness
this.definition = [0, 0, 0, 0, "#000", "0px"]; this.definition = [0, 0, 0, 0, "#000", "0px"];
} }
// Make an ellipse on the whiteboard
// @param {[type]} x the x value of the top left corner
// @param {[type]} y the y value of the top left corner
// @param {string} colour the colour of the object
// @param {number} thickness the thickness of the object's line(s)
make(info) { make(info) {
//console.log "Whiteboard - Making ellipse: "
//console.log info
let color, thickness, x, y; let color, thickness, x, y;
if((info != null ? info.points : void 0) != null) { if((info != null ? info.points : void 0) != null) {
x = info.points[0]; x = info.points[0];
@ -21,7 +32,14 @@ this.WhiteboardEllipseModel = (function() {
return this.obj; return this.obj;
} }
// Update ellipse drawn
// @param {number} x1 the x value of the top left corner in percent of current slide size
// @param {number} y1 the y value of the top left corner in percent of current slide size
// @param {number} x2 the x value of the bottom right corner in percent of current slide size
// @param {number} y2 the y value of the bottom right corner in percent of current slide size
// @param {boolean} square (draw a circle or not
update(info) { update(info) {
//console.log info
let circle, coords, r, ref, ref1, reversed, rx, ry, x1, x2, y1, y2; let circle, coords, r, ref, ref1, reversed, rx, ry, x1, x2, y1, y2;
if((info != null ? info.points : void 0) != null) { if((info != null ? info.points : void 0) != null) {
x1 = info.points[0]; x1 = info.points[0];
@ -37,8 +55,11 @@ this.WhiteboardEllipseModel = (function() {
ref1 = [y2, y1], y1 = ref1[0], y2 = ref1[1]; ref1 = [y2, y1], y1 = ref1[0], y2 = ref1[1];
reversed = true; reversed = true;
} }
//if the control key is pressed then the width and height of the ellipse are equal (a circle)
//we calculate this by making the y2 coord equal to the y1 coord plus the width of x2-x1 and corrected for the slide size
if(circle) { if(circle) {
if(reversed) { if(reversed) { // if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * this.gw / this.gh; y1 = y2 - (x2 - x1) * this.gw / this.gh;
} else { } else {
y2 = y1 + (x2 - x1) * this.gw / this.gh; y2 = y1 + (x2 - x1) * this.gw / this.gh;
@ -50,6 +71,9 @@ this.WhiteboardEllipseModel = (function() {
y1: y1, y1: y1,
y2: y2 y2: y2
}; };
//console.log(coords)
rx = (x2 - x1) / 2; rx = (x2 - x1) / 2;
ry = (y2 - y1) / 2; ry = (y2 - y1) / 2;
r = { r = {
@ -59,6 +83,9 @@ this.WhiteboardEllipseModel = (function() {
cy: (ry + y1) * this.gh + this.yOffset cy: (ry + y1) * this.gh + this.yOffset
}; };
this.obj.attr(r); this.obj.attr(r);
//console.log( "@gw: " + @gw + "\n@gh: " + @gh + "\n@xOffset: " + @xOffset + "\n@yOffset: " + @yOffset );
// we need to update all these values, specially for when shapes are drawn backwards
this.definition[0] = x1; this.definition[0] = x1;
this.definition[1] = y1; this.definition[1] = y1;
this.definition[2] = x2; this.definition[2] = x2;
@ -67,6 +94,13 @@ this.WhiteboardEllipseModel = (function() {
} }
} }
// Draw an ellipse on the whiteboard
// @param {number} x1 the x value of the top left corner
// @param {number} y1 the y value of the top left corner
// @param {number} x2 the x value of the bottom right corner
// @param {number} y2 the y value of the bottom right corner
// @param {string} colour the colour of the object
// @param {number} thickness the thickness of the object's line(s)
draw(x1, y1, x2, y2, colour, thickness) { draw(x1, y1, x2, y2, colour, thickness) {
let elip, ref, ref1, rx, ry, x, y; let elip, ref, ref1, rx, ry, x, y;
if(x2 < x1) { if(x2 < x1) {
@ -84,9 +118,53 @@ this.WhiteboardEllipseModel = (function() {
return elip; return elip;
} }
dragOnStart(x, y) {} // When first starting drawing the ellipse
dragOnMove(dx, dy, x, y, e) {} // @param {number} x the x value of cursor at the time in relation to the left side of the browser
dragOnStop(e) {} // @param {number} y the y value of cursor at the time in relation to the top of the browser
// TODO: moved here but not finished
dragOnStart(x, y) {
// sx = (@paperWidth - @gw) / 2
// sy = (@paperHeight - @gh) / 2
// // find the x and y values in relation to the whiteboard
// @ellipseX = (x - @containerOffsetLeft - sx + @xOffset)
// @ellipseY = (y - @containerOffsetTop - sy + @yOffset)
// globals.connection.emitMakeShape "ellipse",
// [ @ellipseX / @paperWidth, @ellipseY / @paperHeight, @currentColour, @currentThickness ]
}
// When first starting to draw an ellipse
// @param {number} dx the difference in the x value at the start as opposed to the x value now
// @param {number} dy the difference in the y value at the start as opposed to the y value now
// @param {number} x the x value of cursor at the time in relation to the left side of the browser
// @param {number} y the y value of cursor at the time in relation to the top of the browser
// @param {Event} e the mouse event
// TODO: moved here but not finished
dragOnMove(dx, dy, x, y, e) {
// // if shift is pressed, draw a circle instead of ellipse
// dy = dx if @shiftPressed
// dx = dx / 2
// dy = dy / 2
// // adjust for negative values as well
// x = @ellipseX + dx
// y = @ellipseY + dy
// dx = (if dx < 0 then -dx else dx)
// dy = (if dy < 0 then -dy else dy)
// globals.connection.emitUpdateShape "ellipse",
// [ x / @paperWidth, y / @paperHeight, dx / @paperWidth, dy / @paperHeight ]
}
// When releasing the mouse after drawing the ellipse
// @param {Event} e the mouse event
// TODO: moved here but not finished
dragOnStop(e) {
// attrs = undefined
// attrs = @currentEllipse.attrs if @currentEllipse?
// if attrs?
// globals.connection.emitPublishShape "ellipse",
// [ attrs.cx / @gw, attrs.cy / @gh, attrs.rx / @gw, attrs.ry / @gh,
// @currentColour, @currentThickness ]
// @currentEllipse = null # late updates will be blocked by this
}
} }
return WhiteboardEllipseModel; return WhiteboardEllipseModel;

View File

@ -1,13 +1,27 @@
let MAX_PATHS_IN_SEQUENCE = 30; let MAX_PATHS_IN_SEQUENCE = 30;
this.WhiteboardLineModel = (function() { this.WhiteboardLineModel = (function() {
// A line in the whiteboard
// Note: is used to draw lines from the pencil tool and from the line tool, this is why some
// methods can receive different set of parameters.
// TODO: Maybe this should be split in WhiteboardPathModel for the pencil and
// WhiteboardLineModel for the line tool
class WhiteboardLineModel extends WhiteboardToolModel { class WhiteboardLineModel extends WhiteboardToolModel {
constructor(paper) { constructor(paper) {
super(paper); super(paper);
this.paper = paper; this.paper = paper;
// the defintion of this shape, kept so we can redraw the shape whenever needed
// format: svg path, stroke color, thickness
this.definition = ["", "#000", "0px"]; this.definition = ["", "#000", "0px"];
} }
// Creates a line in the paper
// @param {number} x the x value of the line start point as a percentage of the original width
// @param {number} y the y value of the line start point as a percentage of the original height
// @param {string} colour the colour of the shape to be drawn
// @param {number} thickness the thickness of the line to be drawn
make(info) { make(info) {
let color, path, pathPercent, thickness, x, x1, y, y1; let color, path, pathPercent, thickness, x, x1, y, y1;
if((info != null ? info.points : void 0) != null) { if((info != null ? info.points : void 0) != null) {
@ -31,6 +45,16 @@ this.WhiteboardLineModel = (function() {
return this.obj; return this.obj;
} }
// Update the line dimensions
// @param {number} x1 1) the x of the first point
// 2) the next x point to be added to the line
// @param {number} y1 1) the y of the first point
// 2) the next y point to be added to the line
// @param {number,boolean} x2 1) the x of the second point
// 2) true if the line should be added to the current line,
// false if it should replace the last point
// @param {number} y2 1) the y of the second point
// 2) undefined
update(info) { update(info) {
let path, x1, x2, y1, y2; let path, x1, x2, y1, y2;
if((info != null ? info.points : void 0) != null) { if((info != null ? info.points : void 0) != null) {
@ -49,10 +73,99 @@ this.WhiteboardLineModel = (function() {
} }
} }
draw(x1, y1, x2, y2, colour, thickness) {} // Draw a line on the paper
dragOnStart(x, y) {} // @param {number,string} x1 1) the x value of the first point
dragOnMove(dx, dy, x, y) {} // 2) the string path
dragOnEnd(e) {} // @param {number,string} y1 1) the y value of the first point
// 2) the colour
// @param {number} x2 1) the x value of the second point
// 2) the thickness
// @param {number} y2 1) the y value of the second point
// 2) undefined
// @param {string} colour 1) the colour of the shape to be drawn
// 2) undefined
// @param {number} thickness 1) the thickness of the line to be drawn
// 2) undefined
draw(x1, y1, x2, y2, colour, thickness) {
// if the drawing is from the pencil tool, it comes as a path first
// if _.isString(x1)
// colour = y1
// thickness = x2
// path = x1
// // if the drawing is from the line tool, it comes with two points
// else
// path = @_buildPath(points)
// line = @paper.path(@_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset))
// line.attr Utils.strokeAndThickness(colour, thickness)
// line.attr({"stroke-linejoin": "round"})
// line
}
// When dragging for drawing lines starts
// @param {number} x the x value of the cursor
// @param {number} y the y value of the cursor
// TODO: moved here but not finished
dragOnStart(x, y) {
// // find the x and y values in relation to the whiteboard
// sx = (@paperWidth - @gw) / 2
// sy = (@paperHeight - @gh) / 2
// @lineX = x - @containerOffsetLeft - sx + @xOffset
// @lineY = y - @containerOffsetTop - sy + @yOffset
// values = [ @lineX / @paperWidth, @lineY / @paperHeight, @currentColour, @currentThickness ]
// globals.connection.emitMakeShape "line", values
}
// As line drawing drag continues
// @param {number} dx the difference between the x value from _lineDragStart and now
// @param {number} dy the difference between the y value from _lineDragStart and now
// @param {number} x the x value of the cursor
// @param {number} y the y value of the cursor
// TODO: moved here but not finished
dragOnMove(dx, dy, x, y) {
// sx = (@paperWidth - @gw) / 2
// sy = (@paperHeight - @gh) / 2
// [cx, cy] = @_currentSlideOffsets()
// // find the x and y values in relation to the whiteboard
// @cx2 = x - @containerOffsetLeft - sx + @xOffset
// @cy2 = y - @containerOffsetTop - sy + @yOffset
// if @shiftPressed
// globals.connection.emitUpdateShape "line", [ @cx2 / @paperWidth, @cy2 / @paperHeight, false ]
// else
// @currentPathCount++
// if @currentPathCount < MAX_PATHS_IN_SEQUENCE
// globals.connection.emitUpdateShape "line", [ @cx2 / @paperHeight, @cy2 / @paperHeight, true ]
// else if @obj?
// @currentPathCount = 0
// // save the last path of the line
// @obj.attrs.path.pop()
// path = @obj.attrs.path.join(" ")
// @obj.attr path: (path + "L" + @lineX + " " + @lineY)
// // scale the path appropriately before sending
// pathStr = @obj.attrs.path.join(",")
// globals.connection.emitPublishShape "path",
// [ @_scaleLinePath(pathStr, 1 / @gw, 1 / @gh),
// @currentColour, @currentThickness ]
// globals.connection.emitMakeShape "line",
// [ @lineX / @paperWidth, @lineY / @paperHeight, @currentColour, @currentThickness ]
// @lineX = @cx2
// @lineY = @cy2
}
// Drawing line has ended
// @param {Event} e the mouse event
// TODO: moved here but not finished
dragOnEnd(e) {
// if @obj?
// path = @obj.attrs.path
// @obj = null # any late updates will be blocked by this
// // scale the path appropriately before sending
// globals.connection.emitPublishShape "path",
// [ @_scaleLinePath(path.join(","), 1 / @gw, 1 / @gh),
// @currentColour, @currentThickness ]
}
_buildPath(points) { _buildPath(points) {
let i, path; let i, path;
@ -69,6 +182,10 @@ this.WhiteboardLineModel = (function() {
} }
} }
// Scales a path string to fit within a width and height of the new paper size
// @param {number} w width of the shape as a percentage of the original width
// @param {number} h height of the shape as a percentage of the original height
// @return {string} the path string after being manipulated to new paper size
_scaleLinePath(string, w, h, xOffset, yOffset) { _scaleLinePath(string, w, h, xOffset, yOffset) {
let j, len, path, points; let j, len, path, points;
if(xOffset == null) { if(xOffset == null) {
@ -81,6 +198,8 @@ this.WhiteboardLineModel = (function() {
points = string.match(/(\d+[.]?\d*)/g); points = string.match(/(\d+[.]?\d*)/g);
len = points.length; len = points.length;
j = 0; j = 0;
// go through each point and multiply it by the new height and width
while(j < len) { while(j < len) {
if(j !== 0) { if(j !== 0) {
path += `${points[j + 1] * h}${yOffset}L${points[j] * w + xOffset},${points[j + 1] * h + yOffset}`; path += `${points[j + 1] * h}${yOffset}L${points[j] * w + xOffset},${points[j + 1] * h + yOffset}`;

View File

@ -1,14 +1,29 @@
// "Paper" which is the Raphael term for the entire SVG object on the webpage.
// This class deals with this SVG component only.
Meteor.WhiteboardPaperModel = (function() { Meteor.WhiteboardPaperModel = (function() {
class WhiteboardPaperModel { class WhiteboardPaperModel {
// Container must be a DOM element
constructor(container) { constructor(container) {
this.container = container; this.container = container;
// a WhiteboardCursorModel
this.cursor = null; this.cursor = null;
// all slides in the presentation indexed by url
this.slides = {}; this.slides = {};
this.panX = null; this.panX = null;
this.panY = null; this.panY = null;
this.current = {}; this.current = {};
// the slide being shown
this.current.slide = null; this.current.slide = null;
// a raphaeljs set with all the shapes in the current slide
this.current.shapes = null; this.current.shapes = null;
// a list of shapes as passed to this client when it receives `all_slides`
// (se we are able to redraw the shapes whenever needed)
this.current.shapeDefinitions = []; this.current.shapeDefinitions = [];
this.zoomLevel = 1; this.zoomLevel = 1;
this.shiftPressed = false; this.shiftPressed = false;
@ -21,7 +36,13 @@ Meteor.WhiteboardPaperModel = (function() {
this.heightRatio = 100; this.heightRatio = 100;
} }
// Initializes the paper in the page.
// Can't do these things in initialize() because by then some elements
// are not yet created in the page.
create() { create() {
// paper is embedded within the div#slide of the page.
// @raphaelObj ?= ScaleRaphael(@container, "900", "500")
let h, w; let h, w;
h = $(`#${this.container}`).height(); h = $(`#${this.container}`).height();
w = $(`#${this.container}`).width(); w = $(`#${this.container}`).width();
@ -36,7 +57,7 @@ Meteor.WhiteboardPaperModel = (function() {
if(this.slides) { if(this.slides) {
this.rebuild(); this.rebuild();
} else { } else {
this.slides = {}; this.slides = {}; // if previously loaded
} }
if(navigator.userAgent.indexOf("Firefox") !== -1) { if(navigator.userAgent.indexOf("Firefox") !== -1) {
this.raphaelObj.renderfix(); this.raphaelObj.renderfix();
@ -44,6 +65,8 @@ Meteor.WhiteboardPaperModel = (function() {
return this.raphaelObj; return this.raphaelObj;
} }
// Re-add the images to the paper that are found
// in the slides array (an object of urls and dimensions).
rebuild() { rebuild() {
let results, url; let results, url;
this.current.slide = null; this.current.slide = null;
@ -65,14 +88,29 @@ Meteor.WhiteboardPaperModel = (function() {
return (ref = this.raphaelObj) != null ? ref.changeSize(width, height) : void 0; return (ref = this.raphaelObj) != null ? ref.changeSize(width, height) : void 0;
} }
// Add an image to the paper.
// @param {string} url the URL of the image to add to the paper
// @param {number} width the width of the image (in pixels)
// @param {number} height the height of the image (in pixels)
// @return {Raphael.image} the image object added to the whiteboard
addImageToPaper(url, width, height) { addImageToPaper(url, width, height) {
let cx, cy, img, max, sh, sw; let cx, cy, img, max, sh, sw;
this._updateContainerDimensions(); this._updateContainerDimensions();
// solve for the ratio of what length is going to fit more than the other
max = Math.max(width / this.containerWidth, height / this.containerHeight); max = Math.max(width / this.containerWidth, height / this.containerHeight);
// fit it all in appropriately
url = this._slideUrl(url); url = this._slideUrl(url);
sw = width / max; sw = width / max;
sh = height / max; sh = height / max;
//cx = (@containerWidth / 2) - (width / 2)
//cy = (@containerHeight / 2) - (height / 2)
img = this.raphaelObj.image(url, cx = 0, cy = 0, width, height); img = this.raphaelObj.image(url, cx = 0, cy = 0, width, height);
// sw slide width as percentage of original width of paper
// sh slide height as a percentage of original height of paper
// x-offset from top left corner as percentage of original width of paper
// y-offset from top left corner as percentage of original height of paper
this.slides[url] = new WhiteboardSlideModel(img.id, url, img, width, height, sw, sh, cx, cy); this.slides[url] = new WhiteboardSlideModel(img.id, url, img, width, height, sw, sh, cx, cy);
if(this.current.slide == null) { if(this.current.slide == null) {
img.toBack(); img.toBack();
@ -82,9 +120,12 @@ Meteor.WhiteboardPaperModel = (function() {
} else { } else {
img.hide(); img.hide();
} }
// TODO: other places might also required an update in these dimensions
this._updateContainerDimensions(); this._updateContainerDimensions();
this._updateZoomRatios(); this._updateZoomRatios();
if(this.raphaelObj.w === 100) { if(this.raphaelObj.w === 100) { // on first load: Raphael object is initially tiny
this.cursor.setRadius(0.65 * this.widthRatio / 100); this.cursor.setRadius(0.65 * this.widthRatio / 100);
} else { } else {
this.cursor.setRadius(6 * this.widthRatio / 100); this.cursor.setRadius(6 * this.widthRatio / 100);
@ -92,6 +133,7 @@ Meteor.WhiteboardPaperModel = (function() {
return img; return img;
} }
// Removes all the images from the Raphael paper.
removeAllImagesFromPaper() { removeAllImagesFromPaper() {
let ref, ref1, url; let ref, ref1, url;
for(url in this.slides) { for(url in this.slides) {
@ -99,12 +141,17 @@ Meteor.WhiteboardPaperModel = (function() {
if ((ref = this.raphaelObj.getById((ref1 = this.slides[url]) != null ? ref1.getId() : void 0)) != null) { if ((ref = this.raphaelObj.getById((ref1 = this.slides[url]) != null ? ref1.getId() : void 0)) != null) {
ref.remove(); ref.remove();
} }
//@trigger('paper:image:removed', @slides[url].getId()) # Removes the previous image preventing images from being redrawn over each other repeatedly
} }
} }
this.slides = {}; this.slides = {};
return this.current.slide = null; return this.current.slide = null;
} }
// Switches the tool and thus the functions that get
// called when certain events are fired from Raphael.
// @param {string} tool the tool to turn on
// @return {undefined}
setCurrentTool(tool) { setCurrentTool(tool) {
this.currentTool = tool; this.currentTool = tool;
console.log("setting current tool to", tool); console.log("setting current tool to", tool);
@ -122,6 +169,7 @@ Meteor.WhiteboardPaperModel = (function() {
} }
} }
// Clear all shapes from this paper.
clearShapes() { clearShapes() {
if(this.current.shapes != null) { if(this.current.shapes != null) {
this.current.shapes.forEach(element => { this.current.shapes.forEach(element => {
@ -140,7 +188,7 @@ Meteor.WhiteboardPaperModel = (function() {
} }
createCursor() { createCursor() {
if(this.raphaelObj.w === 100) { if(this.raphaelObj.w === 100) { // on first load: Raphael object is initially tiny
this.cursor = new WhiteboardCursorModel(this.raphaelObj, 0.65); this.cursor = new WhiteboardCursorModel(this.raphaelObj, 0.65);
this.cursor.setRadius(0.65 * this.widthRatio / 100); this.cursor.setRadius(0.65 * this.widthRatio / 100);
} else { } else {
@ -150,14 +198,20 @@ Meteor.WhiteboardPaperModel = (function() {
return this.cursor.draw(); return this.cursor.draw();
} }
// Updated a shape `shape` with the data in `data`.
// TODO: check if the objects exist before calling update, if they don't they should be created
updateShape(shape, data) { updateShape(shape, data) {
return this.current[shape].update(data); return this.current[shape].update(data);
} }
// Make a shape `shape` with the data in `data`.
makeShape(shape, data) { makeShape(shape, data) {
let base, base1, i, len, obj, tool, toolModel; let base, base1, i, len, obj, tool, toolModel;
data.thickness *= this.adjustedWidth / 1000; data.thickness *= this.adjustedWidth / 1000;
tool = null; tool = null;
//TODO pay attention to this array, data in this array slows down the whiteboard
//console.log @current
//console.log @
this.current[shape] = this._createTool(shape); this.current[shape] = this._createTool(shape);
toolModel = this.current[shape]; toolModel = this.current[shape];
tool = this.current[shape].make(data); tool = this.current[shape].make(data);
@ -168,6 +222,8 @@ Meteor.WhiteboardPaperModel = (function() {
this.current.shapes.push(tool); this.current.shapes.push(tool);
this.current.shapeDefinitions.push(toolModel.getDefinition()); this.current.shapeDefinitions.push(toolModel.getDefinition());
} }
//We have a separate case for Poll as it returns an array instead of just one object
if((tool != null) && shape === "poll_result") { if((tool != null) && shape === "poll_result") {
if((base1 = this.current).shapes == null) { if((base1 = this.current).shapes == null) {
base1.shapes = this.raphaelObj.set(); base1.shapes = this.raphaelObj.set();
@ -180,17 +236,23 @@ Meteor.WhiteboardPaperModel = (function() {
} }
} }
// Update the cursor position on screen
// @param {number} x the x value of the cursor as a percentage of the width
// @param {number} y the y value of the cursor as a percentage of the height
moveCursor(x, y) { moveCursor(x, y) {
let cx, cy, ref, ref1, slideHeight, slideWidth; let cx, cy, ref, ref1, slideHeight, slideWidth;
ref = this._currentSlideOffsets(), cx = ref[0], cy = ref[1]; ref = this._currentSlideOffsets(), cx = ref[0], cy = ref[1];
ref1 = this._currentSlideOriginalDimensions(), slideWidth = ref1[0], slideHeight = ref1[1]; ref1 = this._currentSlideOriginalDimensions(), slideWidth = ref1[0], slideHeight = ref1[1];
this.cursor.setPosition(x * slideWidth + cx, y * slideHeight + cy); this.cursor.setPosition(x * slideWidth + cx, y * slideHeight + cy);
//if the slide is zoomed in then move the cursor based on where the viewBox is looking
if((this.viewBoxXpos != null) && (this.viewBoxYPos != null) && (this.viewBoxWidth != null) && (this.viewBoxHeight != null)) { if((this.viewBoxXpos != null) && (this.viewBoxYPos != null) && (this.viewBoxWidth != null) && (this.viewBoxHeight != null)) {
return this.cursor.setPosition(this.viewBoxXpos + x * this.viewBoxWidth, this.viewBoxYPos + y * this.viewBoxHeight); return this.cursor.setPosition(this.viewBoxXpos + x * this.viewBoxWidth, this.viewBoxYPos + y * this.viewBoxHeight);
} }
} }
zoomAndPan(widthRatio, heightRatio, xOffset, yOffset) { zoomAndPan(widthRatio, heightRatio, xOffset, yOffset) {
// console.log "zoomAndPan #{widthRatio} #{heightRatio} #{xOffset} #{yOffset}"
let newHeight, newWidth, newX, newY; let newHeight, newWidth, newX, newY;
newX = -xOffset * 2 * this.adjustedWidth / 100; newX = -xOffset * 2 * this.adjustedWidth / 100;
newY = -yOffset * 2 * this.adjustedHeight / 100; newY = -yOffset * 2 * this.adjustedHeight / 100;
@ -204,6 +266,7 @@ Meteor.WhiteboardPaperModel = (function() {
return this.adjustedHeight = height; return this.adjustedHeight = height;
} }
// Update the dimensions of the container.
_updateContainerDimensions() { _updateContainerDimensions() {
let $container, containerDimensions, ref, ref1; let $container, containerDimensions, ref, ref1;
$container = $('#whiteboard-paper'); $container = $('#whiteboard-paper');
@ -229,6 +292,10 @@ Meteor.WhiteboardPaperModel = (function() {
return this.heightRatio = currentSlideDoc != null ? currentSlideDoc.slide.height_ratio : void 0; return this.heightRatio = currentSlideDoc != null ? currentSlideDoc.slide.height_ratio : void 0;
} }
// Retrieves an image element from the paper.
// The url must be in the slides array.
// @param {string} url the url of the image (must be in slides array)
// @return {Raphael.image} return the image or null if not found
_getImageFromPaper(url) { _getImageFromPaper(url) {
let id; let id;
if(this.slides[url]) { if(this.slides[url]) {
@ -264,6 +331,7 @@ Meteor.WhiteboardPaperModel = (function() {
} }
} }
// Wrapper method to create a tool for the whiteboard
_createTool(type) { _createTool(type) {
let height, model, ref, ref1, ref2, slideHeight, slideWidth, tool, width, xOffset, yOffset; let height, model, ref, ref1, ref2, slideHeight, slideWidth, tool, width, xOffset, yOffset;
switch(type) { switch(type) {
@ -294,6 +362,7 @@ Meteor.WhiteboardPaperModel = (function() {
ref1 = this._currentSlideOffsets(), xOffset = ref1[0], yOffset = ref1[1]; ref1 = this._currentSlideOffsets(), xOffset = ref1[0], yOffset = ref1[1];
ref2 = this._currentSlideDimensions(), width = ref2[0], height = ref2[1]; ref2 = this._currentSlideDimensions(), width = ref2[0], height = ref2[1];
tool = new model(this.raphaelObj); tool = new model(this.raphaelObj);
// TODO: why are the parameters inverted and it works?
tool.setPaperSize(slideHeight, slideWidth); tool.setPaperSize(slideHeight, slideWidth);
tool.setOffsets(xOffset, yOffset); tool.setOffsets(xOffset, yOffset);
tool.setPaperDimensions(width, height); tool.setPaperDimensions(width, height);
@ -303,14 +372,18 @@ Meteor.WhiteboardPaperModel = (function() {
} }
} }
// Adds the base url (the protocol+server part) to `url` if needed.
_slideUrl(url) { _slideUrl(url) {
if(url != null ? url.match(/http[s]?:/) : void 0) { if(url != null ? url.match(/http[s]?:/) : void 0) {
return url; return url;
} else { } else {
return console.log(`The url '${url}'' did not match the expected format of: http/s`); return console.log(`The url '${url}'' did not match the expected format of: http/s`);
//globals.presentationServer + url
} }
} }
//Changes the currently displayed page/slide (if any) with this one
//@param {data} message object containing the "presentation" object
_displayPage(data, originalWidth, originalHeight) { _displayPage(data, originalWidth, originalHeight) {
let _this, boardHeight, boardWidth, currentPresentation, currentSlide, currentSlideCursor, presentationId, ref; let _this, boardHeight, boardWidth, currentPresentation, currentSlide, currentSlideCursor, presentationId, ref;
this.removeAllImagesFromPaper(); this.removeAllImagesFromPaper();
@ -330,7 +403,7 @@ Meteor.WhiteboardPaperModel = (function() {
this.zoomObserver.stop(); this.zoomObserver.stop();
} }
_this = this; _this = this;
this.zoomObserver = currentSlideCursor.observe({ this.zoomObserver = currentSlideCursor.observe({ // watching the current slide changes
changed(newDoc, oldDoc) { changed(newDoc, oldDoc) {
let newRatio, oldRatio, ref1, ref2; let newRatio, oldRatio, ref1, ref2;
if(originalWidth <= originalHeight) { if(originalWidth <= originalHeight) {
@ -357,7 +430,7 @@ Meteor.WhiteboardPaperModel = (function() {
} }
} }
} }
if(_this.raphaelObj === 100) { if(_this.raphaelObj === 100) { // on first load: Raphael object is initially tiny
return _this.cursor.setRadius(0.65 * newDoc.slide.width_ratio / 100); return _this.cursor.setRadius(0.65 * newDoc.slide.width_ratio / 100);
} else { } else {
return _this.cursor.setRadius(6 * newDoc.slide.width_ratio / 100); return _this.cursor.setRadius(6 * newDoc.slide.width_ratio / 100);
@ -365,6 +438,7 @@ Meteor.WhiteboardPaperModel = (function() {
} }
}); });
if(originalWidth <= originalHeight) { if(originalWidth <= originalHeight) {
// square => boardHeight is the shortest side
this.adjustedWidth = boardHeight * originalWidth / originalHeight; this.adjustedWidth = boardHeight * originalWidth / originalHeight;
$('#whiteboard-paper').width(this.adjustedWidth); $('#whiteboard-paper').width(this.adjustedWidth);
this.addImageToPaper(data, this.adjustedWidth, boardHeight); this.addImageToPaper(data, this.adjustedWidth, boardHeight);

View File

@ -5,17 +5,30 @@ let bind = function(fn, me) {
}; };
}; };
// A poll in the whiteboard
this.WhiteboardPollModel = (function() { this.WhiteboardPollModel = (function() {
class WhiteboardPollModel extends WhiteboardToolModel { class WhiteboardPollModel extends WhiteboardToolModel {
constructor(paper1) { constructor(paper1) {
super(paper1); super(paper1);
this.paper = paper1; this.paper = paper1;
this.make = bind(this.make, this); this.make = bind(this.make, this);
// the defintion of this shape, kept so we can redraw the shape whenever needed
// format: x1, y1, x2, y2, stroke color, thickness, fill
this.definition = [0, 0, 0, 0, "#333333", "2px", "#ffffff"]; this.definition = [0, 0, 0, 0, "#333333", "2px", "#ffffff"];
} }
// Creates a polling in the paper
// @param {number} x1 the x value of the top left corner
// @param {number} y1 the y value of the top left corner
// @param {number} x2 the x value of the bottom right corner
// @param {number} y2 the y value of the bottom right corner
// @param {number} thickness the thickness of the object's line(s)
// @param {string} backgroundColor the background color of the base poll rectangle
// @param {number} calcFontSize the default font-size of the text objects
make(startingData) { make(startingData) {
let backgroundColor, barHeight, barWidth, calcFontSize, calculatedData, centerCell, color, height, horizontalPadding, i, k, l, leftCell, m, magicNumber, maxBarWidth, maxDigitWidth, maxLeftWidth, maxLineHeight, maxNumVotes, maxRightWidth, n, objects, percResult, ref, ref1, ref2, ref3, ref4, ref5, rightCell, svgNSi, tempSpanEl, tempTextNode, textArray, thickness, verticalPadding, votesTotal, width, x, x1, x2, xBar, xLeft, xNumVotes, xNumVotesDefault, xNumVotesMovedRight, xRight, y, y1, y2, yBar, yLeft, yNumVotes, yRight; let backgroundColor, barHeight, barWidth, calcFontSize, calculatedData, centerCell, color, height, horizontalPadding, i, k, l, leftCell, m, magicNumber, maxBarWidth, maxDigitWidth, maxLeftWidth, maxLineHeight, maxNumVotes, maxRightWidth, n, objects, percResult, ref, ref1, ref2, ref3, ref4, ref5, rightCell, svgNSi, tempSpanEl, tempTextNode, textArray, thickness, verticalPadding, votesTotal, width, x, x1, x2, xBar, xLeft, xNumVotes, xNumVotesDefault, xNumVotesMovedRight, xRight, y, y1, y2, yBar, yLeft, yNumVotes, yRight;
//data needed to create the first base rectangle filled with white color
x1 = startingData.points[0]; x1 = startingData.points[0];
y1 = startingData.points[1]; y1 = startingData.points[1];
x2 = startingData.points[2] + startingData.points[0] - 0.001; x2 = startingData.points[2] + startingData.points[0] - 0.001;
@ -28,7 +41,10 @@ this.WhiteboardPollModel = (function() {
votesTotal = 0; votesTotal = 0;
maxNumVotes = 0; maxNumVotes = 0;
textArray = []; textArray = [];
//creating an array of text objects for the labels, percentages and number inside line bars
if(startingData.result != null) { if(startingData.result != null) {
//counting the total number of votes and finding the biggest number of votes
for(i = k = 0, ref = startingData.result.length - 1; 0 <= ref ? k <= ref : k >= ref; i = 0 <= ref ? ++k : --k) { for(i = k = 0, ref = startingData.result.length - 1; 0 <= ref ? k <= ref : k >= ref; i = 0 <= ref ? ++k : --k) {
votesTotal += startingData.result[i].num_votes; votesTotal += startingData.result[i].num_votes;
if(maxNumVotes < startingData.result[i].num_votes) { if(maxNumVotes < startingData.result[i].num_votes) {
@ -36,6 +52,7 @@ this.WhiteboardPollModel = (function() {
} }
textArray[i] = []; textArray[i] = [];
} }
//filling the array with proper text objects to display
for(i = l = 0, ref1 = startingData.result.length - 1; 0 <= ref1 ? l <= ref1 : l >= ref1; i = 0 <= ref1 ? ++l : --l) { for(i = l = 0, ref1 = startingData.result.length - 1; 0 <= ref1 ? l <= ref1 : l >= ref1; i = 0 <= ref1 ? ++l : --l) {
textArray[i].push(startingData.result[i].key, `${startingData.result[i].num_votes}`); textArray[i].push(startingData.result[i].key, `${startingData.result[i].num_votes}`);
if(votesTotal === 0) { if(votesTotal === 0) {
@ -46,16 +63,26 @@ this.WhiteboardPollModel = (function() {
} }
} }
} }
//if coordinates are reversed - change them back
if(x2 < x1) { if(x2 < x1) {
ref2 = [x2, x1], x1 = ref2[0], x2 = ref2[1]; ref2 = [x2, x1], x1 = ref2[0], x2 = ref2[1];
} }
if(y2 < y1) { if(y2 < y1) {
ref3 = [y2, y1], y1 = ref3[0], y2 = ref3[1]; ref3 = [y2, y1], y1 = ref3[0], y2 = ref3[1];
} }
//Params:
//x - the actual calculated x value of the top left corner of the polling area
//y - the actual calculated y value of the top left corner of the polling area
//width - the width of the polling area
//height - the height of the polling area
x = x1 * this.gw + this.xOffset; x = x1 * this.gw + this.xOffset;
y = y1 * this.gh + this.yOffset; y = y1 * this.gh + this.yOffset;
width = (x2 * this.gw + this.xOffset) - x; width = (x2 * this.gw + this.xOffset) - x;
height = (y2 * this.gh + this.yOffset) - y; height = (y2 * this.gh + this.yOffset) - y;
//creating a base outer rectangle
this.obj = this.paper.rect(x, y, width, height, 0); this.obj = this.paper.rect(x, y, width, height, 0);
this.obj.attr("fill", backgroundColor); this.obj.attr("fill", backgroundColor);
this.obj.attr("stroke-width", 0); this.obj.attr("stroke-width", 0);
@ -63,10 +90,14 @@ this.WhiteboardPollModel = (function() {
shape: "poll_result", shape: "poll_result",
data: [x1, y1, x2, y2, this.obj.attrs["stroke"], this.obj.attrs["stroke-width"], this.obj.attrs["fill"]] data: [x1, y1, x2, y2, this.obj.attrs["stroke"], this.obj.attrs["stroke-width"], this.obj.attrs["fill"]]
}; };
//recalculated coordinates, width and height for the inner rectangle
width = width * 0.95; width = width * 0.95;
height = height - width * 0.05; height = height - width * 0.05;
x = x + width * 0.025; x = x + width * 0.025;
y = y + width * 0.025; y = y + width * 0.025;
//creating a base inner rectangle
this.obj1 = this.paper.rect(x, y, width, height, 0); this.obj1 = this.paper.rect(x, y, width, height, 0);
this.obj1.attr("stroke", "#333333"); this.obj1.attr("stroke", "#333333");
this.obj1.attr("fill", backgroundColor); this.obj1.attr("fill", backgroundColor);
@ -75,6 +106,8 @@ this.WhiteboardPollModel = (function() {
shape: "poll_result", shape: "poll_result",
data: [x1, y1, x2, y2, this.obj.attrs["stroke"], this.obj1.attrs["stroke-width"], this.obj1.attrs["fill"]] data: [x1, y1, x2, y2, this.obj.attrs["stroke"], this.obj1.attrs["stroke-width"], this.obj1.attrs["fill"]]
}; };
//Calculating a proper font-size, and the maximum widht and height of the objects
calculatedData = calculateFontAndWidth(textArray, calcFontSize, width, height, x, y); calculatedData = calculateFontAndWidth(textArray, calcFontSize, width, height, x, y);
calcFontSize = calculatedData[0]; calcFontSize = calculatedData[0];
maxLeftWidth = calculatedData[1]; maxLeftWidth = calculatedData[1];
@ -84,6 +117,8 @@ this.WhiteboardPollModel = (function() {
maxBarWidth = width * 0.9 - maxLeftWidth - maxRightWidth; maxBarWidth = width * 0.9 - maxLeftWidth - maxRightWidth;
barHeight = height * 0.75 / textArray.length; barHeight = height * 0.75 / textArray.length;
svgNSi = "http://www.w3.org/2000/svg"; svgNSi = "http://www.w3.org/2000/svg";
//Initializing a text element for further calculations and for the left column of keys
this.obj2 = this.paper.text(x, y, ""); this.obj2 = this.paper.text(x, y, "");
this.obj2.attr({ this.obj2.attr({
"fill": "#333333", "fill": "#333333",
@ -96,6 +131,8 @@ this.WhiteboardPollModel = (function() {
while((leftCell != null) && leftCell.hasChildNodes()) { while((leftCell != null) && leftCell.hasChildNodes()) {
leftCell.removeChild(leftCell.firstChild); leftCell.removeChild(leftCell.firstChild);
} }
//Initializing a text element for the right column of percentages
this.obj3 = this.paper.text(x, y, ""); this.obj3 = this.paper.text(x, y, "");
this.obj3.attr({ this.obj3.attr({
"fill": "#333333", "fill": "#333333",
@ -108,19 +145,39 @@ this.WhiteboardPollModel = (function() {
while((rightCell != null) && rightCell.hasChildNodes()) { while((rightCell != null) && rightCell.hasChildNodes()) {
rightCell.removeChild(rightCell.firstChild); rightCell.removeChild(rightCell.firstChild);
} }
//setting a font size for the text elements on the left and on the right
leftCell.style['font-size'] = calcFontSize; leftCell.style['font-size'] = calcFontSize;
rightCell.style['font-size'] = calcFontSize; rightCell.style['font-size'] = calcFontSize;
//Horizontal padding
horizontalPadding = width * 0.1 / 4; horizontalPadding = width * 0.1 / 4;
//Vertical padding
verticalPadding = height * 0.25 / (textArray.length + 1); verticalPadding = height * 0.25 / (textArray.length + 1);
//*****************************************************************************************************
//******************************************MAGIC NUMBER***********************************************
//There is no automatic vertical centering in SVG.
//To center the text element we have to move it down by the half of its height.
//But every text element has its own padding by default.
//The height we receive by calling getBBox() includes padding, but the anchor point doesn't consider it.
//This way the text element is moved down a little bit too much and we have to move it up a bit.
//Number 3.5 seems to work fine.
// Oleksandr Zhurbenko. August 19, 2015
magicNumber = 3.5; magicNumber = 3.5;
//*****************************************************************************************************
//Initial coordinates of the key column
yLeft = y + verticalPadding + barHeight / 2 - magicNumber; yLeft = y + verticalPadding + barHeight / 2 - magicNumber;
xLeft = x + horizontalPadding + 1; xLeft = x + horizontalPadding + 1;
//Initial coordinates of the line bar column
xBar = x + maxLeftWidth + horizontalPadding * 2; xBar = x + maxLeftWidth + horizontalPadding * 2;
yBar = y + verticalPadding; yBar = y + verticalPadding;
//Initial coordinates of the percentage column
yRight = y + verticalPadding + barHeight / 2 - magicNumber; yRight = y + verticalPadding + barHeight / 2 - magicNumber;
xRight = x + horizontalPadding * 3 + maxLeftWidth + maxRightWidth + maxBarWidth + 1; xRight = x + horizontalPadding * 3 + maxLeftWidth + maxRightWidth + maxBarWidth + 1;
objects = [this.obj, this.obj1, this.obj2, this.obj3]; objects = [this.obj, this.obj1, this.obj2, this.obj3];
for(i = m = 0, ref4 = textArray.length - 1; 0 <= ref4 ? m <= ref4 : m >= ref4; i = 0 <= ref4 ? ++m : --m) { for(i = m = 0, ref4 = textArray.length - 1; 0 <= ref4 ? m <= ref4 : m >= ref4; i = 0 <= ref4 ? ++m : --m) {
//Adding an element to the left column
tempSpanEl = document.createElementNS(svgNSi, "tspan"); tempSpanEl = document.createElementNS(svgNSi, "tspan");
tempSpanEl.setAttributeNS(null, "x", xLeft); tempSpanEl.setAttributeNS(null, "x", xLeft);
tempSpanEl.setAttributeNS(null, "y", yLeft); tempSpanEl.setAttributeNS(null, "y", yLeft);
@ -128,6 +185,8 @@ this.WhiteboardPollModel = (function() {
tempTextNode = document.createTextNode(textArray[i][0]); tempTextNode = document.createTextNode(textArray[i][0]);
tempSpanEl.appendChild(tempTextNode); tempSpanEl.appendChild(tempTextNode);
leftCell.appendChild(tempSpanEl); leftCell.appendChild(tempSpanEl);
//drawing a black graph bar
if(maxNumVotes === 0 || startingData.result[i].num_votes === 0) { if(maxNumVotes === 0 || startingData.result[i].num_votes === 0) {
barWidth = 2; barWidth = 2;
} else { } else {
@ -138,6 +197,8 @@ this.WhiteboardPollModel = (function() {
this.obj4.attr("fill", "#333333"); this.obj4.attr("fill", "#333333");
this.obj4.attr("stroke-width", zoomStroke(formatThickness(0))); this.obj4.attr("stroke-width", zoomStroke(formatThickness(0)));
objects.push(this.obj4); objects.push(this.obj4);
//Adding an element to the right column
tempSpanEl = document.createElementNS(svgNSi, "tspan"); tempSpanEl = document.createElementNS(svgNSi, "tspan");
tempSpanEl.setAttributeNS(null, "x", xRight); tempSpanEl.setAttributeNS(null, "x", xRight);
tempSpanEl.setAttributeNS(null, "y", yRight); tempSpanEl.setAttributeNS(null, "y", yRight);
@ -145,10 +206,14 @@ this.WhiteboardPollModel = (function() {
tempTextNode = document.createTextNode(textArray[i][2]); tempTextNode = document.createTextNode(textArray[i][2]);
tempSpanEl.appendChild(tempTextNode); tempSpanEl.appendChild(tempTextNode);
rightCell.appendChild(tempSpanEl); rightCell.appendChild(tempSpanEl);
//changing the Y coordinate for all the objects
yBar = yBar + barHeight + verticalPadding; yBar = yBar + barHeight + verticalPadding;
yLeft = yLeft + barHeight + verticalPadding; yLeft = yLeft + barHeight + verticalPadding;
yRight = yRight + barHeight + verticalPadding; yRight = yRight + barHeight + verticalPadding;
} }
//Initializing a text element for the number of votes text field inside the line bar
this.obj5 = this.paper.text(x, y, ""); this.obj5 = this.paper.text(x, y, "");
this.obj5.attr({ this.obj5.attr({
"fill": "#333333", "fill": "#333333",
@ -159,10 +224,14 @@ this.WhiteboardPollModel = (function() {
while((centerCell != null) && centerCell.hasChildNodes()) { while((centerCell != null) && centerCell.hasChildNodes()) {
centerCell.removeChild(centerCell.firstChild); centerCell.removeChild(centerCell.firstChild);
} }
//Initial coordinates of the text inside the bar column
xNumVotesDefault = x + maxLeftWidth + horizontalPadding * 2; xNumVotesDefault = x + maxLeftWidth + horizontalPadding * 2;
xNumVotesMovedRight = xNumVotesDefault + barWidth / 2 + horizontalPadding + maxDigitWidth / 2; xNumVotesMovedRight = xNumVotesDefault + barWidth / 2 + horizontalPadding + maxDigitWidth / 2;
yNumVotes = y + verticalPadding - magicNumber; yNumVotes = y + verticalPadding - magicNumber;
color = "white"; color = "white";
//Drawing the text element with the number of votes inside of the black line bars
//Or outside if a line bar is too small
for(i = n = 0, ref5 = textArray.length - 1; 0 <= ref5 ? n <= ref5 : n >= ref5; i = 0 <= ref5 ? ++n : --n) { for(i = n = 0, ref5 = textArray.length - 1; 0 <= ref5 ? n <= ref5 : n >= ref5; i = 0 <= ref5 ? ++n : --n) {
if(maxNumVotes === 0 || startingData.result[i].num_votes === 0) { if(maxNumVotes === 0 || startingData.result[i].num_votes === 0) {
barWidth = 2; barWidth = 2;
@ -190,6 +259,7 @@ this.WhiteboardPollModel = (function() {
return objects; return objects;
} }
// Update the poll dimensions. Does nothing.
update(startingData) {} update(startingData) {}
} }
@ -199,8 +269,12 @@ this.WhiteboardPollModel = (function() {
calculateFontAndWidth = function(textArray, calcFontSize, width, height, x, y) { calculateFontAndWidth = function(textArray, calcFontSize, width, height, x, y) {
let calculatedData, flag, i, j, k, l, len, line, m, maxDigitWidth, maxLeftWidth, maxLineHeight, maxLineWidth, maxRightWidth, ref, ref1, spanHeight, spanWidth, test; let calculatedData, flag, i, j, k, l, len, line, m, maxDigitWidth, maxLeftWidth, maxLineHeight, maxLineWidth, maxRightWidth, ref, ref1, spanHeight, spanWidth, test;
calculatedData = []; calculatedData = [];
//maximum line width can be either 1/3 of the line or 40px
//maximum line height is 75% of the initial size of the box divided by the number of lines
maxLineWidth = width / 3; maxLineWidth = width / 3;
maxLineHeight = height * 0.75 / (textArray != null ? textArray.length : void 0); maxLineHeight = height * 0.75 / (textArray != null ? textArray.length : void 0);
//calculating a proper font-size
flag = true; flag = true;
while(flag) { while(flag) {
flag = false; flag = false;
@ -217,6 +291,8 @@ calculateFontAndWidth = function(textArray, calcFontSize, width, height, x, y) {
} }
} }
calculatedData.push(calcFontSize); calculatedData.push(calcFontSize);
//looking for a maximum width and height of the left and right text elements
maxLeftWidth = 0; maxLeftWidth = 0;
maxRightWidth = 0; maxRightWidth = 0;
maxLineHeight = 0; maxLineHeight = 0;

View File

@ -4,15 +4,24 @@ const bind = function(fn, me) {
}; };
}; };
// A rectangle in the whiteboard
this.WhiteboardRectModel = (function() { this.WhiteboardRectModel = (function() {
class WhiteboardRectModel extends WhiteboardToolModel{ class WhiteboardRectModel extends WhiteboardToolModel{
constructor(paper) { constructor(paper) {
super(paper); super(paper);
this.paper = paper; this.paper = paper;
this.make = bind(this.make, this); this.make = bind(this.make, this);
// the defintion of this shape, kept so we can redraw the shape whenever needed
// format: x1, y1, x2, y2, stroke color, thickness
this.definition = [0, 0, 0, 0, "#000", "0px"]; this.definition = [0, 0, 0, 0, "#000", "0px"];
} }
// Creates a rectangle in the paper
// @param {number} x the x value of the top left corner
// @param {number} y the y value of the top left corner
// @param {string} colour the colour of the object
// @param {number} thickness the thickness of the object's line(s)
make(startingData) { make(startingData) {
let color, thickness, x, y; let color, thickness, x, y;
x = startingData.points[0]; x = startingData.points[0];
@ -29,6 +38,12 @@ this.WhiteboardRectModel = (function() {
return this.obj; return this.obj;
} }
// Update the rectangle dimensions
// @param {number} x1 the x value of the top left corner
// @param {number} y1 the y value of the top left corner
// @param {number} x2 the x value of the bottom right corner
// @param {number} y2 the y value of the bottom right corner
// @param {boolean} square (draw a square or not)
update(startingData) { update(startingData) {
let height, ref, ref1, reversed, square, width, x, x1, x2, y, y1, y2; let height, ref, ref1, reversed, square, width, x, x1, x2, y, y1, y2;
x1 = startingData.points[0]; x1 = startingData.points[0];
@ -45,7 +60,7 @@ this.WhiteboardRectModel = (function() {
reversed = true; reversed = true;
} }
if(square) { if(square) {
if(reversed) { if(reversed) { //if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * this.gw / this.gh; y1 = y2 - (x2 - x1) * this.gw / this.gh;
} else { } else {
y2 = y1 + (x2 - x1) * this.gw / this.gh; y2 = y1 + (x2 - x1) * this.gw / this.gh;
@ -55,6 +70,7 @@ this.WhiteboardRectModel = (function() {
y = y1 * this.gh + this.yOffset; y = y1 * this.gh + this.yOffset;
width = (x2 * this.gw + this.xOffset) - x; width = (x2 * this.gw + this.xOffset) - x;
height = (y2 * this.gh + this.yOffset) - y; height = (y2 * this.gh + this.yOffset) - y;
//if !square
this.obj.attr({ this.obj.attr({
x: x, x: x,
y: y, y: y,
@ -69,6 +85,8 @@ this.WhiteboardRectModel = (function() {
width: width width: width
height: width height: width
*/ */
// we need to update all these values, specially for when shapes are drawn backwards
this.definition.data[0] = x1; this.definition.data[0] = x1;
this.definition.data[1] = y1; this.definition.data[1] = y1;
this.definition.data[2] = x2; this.definition.data[2] = x2;
@ -76,6 +94,13 @@ this.WhiteboardRectModel = (function() {
} }
} }
// Draw a rectangle on the paper
// @param {number} x1 the x value of the top left corner
// @param {number} y1 the y value of the top left corner
// @param {number} x2 the x value of the bottom right corner
// @param {number} y2 the y value of the bottom right corner
// @param {string} colour the colour of the object
// @param {number} thickness the thickness of the object's line(s)
draw(x1, y1, x2, y2, colour, thickness) { draw(x1, y1, x2, y2, colour, thickness) {
let r, ref, ref1, x, y; let r, ref, ref1, x, y;
if(x2 < x1) { if(x2 < x1) {
@ -91,9 +116,58 @@ this.WhiteboardRectModel = (function() {
return r; return r;
} }
dragOnStart(x, y) {} // Creating a rectangle has started
dragOnMove(dx, dy, x, y, e) {} // @param {number} x the x value of cursor at the time in relation to the left side of the browser
dragOnEnd(e) {} // @param {number} y the y value of cursor at the time in relation to the top of the browser
// TODO: moved here but not finished
dragOnStart(x, y) {
// sx = (@paperWidth - @gw) / 2
// sy = (@paperHeight - @gh) / 2
// // find the x and y values in relation to the whiteboard
// @cx2 = (x - @containerOffsetLeft - sx + @xOffset) / @paperWidth
// @cy2 = (y - @containerOffsetTop - sy + @yOffset) / @paperHeight
// globals.connection.emitMakeShape "rect",
// [ @cx2, @cy2, @currentColour, @currentThickness ]
}
// Adjusting rectangle continues
// @param {number} dx the difference in the x value at the start as opposed to the x value now
// @param {number} dy the difference in the y value at the start as opposed to the y value now
// @param {number} x the x value of cursor at the time in relation to the left side of the browser
// @param {number} y the y value of cursor at the time in relation to the top of the browser
// @param {Event} e the mouse event
// TODO: moved here but not finished
dragOnMove(dx, dy, x, y, e) {
// // if shift is pressed, make it a square
// dy = dx if @shiftPressed
// dx = dx / @paperWidth
// dy = dy / @paperHeight
// // adjust for negative values as well
// if dx >= 0
// x1 = @cx2
// else
// x1 = @cx2 + dx
// dx = -dx
// if dy >= 0
// y1 = @cy2
// else
// y1 = @cy2 + dy
// dy = -dy
// globals.connection.emitUpdateShape "rect", [ x1, y1, dx, dy ]
}
// When rectangle finished being drawn
// @param {Event} e the mouse event
// TODO: moved here but not finished
dragOnEnd(e) {
// if @obj?
// attrs = @obj.attrs
// if attrs?
// globals.connection.emitPublishShape "rect",
// [ attrs.x / @gw, attrs.y / @gh, attrs.width / @gw, attrs.height / @gh,
// @currentColour, @currentThickness ]
// @obj = null
}
} }
return WhiteboardRectModel; return WhiteboardRectModel;

View File

@ -1,5 +1,8 @@
// A slide in the whiteboard
this.WhiteboardSlideModel = (function() { this.WhiteboardSlideModel = (function() {
class WhiteboardSlideModel { class WhiteboardSlideModel {
// TODO: check if we really need original and display width and heights separate or if they can be the same
constructor( constructor(
id, id,
url, url,

View File

@ -1,3 +1,4 @@
// A text in the whiteboard
this.WhiteboardTextModel = (function() { this.WhiteboardTextModel = (function() {
let checkDashPosition, checkWidth; let checkDashPosition, checkWidth;
@ -5,10 +6,15 @@ this.WhiteboardTextModel = (function() {
constructor(paper) { constructor(paper) {
super(paper); super(paper);
this.paper = paper; this.paper = paper;
// the defintion of this shape, kept so we can redraw the shape whenever needed
// format: x, y, width, height, colour, fontSize, calcFontSize, text
this.definition = [0, 0, 0, 0, "#000", 0, 0, ""]; this.definition = [0, 0, 0, 0, "#000", 0, 0, ""];
} }
// Make a text on the whiteboard
make(startingData) { make(startingData) {
//console.log "making text:" + JSON.stringify startingData
let calcFontSize, colour, fontSize, height, text, width, x, y; let calcFontSize, colour, fontSize, height, text, width, x, y;
x = startingData.x; x = startingData.x;
y = startingData.y; y = startingData.y;
@ -22,21 +28,26 @@ this.WhiteboardTextModel = (function() {
shape: "text", shape: "text",
data: [x, y, width, height, colour, fontSize, calcFontSize, text] data: [x, y, width, height, colour, fontSize, calcFontSize, text]
}; };
//calcFontSize = (calcFontSize/100 * @gh)
x = (x * this.gw) + this.xOffset; x = (x * this.gw) + this.xOffset;
y = (y * this.gh) + this.yOffset + calcFontSize; y = (y * this.gh) + this.yOffset + calcFontSize;
width = width / 100 * this.gw; width = width / 100 * this.gw;
this.obj = this.paper.text(x / 100, y / 100, ""); this.obj = this.paper.text(x / 100, y / 100, "");
this.obj.attr({ this.obj.attr({
"fill": colour, "fill": colour,
"font-family": "Arial", "font-family": "Arial", // TODO: make dynamic
"font-size": calcFontSize "font-size": calcFontSize
}); });
this.obj.node.style["text-anchor"] = "start"; this.obj.node.style["text-anchor"] = "start"; // force left align
this.obj.node.style["textAnchor"] = "start"; this.obj.node.style["textAnchor"] = "start"; // for firefox, 'cause they like to be different
return this.obj; return this.obj;
} }
// Update text shape drawn
// @param {object} the object containing the shape info
update(startingData) { update(startingData) {
//console.log "updating text" + JSON.stringify startingData
let calcFontSize, cell, colour, computedTextLength, cumulY, curNumChars, dashArray, dashFound, dy, fontSize, height, i, indexPos, line, maxWidth, myText, myTextNode, result, svgNS, tempText, tspanEl, word, words, x, y; let calcFontSize, cell, colour, computedTextLength, cumulY, curNumChars, dashArray, dashFound, dy, fontSize, height, i, indexPos, line, maxWidth, myText, myTextNode, result, svgNS, tempText, tspanEl, word, words, x, y;
x = startingData.x; x = startingData.x;
y = startingData.y; y = startingData.y;
@ -53,13 +64,18 @@ this.WhiteboardTextModel = (function() {
maxWidth = maxWidth / 100 * this.gw; maxWidth = maxWidth / 100 * this.gw;
this.obj.attr({ this.obj.attr({
"fill": colour, "fill": colour,
"font-family": "Arial", "font-family": "Arial", // TODO: make dynamic
"font-size": calcFontSize "font-size": calcFontSize
}); });
cell = this.obj.node; cell = this.obj.node;
while((cell != null) && cell.hasChildNodes()) { while((cell != null) && cell.hasChildNodes()) {
cell.removeChild(cell.firstChild); cell.removeChild(cell.firstChild);
} }
// used code from textFlow lib http://www.carto.net/papers/svg/textFlow/
// but had to merge it here because "cell" was bigger than what the stack could take
//extract and add line breaks for start
dashArray = new Array(); dashArray = new Array();
dashFound = true; dashFound = true;
indexPos = 0; indexPos = 0;
@ -68,12 +84,14 @@ this.WhiteboardTextModel = (function() {
while(dashFound === true) { while(dashFound === true) {
result = myText.indexOf("-", indexPos); result = myText.indexOf("-", indexPos);
if(result === -1) { if(result === -1) {
//could not find a dash
dashFound = false; dashFound = false;
} else { } else {
dashArray.push(result); dashArray.push(result);
indexPos = result + 1; indexPos = result + 1;
} }
} }
//split the text at all spaces and dashes
words = myText.split(/[\s-]/); words = myText.split(/[\s-]/);
line = ""; line = "";
dy = 0; dy = 0;
@ -82,6 +100,8 @@ this.WhiteboardTextModel = (function() {
myTextNode = void 0; myTextNode = void 0;
tspanEl = void 0; tspanEl = void 0;
i = 0; i = 0;
//checking if any of the words exceed the width of a textBox
words = checkWidth(words, maxWidth, x, dy, cell); words = checkWidth(words, maxWidth, x, dy, cell);
while(i < words.length) { while(i < words.length) {
word = words[i]; word = words[i];
@ -89,13 +109,15 @@ this.WhiteboardTextModel = (function() {
if(computedTextLength > maxWidth || i === 0) { if(computedTextLength > maxWidth || i === 0) {
if(computedTextLength > maxWidth) { if(computedTextLength > maxWidth) {
tempText = tspanEl.firstChild.nodeValue; tempText = tspanEl.firstChild.nodeValue;
tempText = tempText.slice(0, tempText.length - words[i - 1].length - 2); tempText = tempText.slice(0, tempText.length - words[i - 1].length - 2); //the -2 is because we also strip off white space
tspanEl.firstChild.nodeValue = tempText; tspanEl.firstChild.nodeValue = tempText;
} }
//setting up coordinates for the first line of text
if(i === 0) { if(i === 0) {
dy = calcFontSize; dy = calcFontSize;
cumulY += dy; cumulY += dy;
} }
//alternatively one could use textLength and lengthAdjust, however, currently this is not too well supported in SVG UA's
tspanEl = document.createElementNS(svgNS, "tspan"); tspanEl = document.createElementNS(svgNS, "tspan");
tspanEl.setAttributeNS(null, "x", x); tspanEl.setAttributeNS(null, "x", x);
tspanEl.setAttributeNS(null, "dy", dy); tspanEl.setAttributeNS(null, "dy", dy);
@ -140,6 +162,7 @@ this.WhiteboardTextModel = (function() {
} }
} }
//this function checks if there should be a dash at the given position, instead of a blank
checkDashPosition = function(dashArray, pos) { checkDashPosition = function(dashArray, pos) {
let i, result; let i, result;
result = false; result = false;
@ -153,6 +176,8 @@ this.WhiteboardTextModel = (function() {
return result; return result;
}; };
//this function checks the width of the word and adds a " " if the width of the word exceeds the width of the textbox
//in order for the word to be split and shown properly
checkWidth = function(words, maxWidth, x, dy, cell) { checkWidth = function(words, maxWidth, x, dy, cell) {
let count, num, partWord, start, str, svgNSi, temp, temp3, tempArray, tempSpanEl, tempTextNode, tempWord; let count, num, partWord, start, str, svgNSi, temp, temp3, tempArray, tempSpanEl, tempTextNode, tempWord;
count = 0; count = 0;
@ -166,16 +191,21 @@ this.WhiteboardTextModel = (function() {
tempTextNode = document.createTextNode(str); tempTextNode = document.createTextNode(str);
tempSpanEl.appendChild(tempTextNode); tempSpanEl.appendChild(tempTextNode);
num = 0; num = 0;
//creating a textNode and adding it to the cell to check the width
while(num < temp.length) { while(num < temp.length) {
tempSpanEl.firstChild.nodeValue = temp[num]; tempSpanEl.firstChild.nodeValue = temp[num];
cell.appendChild(tempSpanEl); cell.appendChild(tempSpanEl);
//if width is bigger than maxWidth + whitespace between textBox borders and a word
if(tempSpanEl.getComputedTextLength() + 10 > maxWidth) { if(tempSpanEl.getComputedTextLength() + 10 > maxWidth) {
tempWord = temp[num]; tempWord = temp[num];
cell.removeChild(cell.firstChild); cell.removeChild(cell.firstChild);
//initializing temp variables
count = 1; count = 1;
start = 0; start = 0;
partWord = `${tempWord[0]}`; partWord = `${tempWord[0]}`;
tempArray = []; tempArray = [];
//check the width by increasing the word character by character
while(count < tempWord.length) { while(count < tempWord.length) {
partWord += tempWord[count]; partWord += tempWord[count];
tempSpanEl.firstChild.nodeValue = partWord; tempSpanEl.firstChild.nodeValue = partWord;

View File

@ -1,11 +1,20 @@
// A triangle in the whiteboard
this.WhiteboardTriangleModel = (function() { this.WhiteboardTriangleModel = (function() {
class WhiteboardTriangleModel extends WhiteboardToolModel { class WhiteboardTriangleModel extends WhiteboardToolModel {
constructor(paper) { constructor(paper) {
super(paper); super(paper);
this.paper = paper; this.paper = paper;
// the defintion of this shape, kept so we can redraw the shape whenever needed
// format: x1, y1, x2, y2, stroke color, thickness
this.definition = [0, 0, 0, 0, "#000", "0px"]; this.definition = [0, 0, 0, 0, "#000", "0px"];
} }
// Make a triangle on the whiteboard
// @param {[type]} x the x value of the top left corner
// @param {[type]} y the y value of the top left corner
// @param {string} colour the colour of the object
// @param {number} thickness the thickness of the object's line(s)
make(info) { make(info) {
let color, path, thickness, x, y; let color, path, thickness, x, y;
if((info != null ? info.points : void 0) != null) { if((info != null ? info.points : void 0) != null) {
@ -25,6 +34,11 @@ this.WhiteboardTriangleModel = (function() {
return this.obj; return this.obj;
} }
// Update triangle drawn
// @param {number} x1 the x value of the top left corner
// @param {number} y1 the y value of the top left corner
// @param {number} x2 the x value of the bottom right corner
// @param {number} y2 the y value of the bottom right corner
update(info) { update(info) {
let path, ref, x1, x2, xBottomLeft, xBottomRight, xTop, y1, y2, yBottomLeft, yBottomRight, yTop; let path, ref, x1, x2, xBottomLeft, xBottomRight, xTop, y1, y2, yBottomLeft, yBottomRight, yTop;
if((info != null ? info.points : void 0) != null) { if((info != null ? info.points : void 0) != null) {
@ -46,6 +60,13 @@ this.WhiteboardTriangleModel = (function() {
} }
} }
// Draw a triangle on the whiteboard
// @param {number} x1 the x value of the top left corner
// @param {number} y1 the y value of the top left corner
// @param {number} x2 the x value of the bottom right corner
// @param {number} y2 the y value of the bottom right corner
// @param {string} colour the colour of the object
// @param {number} thickness the thickness of the object's line(s)
draw(x1, y1, x2, y2, colour, thickness) { draw(x1, y1, x2, y2, colour, thickness) {
let path, ref, triangle, xBottomLeft, xBottomRight, xTop, yBottomLeft, yBottomRight, yTop; let path, ref, triangle, xBottomLeft, xBottomRight, xTop, yBottomLeft, yBottomRight, yTop;
ref = this._getCornersFromPoints(x1, y1, x2, y2), xTop = ref[0], yTop = ref[1], xBottomLeft = ref[2], yBottomLeft = ref[3], xBottomRight = ref[4], yBottomRight = ref[5]; ref = this._getCornersFromPoints(x1, y1, x2, y2), xTop = ref[0], yTop = ref[1], xBottomLeft = ref[2], yBottomLeft = ref[3], xBottomRight = ref[4], yBottomRight = ref[5];
@ -74,6 +95,10 @@ this.WhiteboardTriangleModel = (function() {
return `M${xTop},${yTop},${xBottomLeft},${yBottomLeft},${xBottomRight},${yBottomRight}z`; return `M${xTop},${yTop},${xBottomLeft},${yBottomLeft},${xBottomRight},${yBottomRight}z`;
} }
// Scales a triangle path string to fit within a width and height of the new paper size
// @param {number} w width of the shape as a percentage of the original width
// @param {number} h height of the shape as a percentage of the original height
// @return {string} the path string after being manipulated to new paper size
_scaleTrianglePath(string, w, h, xOffset, yOffset) { _scaleTrianglePath(string, w, h, xOffset, yOffset) {
let j, len, path, points; let j, len, path, points;
if(xOffset == null) { if(xOffset == null) {
@ -86,6 +111,8 @@ this.WhiteboardTriangleModel = (function() {
points = string.match(/(\d+[.]?\d*)/g); points = string.match(/(\d+[.]?\d*)/g);
len = points.length; len = points.length;
j = 0; j = 0;
// go through each point and multiply it by the new height and width
path = "M"; path = "M";
while(j < len) { while(j < len) {
if(j !== 0) { if(j !== 0) {

View File

@ -1,7 +1,12 @@
// TODO: should be split on server and client side
// // Global configurations file
let config, file, ref, transports, winston; let config, file, ref, transports, winston;
config = {}; config = {};
// Default global variables
config.appName = 'BigBlueButton HTML5 Client'; config.appName = 'BigBlueButton HTML5 Client';
config.bbbServerVersion = '1.0-beta'; config.bbbServerVersion = '1.0-beta';
@ -20,20 +25,30 @@ config.maxChatLength = 140;
config.lockOnJoin = true; config.lockOnJoin = true;
//// Application configurations
config.app = {}; config.app = {};
//default font sizes for mobile / desktop
config.app.mobileFont = 16; config.app.mobileFont = 16;
config.app.desktopFont = 14; config.app.desktopFont = 14;
// Will offer the user to join the audio when entering the meeting
config.app.autoJoinAudio = false; config.app.autoJoinAudio = false;
config.app.listenOnly = false; config.app.listenOnly = false;
config.app.skipCheck = false; config.app.skipCheck = false;
// The amount of time the client will wait before making another call to successfully hangup the WebRTC conference call
config.app.WebRTCHangupRetryInterval = 2000; config.app.WebRTCHangupRetryInterval = 2000;
// Configs for redis
config.redis = {}; config.redis = {};
config.redis.host = "127.0.0.1"; config.redis.host = "127.0.0.1";
@ -64,11 +79,14 @@ config.redis.channels.toBBBApps.whiteboard = "bigbluebutton:to-bbb-apps:whiteboa
config.redis.channels.toBBBApps.polling = "bigbluebutton:to-bbb-apps:polling"; config.redis.channels.toBBBApps.polling = "bigbluebutton:to-bbb-apps:polling";
// Logging
config.log = {}; config.log = {};
if(Meteor.isServer) { if(Meteor.isServer) {
config.log.path = (typeof process !== "undefined" && process !== null ? (ref = process.env) != null ? ref.NODE_ENV : void 0 : void 0) === "production" ? "/var/log/bigbluebutton/bbbnode.log" : `${process.env.PWD}/../log/development.log`; config.log.path = (typeof process !== "undefined" && process !== null ? (ref = process.env) != null ? ref.NODE_ENV : void 0 : void 0) === "production" ? "/var/log/bigbluebutton/bbbnode.log" : `${process.env.PWD}/../log/development.log`;
winston = Winston; // Setting up a logger in Meteor.log
winston = Winston; //Meteor.require 'winston'
file = config.log.path; file = config.log.path;
transports = [ transports = [
new winston.transports.Console(), new winston.transports.File({ new winston.transports.Console(), new winston.transports.File({

View File

@ -1,5 +1,8 @@
// used in Flash and HTML to show a legitimate break in the line
this.BREAK_LINE = '<br/>'; this.BREAK_LINE = '<br/>';
// soft return in HTML to signify a broken line without displaying the escaped '<br/>' line break text
this.CARRIAGE_RETURN = '\r'; this.CARRIAGE_RETURN = '\r';
// handle this the same as carriage return, in case text copied has this
this.NEW_LINE = '\n'; this.NEW_LINE = '\n';

View File

@ -3,6 +3,7 @@ this.Router.configure({
}); });
this.Router.map(function() { this.Router.map(function() {
// this is how we handle login attempts
this.route("main", { this.route("main", {
path: "/html5client/:meeting_id/:user_id/:auth_token", path: "/html5client/:meeting_id/:user_id/:auth_token",
where: "client", where: "client",
@ -12,7 +13,10 @@ this.Router.map(function() {
userId = this.params.user_id; userId = this.params.user_id;
authToken = this.params.auth_token; authToken = this.params.auth_token;
setInSession("loginUrl", this.originalUrl); setInSession("loginUrl", this.originalUrl);
// catch if any of the user's meeting data is invalid
if ((authToken == null) || (meetingId == null) || (userId == null)) { if ((authToken == null) || (meetingId == null) || (userId == null)) {
// if their data is invalid, redirect the user to the logout page
document.location = getInSession('logoutURL'); document.location = getInSession('logoutURL');
} else { } else {
Meteor.call("validateAuthToken", meetingId, userId, authToken); Meteor.call("validateAuthToken", meetingId, userId, authToken);
@ -27,6 +31,8 @@ this.Router.map(function() {
return this.next(); return this.next();
} }
}); });
// the user successfully logged in
this.route("signedin", { this.route("signedin", {
path: "/html5client", path: "/html5client",
where: "client", where: "client",
@ -37,8 +43,13 @@ this.Router.map(function() {
authToken = getInSession("authToken"); authToken = getInSession("authToken");
onErrorFunction = function(error, result) { onErrorFunction = function(error, result) {
console.log("ONERRORFUNCTION"); console.log("ONERRORFUNCTION");
//make sure the user is not let through
Meteor.call("userLogout", meetingId, userId, authToken); Meteor.call("userLogout", meetingId, userId, authToken);
clearSessionVar(); clearSessionVar();
// Attempt to log back in
if (error == null) { if (error == null) {
window.location.href = getInSession('loginUrl') || getInSession('logoutURL'); window.location.href = getInSession('loginUrl') || getInSession('logoutURL');
} }
@ -65,21 +76,24 @@ this.Router.map(function() {
return Meteor.subscribe('bbb_cursor', meetingId, { return Meteor.subscribe('bbb_cursor', meetingId, {
onReady: function() { onReady: function() {
let a, handleLogourUrlError; let a, handleLogourUrlError;
// done subscribing, start rendering the client and set default settings
_this.render('main'); _this.render('main');
onLoadComplete(); onLoadComplete();
handleLogourUrlError = function() { handleLogourUrlError = function() {
alert("Error: could not find the logoutURL"); alert("Error: could not find the logoutURL");
setInSession("logoutURL", document.location.hostname); setInSession("logoutURL", document.location.hostname);
}; };
// obtain the logoutURL
a = $.ajax({ a = $.ajax({
dataType: 'json', dataType: 'json',
url: '/bigbluebutton/api/enter' url: '/bigbluebutton/api/enter'
}); });
a.done(data => { a.done(data => {
if (data.response.logoutURL != null) { if (data.response.logoutURL != null) { // for a meeting with 0 users
setInSession("logoutURL", data.response.logoutURL); setInSession("logoutURL", data.response.logoutURL);
} else { } else {
if (data.response.logoutUrl != null) { if (data.response.logoutUrl != null) { // for a running meeting
setInSession("logoutURL", data.response.logoutUrl); setInSession("logoutURL", data.response.logoutUrl);
} else { } else {
return handleLogourUrlError(); return handleLogourUrlError();
@ -111,6 +125,8 @@ this.Router.map(function() {
return this.render('loading'); return this.render('loading');
} }
}); });
// endpoint - is the html5client running (ready to handle a user)
this.route('meteorEndpoint', { this.route('meteorEndpoint', {
path: '/check', path: '/check',
where: 'server', where: 'server',
@ -118,6 +134,8 @@ this.Router.map(function() {
this.response.writeHead(200, { this.response.writeHead(200, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
// reply that the html5client is running
this.response.end(JSON.stringify({ this.response.end(JSON.stringify({
"html5clientStatus": "running" "html5clientStatus": "running"
})); }));

View File

@ -1,4 +1,8 @@
Meteor.methods({ Meteor.methods({
// meetingId: the id of the meeting
// chatObject: the object including info on the chat message, including the text
// requesterUserId: the userId of the user sending chat
// requesterToken: the authToken of the requester
sendChatMessagetoServer(meetingId, chatObject, requesterUserId, requesterToken) { sendChatMessagetoServer(meetingId, chatObject, requesterUserId, requesterToken) {
let action, chatType, eventName, message, recipient; let action, chatType, eventName, message, recipient;
chatType = chatObject.chat_type; chatType = chatObject.chat_type;
@ -11,7 +15,7 @@ Meteor.methods({
} else { } else {
eventName = "send_private_chat_message"; eventName = "send_private_chat_message";
if(recipient === requesterUserId) { if(recipient === requesterUserId) {
return 'chatSelf'; return 'chatSelf'; //not allowed
} else { } else {
return 'chatPrivate'; return 'chatPrivate';
} }
@ -35,6 +39,7 @@ Meteor.methods({
} }
}, },
deletePrivateChatMessages(userId, contact_id) { deletePrivateChatMessages(userId, contact_id) {
// if authorized pass through
let contact, requester; let contact, requester;
requester = Meteor.Users.findOne({ requester = Meteor.Users.findOne({
userId: userId userId: userId
@ -46,8 +51,13 @@ Meteor.methods({
} }
}); });
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addChatToCollection = function(meetingId, messageObject) { this.addChatToCollection = function(meetingId, messageObject) {
let id; let id;
// manually convert time from 1.408645053653E12 to 1408645053653 if necessary
// (this is the time_from that the Flash client outputs)
messageObject.from_time = messageObject.from_time.toString().split('.').join("").split("E")[0]; messageObject.from_time = messageObject.from_time.toString().split('.').join("").split("E")[0];
if((messageObject.from_userid != null) && (messageObject.to_userid != null)) { if((messageObject.from_userid != null) && (messageObject.to_userid != null)) {
messageObject.message = translateFlashToHTML5(messageObject.message); messageObject.message = translateFlashToHTML5(messageObject.message);
@ -81,6 +91,7 @@ this.addChatToCollection = function(meetingId, messageObject) {
} }
}; };
// called on server start and meeting end
this.clearChatCollection = function(meetingId) { this.clearChatCollection = function(meetingId) {
if(meetingId != null) { if(meetingId != null) {
return Meteor.Chat.remove({ return Meteor.Chat.remove({
@ -91,6 +102,11 @@ this.clearChatCollection = function(meetingId) {
} }
}; };
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------
// translate '\n' newline character and '\r' carriage returns to '<br/>' breakline character for Flash
this.translateHTML5ToFlash = function(message) { this.translateHTML5ToFlash = function(message) {
let result; let result;
result = message; result = message;
@ -99,6 +115,7 @@ this.translateHTML5ToFlash = function(message) {
return result; return result;
}; };
// translate '<br/>' breakline character to '\r' carriage return character for HTML5
this.translateFlashToHTML5 = function(message) { this.translateFlashToHTML5 = function(message) {
let result; let result;
result = message; result = message;

View File

@ -1,3 +1,6 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.initializeCursor = function(meetingId) { this.initializeCursor = function(meetingId) {
return Meteor.Cursor.upsert({ return Meteor.Cursor.upsert({
meetingId: meetingId meetingId: meetingId
@ -9,7 +12,7 @@ this.initializeCursor = function(meetingId) {
if(err) { if(err) {
return Meteor.log.error(`err upserting cursor for ${meetingId}`); return Meteor.log.error(`err upserting cursor for ${meetingId}`);
} else { } else {
// Meteor.log.info "ok upserting cursor for #{meetingId}"
} }
}); });
}; };
@ -26,11 +29,12 @@ this.updateCursorLocation = function(meetingId, cursorObject) {
if(err != null) { if(err != null) {
return Meteor.log.error(`_unsucc update of cursor for ${meetingId} ${JSON.stringify(cursorObject)} err=${JSON.stringify(err)}`); return Meteor.log.error(`_unsucc update of cursor for ${meetingId} ${JSON.stringify(cursorObject)} err=${JSON.stringify(err)}`);
} else { } else {
// Meteor.log.info "updated cursor for #{meetingId} #{JSON.stringify cursorObject}"
} }
}); });
}; };
// called on server start and meeting end
this.clearCursorCollection = function(meetingId) { this.clearCursorCollection = function(meetingId) {
if(meetingId != null) { if(meetingId != null) {
return Meteor.Cursor.remove({ return Meteor.Cursor.remove({
@ -44,3 +48,7 @@ this.clearCursorCollection = function(meetingId) {
}); });
} }
}; };
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,4 +1,10 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addMeetingToCollection = function(meetingId, name, intendedForRecording, voiceConf, duration, callback) { this.addMeetingToCollection = function(meetingId, name, intendedForRecording, voiceConf, duration, callback) {
//check if the meeting is already in the collection
Meteor.Meetings.upsert({ Meteor.Meetings.upsert({
meetingId: meetingId meetingId: meetingId
}, { }, {
@ -9,13 +15,14 @@ this.addMeetingToCollection = function(meetingId, name, intendedForRecording, vo
voiceConf: voiceConf, voiceConf: voiceConf,
duration: duration, duration: duration,
roomLockSettings: { roomLockSettings: {
// by default the lock settings will be disabled on meeting create
disablePrivateChat: false, disablePrivateChat: false,
disableCam: false, disableCam: false,
disableMic: false, disableMic: false,
lockOnJoin: Meteor.config.lockOnJoin, lockOnJoin: Meteor.config.lockOnJoin,
lockedLayout: false, lockedLayout: false,
disablePublicChat: false, disablePublicChat: false,
lockOnJoinConfigurable: false lockOnJoinConfigurable: false // TODO
} }
} }
}, (_this => { }, (_this => {
@ -33,6 +40,8 @@ this.addMeetingToCollection = function(meetingId, name, intendedForRecording, vo
} }
}; };
})(this)); })(this));
// initialize the cursor in the meeting
return initializeCursor(meetingId); return initializeCursor(meetingId);
}; };
@ -46,18 +55,33 @@ this.clearMeetingsCollection = function(meetingId) {
} }
}; };
//clean up upon a meeting's end
this.removeMeetingFromCollection = function(meetingId, callback) { this.removeMeetingFromCollection = function(meetingId, callback) {
let funct; let funct;
if(Meteor.Meetings.findOne({ if(Meteor.Meetings.findOne({
meetingId: meetingId meetingId: meetingId
}) != null) { }) != null) {
Meteor.log.info(`end of meeting ${meetingId}. Clear the meeting data from all collections`); Meteor.log.info(`end of meeting ${meetingId}. Clear the meeting data from all collections`);
// delete all users in the meeting
clearUsersCollection(meetingId); clearUsersCollection(meetingId);
// delete all slides in the meeting
clearSlidesCollection(meetingId); clearSlidesCollection(meetingId);
// delete all shapes in the meeting
clearShapesCollection(meetingId); clearShapesCollection(meetingId);
// delete all presentations in the meeting
clearPresentationsCollection(meetingId); clearPresentationsCollection(meetingId);
// delete all chat messages in the meeting
clearChatCollection(meetingId); clearChatCollection(meetingId);
// delete the meeting
clearMeetingsCollection(meetingId); clearMeetingsCollection(meetingId);
// delete the cursor for the meeting
clearCursorCollection(meetingId); clearCursorCollection(meetingId);
return callback(); return callback();
} else { } else {
@ -68,3 +92,9 @@ this.removeMeetingFromCollection = function(meetingId, callback) {
return funct(callback); return funct(callback);
} }
}; };
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,3 +1,6 @@
// --------------------------------------------------------------------------------------------
// Public methods on server
// --------------------------------------------------------------------------------------------
Meteor.methods({ Meteor.methods({
publishVoteMessage(meetingId, pollAnswerId, requesterUserId, requesterToken) { publishVoteMessage(meetingId, pollAnswerId, requesterUserId, requesterToken) {
let _poll_id, eventName, message, result; let _poll_id, eventName, message, result;
@ -44,20 +47,29 @@ Meteor.methods({
} }
}); });
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addPollToCollection = function(poll, requester_id, users, meetingId) { this.addPollToCollection = function(poll, requester_id, users, meetingId) {
let _users, answer, entry, i, j, len, len1, ref, user; let _users, answer, entry, i, j, len, len1, ref, user;
//copying all the userids into an array
_users = []; _users = [];
for (i = 0, len = users.length; i < len; i++) { for (i = 0, len = users.length; i < len; i++) {
user = users[i]; user = users[i];
_users.push(user.user.userid); _users.push(user.user.userid);
} }
//adding the initial number of votes for each answer
ref = poll.answers; ref = poll.answers;
for (j = 0, len1 = ref.length; j < len1; j++) { for (j = 0, len1 = ref.length; j < len1; j++) {
answer = ref[j]; answer = ref[j];
answer.num_votes = 0; answer.num_votes = 0;
} }
//adding the initial number of responders and respondents to the poll, which will be displayed for presenter (in HTML5 client) when he starts the poll
poll.num_responders = -1; poll.num_responders = -1;
poll.num_respondents = -1; poll.num_respondents = -1;
//adding all together and inserting into the Polls collection
entry = { entry = {
poll_info: { poll_info: {
"meetingId": meetingId, "meetingId": meetingId,

View File

@ -59,8 +59,12 @@ Meteor.methods({
} }
}); });
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addPresentationToCollection = function(meetingId, presentationObject) { this.addPresentationToCollection = function(meetingId, presentationObject) {
let entry, id; let entry, id;
//check if the presentation is already in the collection
if(Meteor.Presentations.findOne({ if(Meteor.Presentations.findOne({
meetingId: meetingId, meetingId: meetingId,
'presentation.id': presentationObject.id 'presentation.id': presentationObject.id
@ -74,6 +78,7 @@ this.addPresentationToCollection = function(meetingId, presentationObject) {
} }
}; };
return id = Meteor.Presentations.insert(entry); return id = Meteor.Presentations.insert(entry);
//Meteor.log.info "presentation added id =[#{id}]:#{presentationObject.id} in #{meetingId}. Presentations.size is now #{Meteor.Presentations.find({meetingId: meetingId}).count()}"
} }
}; };
@ -97,6 +102,7 @@ this.removePresentationFromCollection = function(meetingId, presentationId) {
} }
}; };
// called on server start and meeting end
this.clearPresentationsCollection = function(meetingId) { this.clearPresentationsCollection = function(meetingId) {
if(meetingId != null) { if(meetingId != null) {
return Meteor.Presentations.remove({ return Meteor.Presentations.remove({
@ -106,3 +112,7 @@ this.clearPresentationsCollection = function(meetingId) {
return Meteor.Presentations.remove({}, Meteor.log.info("cleared Presentations Collection (all meetings)!")); return Meteor.Presentations.remove({}, Meteor.log.info("cleared Presentations Collection (all meetings)!"));
} }
}; };
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,3 +1,6 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addShapeToCollection = function(meetingId, whiteboardId, shapeObject) { this.addShapeToCollection = function(meetingId, whiteboardId, shapeObject) {
let entry, id, removeTempTextShape; let entry, id, removeTempTextShape;
if((shapeObject != null ? shapeObject.shape_type : void 0) === "text") { if((shapeObject != null ? shapeObject.shape_type : void 0) === "text") {
@ -24,19 +27,25 @@ this.addShapeToCollection = function(meetingId, whiteboardId, shapeObject) {
} }
}; };
if(shapeObject.status === "textEdited" || shapeObject.status === "textPublished") { if(shapeObject.status === "textEdited" || shapeObject.status === "textPublished") {
// only keep the final version of the text shape
removeTempTextShape = function(callback) { removeTempTextShape = function(callback) {
Meteor.Shapes.remove({ Meteor.Shapes.remove({
'shape.id': shapeObject.shape.id 'shape.id': shapeObject.shape.id
}); });
// for s in Meteor.Shapes.find({'shape.id':shapeObject.shape.id}).fetch()
// Meteor.log.info "there is this shape: #{s.shape.text}"
return callback(); return callback();
}; };
return removeTempTextShape(() => { return removeTempTextShape(() => {
// display as the prestenter is typing
let id; let id;
id = Meteor.Shapes.insert(entry); id = Meteor.Shapes.insert(entry);
return Meteor.log.info(`${shapeObject.status} substituting the temp shapes with the newer one`); return Meteor.log.info(`${shapeObject.status} substituting the temp shapes with the newer one`);
}); });
} }
} else { } else {
// the mouse button was released - the drawing is complete
// TODO: pencil messages currently don't send draw_end and are labeled all as DRAW_START
if((shapeObject != null ? shapeObject.status : void 0) === "DRAW_END" || ((shapeObject != null ? shapeObject.status : void 0) === "DRAW_START" && (shapeObject != null ? shapeObject.shape_type : void 0) === "pencil")) { if((shapeObject != null ? shapeObject.status : void 0) === "DRAW_END" || ((shapeObject != null ? shapeObject.status : void 0) === "DRAW_START" && (shapeObject != null ? shapeObject.shape_type : void 0) === "pencil")) {
entry = { entry = {
meetingId: meetingId, meetingId: meetingId,
@ -78,6 +87,8 @@ this.removeAllShapesFromSlide = function(meetingId, whiteboardId) {
whiteboardId: whiteboardId whiteboardId: whiteboardId
}, () => { }, () => {
Meteor.log.info("clearing all shapes from slide"); Meteor.log.info("clearing all shapes from slide");
// After shapes are cleared, wait 1 second and set cleaning off
return Meteor.setTimeout(() => { return Meteor.setTimeout(() => {
return Meteor.WhiteboardCleanStatus.update({ return Meteor.WhiteboardCleanStatus.update({
meetingId: meetingId meetingId: meetingId
@ -108,6 +119,7 @@ whiteboardId: whiteboardId
} }
}; };
// called on server start and meeting end
this.clearShapesCollection = function(meetingId) { this.clearShapesCollection = function(meetingId) {
if(meetingId != null) { if(meetingId != null) {
return Meteor.Shapes.remove({ return Meteor.Shapes.remove({
@ -135,3 +147,7 @@ this.clearShapesCollection = function(meetingId) {
}); });
} }
}; };
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,6 +1,10 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.displayThisSlide = function(meetingId, newSlideId, slideObject) { this.displayThisSlide = function(meetingId, newSlideId, slideObject) {
let presentationId; let presentationId;
presentationId = newSlideId.split("/")[0]; presentationId = newSlideId.split("/")[0]; // grab the presentationId part of the slideId
// change current to false for the old slide
Meteor.Slides.update({ Meteor.Slides.update({
presentationId: presentationId, presentationId: presentationId,
"slide.current": true "slide.current": true
@ -9,7 +13,10 @@ this.displayThisSlide = function(meetingId, newSlideId, slideObject) {
"slide.current": false "slide.current": false
} }
}); });
// for the new slide: remove the version which came with presentation_shared_message from the Collection
// to avoid using old data (this message contains everything we need for the new slide)
removeSlideFromCollection(meetingId, newSlideId); removeSlideFromCollection(meetingId, newSlideId);
// add the new slide to the collection
return addSlideToCollection(meetingId, presentationId, slideObject); return addSlideToCollection(meetingId, presentationId, slideObject);
}; };
@ -37,6 +44,7 @@ this.addSlideToCollection = function(meetingId, presentationId, slideObject) {
} }
}; };
return id = Meteor.Slides.insert(entry); return id = Meteor.Slides.insert(entry);
//Meteor.log.info "added slide id =[#{id}]:#{slideObject.id} in #{meetingId}. Now there are #{Meteor.Slides.find({meetingId: meetingId}).count()} slides in the meeting"
} }
}; };
@ -57,6 +65,7 @@ this.removeSlideFromCollection = function(meetingId, slideId) {
} }
}; };
// called on server start and meeting end
this.clearSlidesCollection = function(meetingId) { this.clearSlidesCollection = function(meetingId) {
if(meetingId != null) { if(meetingId != null) {
return Meteor.Slides.remove({ return Meteor.Slides.remove({
@ -66,3 +75,7 @@ this.clearSlidesCollection = function(meetingId) {
return Meteor.Slides.remove({}, Meteor.log.info("cleared Slides Collection (all meetings)!")); return Meteor.Slides.remove({}, Meteor.log.info("cleared Slides Collection (all meetings)!"));
} }
}; };
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,4 +1,15 @@
// --------------------------------------------------------------------------------------------
// Public methods on server
// All these method must first authenticate the user before it calls the private function counterpart below
// which sends the request to bbbApps. If the method is modifying the media the current user is sharing,
// you should perform the request before sending the request to bbbApps. This allows the user request to be performed
// immediately, since they do not require permission for things such as muting themsevles.
// --------------------------------------------------------------------------------------------
Meteor.methods({ Meteor.methods({
// meetingId: the meetingId of the meeting the user is in
// toSetUserId: the userId of the user joining
// requesterUserId: the userId of the requester
// requesterToken: the authToken of the requester
listenOnlyRequestToggle(meetingId, userId, authToken, isJoining) { listenOnlyRequestToggle(meetingId, userId, authToken, isJoining) {
let message, ref, ref1, username, voiceConf; let message, ref, ref1, username, voiceConf;
voiceConf = (ref = Meteor.Meetings.findOne({ voiceConf = (ref = Meteor.Meetings.findOne({
@ -46,6 +57,11 @@ Meteor.methods({
} }
} }
}, },
// meetingId: the meetingId of the meeting the user[s] is in
// toMuteUserId: the userId of the user to be muted
// requesterUserId: the userId of the requester
// requesterToken: the authToken of the requester
muteUser(meetingId, toMuteUserId, requesterUserId, requesterToken) { muteUser(meetingId, toMuteUserId, requesterUserId, requesterToken) {
let action, message; let action, message;
action = function() { action = function() {
@ -78,6 +94,11 @@ Meteor.methods({
}); });
} }
}, },
// meetingId: the meetingId of the meeting the user[s] is in
// toMuteUserId: the userId of the user to be unmuted
// requesterUserId: the userId of the requester
// requesterToken: the authToken of the requester
unmuteUser(meetingId, toMuteUserId, requesterUserId, requesterToken) { unmuteUser(meetingId, toMuteUserId, requesterUserId, requesterToken) {
let action, message; let action, message;
action = function() { action = function() {
@ -125,15 +146,26 @@ Meteor.methods({
version: "0.0.1" version: "0.0.1"
} }
}; };
// publish to pubsub
publish(Meteor.config.redis.channels.toBBBApps.users, message); publish(Meteor.config.redis.channels.toBBBApps.users, message);
} }
}, },
// meetingId: the meeting where the user is
// userId: the userid of the user logging out
// authToken: the authToken of the user
userLogout(meetingId, userId, authToken) { userLogout(meetingId, userId, authToken) {
if(isAllowedTo('logoutSelf', meetingId, userId, authToken)) { if(isAllowedTo('logoutSelf', meetingId, userId, authToken)) {
Meteor.log.info(`a user is logging out from ${meetingId}:${userId}`); Meteor.log.info(`a user is logging out from ${meetingId}:${userId}`);
return requestUserLeaving(meetingId, userId); return requestUserLeaving(meetingId, userId);
} }
}, },
//meetingId: the meeting where the user is
//toKickUserId: the userid of the user to kick
//requesterUserId: the userid of the user that wants to kick
//authToken: the authToken of the user that wants to kick
kickUser(meetingId, toKickUserId, requesterUserId, authToken) { kickUser(meetingId, toKickUserId, requesterUserId, authToken) {
let message; let message;
if(isAllowedTo('kickUser', meetingId, requesterUserId, authToken)) { if(isAllowedTo('kickUser', meetingId, requesterUserId, authToken)) {
@ -150,6 +182,12 @@ Meteor.methods({
return publish(Meteor.config.redis.channels.toBBBApps.users, message); return publish(Meteor.config.redis.channels.toBBBApps.users, message);
} }
}, },
//meetingId: the meeting where the user is
//newPresenterId: the userid of the new presenter
//requesterSetPresenter: the userid of the user that wants to change the presenter
//newPresenterName: user name of the new presenter
//authToken: the authToken of the user that wants to kick
setUserPresenter( setUserPresenter(
meetingId, meetingId,
newPresenterId, newPresenterId,
@ -174,7 +212,18 @@ Meteor.methods({
} }
}); });
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
// Only callable from server
// Received information from BBB-Apps that a user left
// Need to update the collection
// params: meetingid, userid as defined in BBB-Apps
// callback
this.markUserOffline = function(meetingId, userId, callback) { this.markUserOffline = function(meetingId, userId, callback) {
// mark the user as offline. remove from the collection on meeting_end #TODO
let user; let user;
user = Meteor.Users.findOne({ user = Meteor.Users.findOne({
meetingId: meetingId, meetingId: meetingId,
@ -227,6 +276,9 @@ this.markUserOffline = function(meetingId, userId, callback) {
} }
}; };
// Corresponds to a valid action on the HTML clientside
// After authorization, publish a user_leaving_request in redis
// params: meetingid, userid as defined in BBB-App
this.requestUserLeaving = function(meetingId, userId) { this.requestUserLeaving = function(meetingId, userId) {
let listenOnlyMessage, message, ref, userObject, voiceConf; let listenOnlyMessage, message, ref, userObject, voiceConf;
userObject = Meteor.Users.findOne({ userObject = Meteor.Users.findOne({
@ -237,6 +289,8 @@ this.requestUserLeaving = function(meetingId, userId) {
meetingId: meetingId meetingId: meetingId
})) != null ? ref.voiceConf : void 0; })) != null ? ref.voiceConf : void 0;
if((userObject != null) && (voiceConf != null) && (userId != null) && (meetingId != null)) { if((userObject != null) && (voiceConf != null) && (userId != null) && (meetingId != null)) {
// end listenOnly audio for the departing user
if(userObject.user.listenOnly) { if(userObject.user.listenOnly) {
listenOnlyMessage = { listenOnlyMessage = {
payload: { payload: {
@ -252,6 +306,8 @@ this.requestUserLeaving = function(meetingId, userId) {
}; };
publish(Meteor.config.redis.channels.toBBBApps.meeting, listenOnlyMessage); publish(Meteor.config.redis.channels.toBBBApps.meeting, listenOnlyMessage);
} }
// remove user from meeting
message = { message = {
payload: { payload: {
meeting_id: meetingId, meeting_id: meetingId,
@ -269,6 +325,7 @@ this.requestUserLeaving = function(meetingId, userId) {
} }
}; };
//update a voiceUser - a helper method
this.updateVoiceUser = function(meetingId, voiceUserObject, callback) { this.updateVoiceUser = function(meetingId, voiceUserObject, callback) {
let u; let u;
u = Meteor.Users.findOne({ u = Meteor.Users.findOne({
@ -289,7 +346,7 @@ this.updateVoiceUser = function(meetingId, voiceUserObject, callback) {
} }
return callback(); return callback();
}); });
} } // talking
if(voiceUserObject.joined != null) { if(voiceUserObject.joined != null) {
Meteor.Users.update({ Meteor.Users.update({
meetingId: meetingId, meetingId: meetingId,
@ -306,7 +363,7 @@ this.updateVoiceUser = function(meetingId, voiceUserObject, callback) {
} }
return callback(); return callback();
}); });
} } // joined
if(voiceUserObject.locked != null) { if(voiceUserObject.locked != null) {
Meteor.Users.update({ Meteor.Users.update({
meetingId: meetingId, meetingId: meetingId,
@ -321,7 +378,7 @@ this.updateVoiceUser = function(meetingId, voiceUserObject, callback) {
} }
return callback(); return callback();
}); });
} } // locked
if(voiceUserObject.muted != null) { if(voiceUserObject.muted != null) {
Meteor.Users.update({ Meteor.Users.update({
meetingId: meetingId, meetingId: meetingId,
@ -336,7 +393,7 @@ this.updateVoiceUser = function(meetingId, voiceUserObject, callback) {
} }
return callback(); return callback();
}); });
} } // muted
if(voiceUserObject.listen_only != null) { if(voiceUserObject.listen_only != null) {
return Meteor.Users.update({ return Meteor.Users.update({
meetingId: meetingId, meetingId: meetingId,
@ -351,7 +408,7 @@ this.updateVoiceUser = function(meetingId, voiceUserObject, callback) {
} }
return callback(); return callback();
}); });
} } // listenOnly
} else { } else {
Meteor.log.error("ERROR! did not find such voiceUser!"); Meteor.log.error("ERROR! did not find such voiceUser!");
return callback(); return callback();
@ -365,6 +422,10 @@ this.userJoined = function(meetingId, user, callback) {
userId: user.userid, userId: user.userid,
meetingId: meetingId meetingId: meetingId
}); });
// the collection already contains an entry for this user
// because the user is reconnecting OR
// in the case of an html5 client user we added a dummy user on
// register_user_message (to save authToken)
if((u != null) && (u.authToken != null)) { if((u != null) && (u.authToken != null)) {
Meteor.Users.update({ Meteor.Users.update({
userId: user.userid, userId: user.userid,
@ -385,7 +446,7 @@ this.userJoined = function(meetingId, user, callback) {
extern_userid: user.extern_userid, extern_userid: user.extern_userid,
locked: user.locked, locked: user.locked,
time_of_joining: user.timeOfJoining, time_of_joining: user.timeOfJoining,
connection_status: "online", connection_status: "online", // TODO consider other default value
voiceUser: { voiceUser: {
web_userid: user.voiceUser.web_userid, web_userid: user.voiceUser.web_userid,
callernum: user.voiceUser.callernum, callernum: user.voiceUser.callernum,
@ -416,6 +477,7 @@ this.userJoined = function(meetingId, user, callback) {
meetingId: meetingId meetingId: meetingId
})) != null ? ref.meetingName : void 0); })) != null ? ref.meetingName : void 0);
welcomeMessage = welcomeMessage + Meteor.config.defaultWelcomeMessageFooter; welcomeMessage = welcomeMessage + Meteor.config.defaultWelcomeMessageFooter;
// add the welcome message if it's not there already OR update time_of_joining
return Meteor.Chat.upsert({ return Meteor.Chat.upsert({
meetingId: meetingId, meetingId: meetingId,
userId: userId, userId: userId,
@ -439,8 +501,12 @@ this.userJoined = function(meetingId, user, callback) {
} else { } else {
return Meteor.log.info(`_added/updated a system message in chat for user ${userId}`); return Meteor.log.info(`_added/updated a system message in chat for user ${userId}`);
} }
// note that we already called callback() when updating the user. Adding
// the welcome message in the chat is not as vital and we can afford to
// complete it when possible, without blocking the serial event messages processing
}); });
} else { } else {
// Meteor.log.info "NOTE: got user_joined_message #{user.name} #{user.userid}"
return Meteor.Users.upsert({ return Meteor.Users.upsert({
meetingId: meetingId, meetingId: meetingId,
userId: userId userId: userId
@ -506,7 +572,7 @@ this.createDummyUser = function(meetingId, userId, authToken) {
userId: userId, userId: userId,
authToken: authToken, authToken: authToken,
clientType: "HTML5", clientType: "HTML5",
validated: false validated: false //will be validated on validate_auth_token_reply
}, (err, id) => { }, (err, id) => {
return Meteor.log.info(`_added a dummy html5 user with: userId=[${userId}] Users.size is now ${Meteor.Users.find({ return Meteor.log.info(`_added a dummy html5 user with: userId=[${userId}] Users.size is now ${Meteor.Users.find({
meetingId: meetingId meetingId: meetingId
@ -515,7 +581,10 @@ this.createDummyUser = function(meetingId, userId, authToken) {
} }
}; };
// when new lock settings including disableMic are set,
// all viewers that are in the audio bridge with a mic should be muted and locked
this.handleLockingMic = function(meetingId, newSettings) { this.handleLockingMic = function(meetingId, newSettings) {
// send mute requests for the viewer users joined with mic
let i, len, ref, ref1, results, u; let i, len, ref, ref1, results, u;
ref1 = (ref = Meteor.Users.find({ ref1 = (ref = Meteor.Users.find({
meetingId: meetingId, meetingId: meetingId,
@ -528,11 +597,13 @@ this.handleLockingMic = function(meetingId, newSettings) {
results = []; results = [];
for(i = 0, len = ref1.length; i < len; i++) { for(i = 0, len = ref1.length; i < len; i++) {
u = ref1[i]; u = ref1[i];
results.push(Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true)); // Meteor.log.info u.user.name #
results.push(Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true)); //true for muted
} }
return results; return results;
}; };
// change the locked status of a user (lock settings)
this.setUserLockedStatus = function(meetingId, userId, isLocked) { this.setUserLockedStatus = function(meetingId, userId, isLocked) {
let u; let u;
u = Meteor.Users.findOne({ u = Meteor.Users.findOne({
@ -554,14 +625,17 @@ this.setUserLockedStatus = function(meetingId, userId, isLocked) {
return Meteor.log.info(`_setting user locked status for userid:[${userId}] from [${meetingId}] locked=${isLocked}`); return Meteor.log.info(`_setting user locked status for userid:[${userId}] from [${meetingId}] locked=${isLocked}`);
} }
}); });
// if the user is sharing audio, he should be muted upon locking involving disableMic
if(u.user.role === 'VIEWER' && !u.user.listenOnly && u.user.voiceUser.joined && !u.user.voiceUser.muted && isLocked) { if(u.user.role === 'VIEWER' && !u.user.listenOnly && u.user.voiceUser.joined && !u.user.voiceUser.muted && isLocked) {
return Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true); return Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true); //true for muted
} }
} else { } else {
return Meteor.log.error(`(unsuccessful-no such user) setting user locked status for userid:[${userId}] from [${meetingId}] locked=${isLocked}`); return Meteor.log.error(`(unsuccessful-no such user) setting user locked status for userid:[${userId}] from [${meetingId}] locked=${isLocked}`);
} }
}; };
// called on server start and on meeting end
this.clearUsersCollection = function(meetingId) { this.clearUsersCollection = function(meetingId) {
if(meetingId != null) { if(meetingId != null) {
return Meteor.Users.remove({ return Meteor.Users.remove({

View File

@ -1,3 +1,5 @@
// Publish only the online users that are in the particular meetingId
// On the client side we pass the meetingId parameter
Meteor.publish('users', function(meetingId, userid, authToken) { Meteor.publish('users', function(meetingId, userid, authToken) {
let ref, ref1, u, username; let ref, ref1, u, username;
Meteor.log.info(`attempt publishing users for ${meetingId}, ${userid}, ${authToken}`); Meteor.log.info(`attempt publishing users for ${meetingId}, ${userid}, ${authToken}`);
@ -10,6 +12,8 @@ Meteor.publish('users', function(meetingId, userid, authToken) {
if(isAllowedTo('subscribeUsers', meetingId, userid, authToken)) { if(isAllowedTo('subscribeUsers', meetingId, userid, authToken)) {
Meteor.log.info(`${userid} was allowed to subscribe to 'users'`); Meteor.log.info(`${userid} was allowed to subscribe to 'users'`);
username = (u != null ? (ref = u.user) != null ? ref.name : void 0 : void 0) || "UNKNOWN"; username = (u != null ? (ref = u.user) != null ? ref.name : void 0 : void 0) || "UNKNOWN";
// offline -> online
if(((ref1 = u.user) != null ? ref1.connection_status : void 0) !== 'online') { if(((ref1 = u.user) != null ? ref1.connection_status : void 0) !== 'online') {
Meteor.call("validateAuthToken", meetingId, userid, authToken); Meteor.call("validateAuthToken", meetingId, userid, authToken);
} }
@ -37,6 +41,8 @@ Meteor.publish('users', function(meetingId, userid, authToken) {
return requestUserLeaving(meetingId, userid); return requestUserLeaving(meetingId, userid);
}; };
})(this))); })(this)));
//publish the users which are not offline
return Meteor.Users.find({ return Meteor.Users.find({
meetingId: meetingId, meetingId: meetingId,
'user.connection_status': { 'user.connection_status': {
@ -51,7 +57,7 @@ Meteor.publish('users', function(meetingId, userid, authToken) {
Meteor.log.warn("was not authorized to subscribe to 'users'"); Meteor.log.warn("was not authorized to subscribe to 'users'");
return this.error(new Meteor.Error(402, "The user was not authorized to subscribe to 'users'")); return this.error(new Meteor.Error(402, "The user was not authorized to subscribe to 'users'"));
} }
} else { } else { //subscribing before the user was added to the collection
Meteor.call("validateAuthToken", meetingId, userid, authToken); Meteor.call("validateAuthToken", meetingId, userid, authToken);
Meteor.log.error(`there was no such user ${userid} in ${meetingId}`); Meteor.log.error(`there was no such user ${userid} in ${meetingId}`);
return Meteor.Users.find({ return Meteor.Users.find({
@ -90,7 +96,9 @@ Meteor.publish('chat', function(meetingId, userid, authToken) {
}); });
Meteor.publish('bbb_poll', function(meetingId, userid, authToken) { Meteor.publish('bbb_poll', function(meetingId, userid, authToken) {
//checking if it is allowed to see Poll Collection in general
if(isAllowedTo('subscribePoll', meetingId, userid, authToken)) { if(isAllowedTo('subscribePoll', meetingId, userid, authToken)) {
//checking if it is allowed to see a number of votes (presenter only)
if(isAllowedTo('subscribeAnswers', meetingId, userid, authToken)) { if(isAllowedTo('subscribeAnswers', meetingId, userid, authToken)) {
Meteor.log.info("publishing Poll for presenter: " + meetingId + " " + userid + " " + authToken); Meteor.log.info("publishing Poll for presenter: " + meetingId + " " + userid + " " + authToken);
return Meteor.Polls.find({ return Meteor.Polls.find({

View File

@ -1,6 +1,7 @@
const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (let i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (let i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
Meteor.methods({ Meteor.methods({
// Construct and send a message to bbb-web to validate the user
validateAuthToken(meetingId, userId, authToken) { validateAuthToken(meetingId, userId, authToken) {
let message; let message;
Meteor.log.info("sending a validate_auth_token with", { Meteor.log.info("sending a validate_auth_token with", {
@ -47,11 +48,13 @@ Meteor.RedisPubSub = (function() {
_onSubscribe(channel, count) { _onSubscribe(channel, count) {
let message; let message;
Meteor.log.info(`Subscribed to ${channel}`); Meteor.log.info(`Subscribed to ${channel}`);
//grab data about all active meetings on the server
message = { message = {
"header": { "header": {
"name": "get_all_meetings_request" "name": "get_all_meetings_request"
}, },
"payload": {} "payload": {} // I need this, otherwise bbb-apps won't recognize the message
}; };
return publish(Meteor.config.redis.channels.toBBBApps.meeting, message); return publish(Meteor.config.redis.channels.toBBBApps.meeting, message);
} }
@ -75,6 +78,11 @@ Meteor.RedisPubSub = (function() {
return RedisPubSub; return RedisPubSub;
})(); })();
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
// message should be an object
this.publish = function(channel, message) { this.publish = function(channel, message) {
Meteor.log.info(`redis outgoing message ${message.header.name}`, { Meteor.log.info(`redis outgoing message ${message.header.name}`, {
channel: channel, channel: channel,

View File

@ -2,6 +2,8 @@ const indexOf = [].indexOf || function(item) { for(let i = 0, l = this.length; i
Meteor.startup(() => { Meteor.startup(() => {
Meteor.log.info("server start"); Meteor.log.info("server start");
//remove all data
Meteor.WhiteboardCleanStatus.remove({}); Meteor.WhiteboardCleanStatus.remove({});
clearUsersCollection(); clearUsersCollection();
clearChatCollection(); clearChatCollection();
@ -11,10 +13,15 @@ Meteor.startup(() => {
clearPresentationsCollection(); clearPresentationsCollection();
clearPollCollection(); clearPollCollection();
clearCursorCollection(); clearCursorCollection();
// create create a PubSub connection, start listening
Meteor.redisPubSub = new Meteor.RedisPubSub(function() { Meteor.redisPubSub = new Meteor.RedisPubSub(function() {
return Meteor.log.info("created pubsub"); return Meteor.log.info("created pubsub");
}); });
Meteor.myQueue = new PowerQueue({}); Meteor.myQueue = new PowerQueue({
// autoStart:true
// isPaused: true
});
Meteor.myQueue.taskHandler = function(data, next, failures) { Meteor.myQueue.taskHandler = function(data, next, failures) {
let eventName, ref; let eventName, ref;
eventName = (ref = JSON.parse(data.jsonMsg)) != null ? ref.header.name : void 0; eventName = (ref = JSON.parse(data.jsonMsg)) != null ? ref.header.name : void 0;
@ -36,11 +43,45 @@ Meteor.startup(() => {
}); });
} }
}; };
// To ensure that we process the redis json event messages serially we use a
// callback. This callback is to be called when the Meteor collection is
// updated with the information coming in the payload of the json message. The
// callback signalizes to the queue that we are done processing the current
// message in the queue and are ready to move on to the next one. If we do not
// use the callback mechanism we may encounter a race condition situation
// due to not following the order of events coming through the redis pubsub.
// for example: a user_left event reaching the collection before a user_joined
// for the same user.
return this.handleRedisMessage = function(data, callback) { return this.handleRedisMessage = function(data, callback) {
let chatMessage, currentlyBeingRecorded, cursor, dbUser, duration, emojiStatus, eventName, heightRatio, i, intendedForRecording, isLocked, j, k, l, len, len1, len2, len3, len4, listOfMeetings, m, meetingId, meetingName, message, messageObject, newPresenterId, newSettings, newSlide, notLoggedEventTypes, oldSettings, page, pollObj, poll_id, presentation, presentationId, processMeeting, processUser, ref, ref1, ref10, ref11, ref12, ref13, ref14, ref15, ref16, ref17, ref18, ref19, ref2, ref20, ref21, ref3, ref4, ref5, ref6, ref7, ref8, ref9, replyTo, requesterId, set_emoji_time, shape, shapeId, slide, slideId, status, user, userId, userObj, users, validStatus, voiceConf, voiceUserObj, whiteboardId, widthRatio, xOffset, yOffset; let chatMessage, currentlyBeingRecorded, cursor, dbUser, duration, emojiStatus, eventName, heightRatio, i, intendedForRecording, isLocked, j, k, l, len, len1, len2, len3, len4, listOfMeetings, m, meetingId, meetingName, message, messageObject, newPresenterId, newSettings, newSlide, notLoggedEventTypes, oldSettings, page, pollObj, poll_id, presentation, presentationId, processMeeting, processUser, ref, ref1, ref10, ref11, ref12, ref13, ref14, ref15, ref16, ref17, ref18, ref19, ref2, ref20, ref21, ref3, ref4, ref5, ref6, ref7, ref8, ref9, replyTo, requesterId, set_emoji_time, shape, shapeId, slide, slideId, status, user, userId, userObj, users, validStatus, voiceConf, voiceUserObj, whiteboardId, widthRatio, xOffset, yOffset;
message = JSON.parse(data.jsonMsg); message = JSON.parse(data.jsonMsg);
// correlationId = message.payload?.reply_to or message.header?.reply_to
meetingId = (ref = message.payload) != null ? ref.meeting_id : void 0; meetingId = (ref = message.payload) != null ? ref.meeting_id : void 0;
notLoggedEventTypes = ["keep_alive_reply", "page_resized_message", "presentation_page_resized_message", "presentation_cursor_updated_message", "get_presentation_info_reply", "get_chat_history_reply", "get_whiteboard_shapes_reply", "presentation_shared_message", "presentation_conversion_done_message", "presentation_conversion_progress_message", "presentation_page_generated_message", "BbbPubSubPongMessage", "bbb_apps_is_alive_message", "user_voice_talking_message", "meeting_state_message", "get_recording_status_reply"];
// Avoid cluttering the log with json messages carrying little or repetitive
// information. Comment out a message type in the array to be able to see it
// in the log upon restarting of the Meteor process.
notLoggedEventTypes = [
"keep_alive_reply",
"page_resized_message",
"presentation_page_resized_message",
"presentation_cursor_updated_message",
"get_presentation_info_reply",
//"get_users_reply"
"get_chat_history_reply",
//"get_all_meetings_reply"
"get_whiteboard_shapes_reply",
"presentation_shared_message",
"presentation_conversion_done_message",
"presentation_conversion_progress_message",
"presentation_page_generated_message",
//"presentation_page_changed_message"
"BbbPubSubPongMessage",
"bbb_apps_is_alive_message",
"user_voice_talking_message",
"meeting_state_message",
"get_recording_status_reply"];
eventName = message.header.name; eventName = message.header.name;
meetingId = (ref1 = message.payload) != null ? ref1.meeting_id : void 0; meetingId = (ref1 = message.payload) != null ? ref1.meeting_id : void 0;
if(!(((message != null ? message.header : void 0) != null) && (message.payload != null))) { if(!(((message != null ? message.header : void 0) != null) && (message.payload != null))) {
@ -52,13 +93,18 @@ Meteor.startup(() => {
message: data.jsonMsg message: data.jsonMsg
}); });
} }
// we currently disregard the pattern and channel
if(((message != null ? message.header : void 0) != null) && (message.payload != null)) { if(((message != null ? message.header : void 0) != null) && (message.payload != null)) {
if(eventName === 'meeting_created_message') { if(eventName === 'meeting_created_message') {
// Meteor.log.error JSON.stringify message
meetingName = message.payload.name; meetingName = message.payload.name;
intendedForRecording = message.payload.recorded; intendedForRecording = message.payload.recorded;
voiceConf = message.payload.voice_conf; voiceConf = message.payload.voice_conf;
duration = message.payload.duration; duration = message.payload.duration;
return addMeetingToCollection(meetingId, meetingName, intendedForRecording, voiceConf, duration, callback); return addMeetingToCollection(meetingId, meetingName, intendedForRecording, voiceConf, duration, callback);
// handle voice events
} else if ((message.payload.user != null) && (eventName === 'user_left_voice_message' || eventName === 'user_joined_voice_message' || eventName === 'user_voice_talking_message' || eventName === 'user_voice_muted_message')) { } else if ((message.payload.user != null) && (eventName === 'user_left_voice_message' || eventName === 'user_joined_voice_message' || eventName === 'user_voice_talking_message' || eventName === 'user_voice_muted_message')) {
voiceUserObj = { voiceUserObj = {
'web_userid': message.payload.user.voiceUser.web_userid, 'web_userid': message.payload.user.voiceUser.web_userid,
@ -79,13 +125,16 @@ Meteor.startup(() => {
Meteor.log.info("Let's store some data for the running meetings so that when an HTML5 client joins everything is ready!"); Meteor.log.info("Let's store some data for the running meetings so that when an HTML5 client joins everything is ready!");
Meteor.log.info(JSON.stringify(message)); Meteor.log.info(JSON.stringify(message));
listOfMeetings = message.payload.meetings; listOfMeetings = message.payload.meetings;
// Processing the meetings recursively with a callback to notify us,
// ensuring that we update the meeting collection serially
processMeeting = function() { processMeeting = function() {
let meeting; let meeting;
meeting = listOfMeetings.pop(); meeting = listOfMeetings.pop();
if(meeting != null) { if(meeting != null) {
return addMeetingToCollection(meeting.meetingID, meeting.meetingName, meeting.recorded, meeting.voiceBridge, meeting.duration, processMeeting); return addMeetingToCollection(meeting.meetingID, meeting.meetingName, meeting.recorded, meeting.voiceBridge, meeting.duration, processMeeting);
} else { } else {
return callback(); return callback(); // all meeting arrays (if any) have been processed
} }
}; };
return processMeeting(); return processMeeting();
@ -95,11 +144,16 @@ Meteor.startup(() => {
userId: userObj.userid, userId: userObj.userid,
meetingId: message.payload.meeting_id meetingId: message.payload.meeting_id
}); });
// On attempting reconnection of Flash clients (in voiceBridge) we receive
// an extra user_joined_message. Ignore it as it will add an extra user
// in the user list, creating discrepancy with the list in the Flash client
if((dbUser != null ? (ref3 = dbUser.user) != null ? ref3.connection_status : void 0 : void 0) === "offline" && ((ref4 = message.payload.user) != null ? ref4.phone_user : void 0)) { if((dbUser != null ? (ref3 = dbUser.user) != null ? ref3.connection_status : void 0 : void 0) === "offline" && ((ref4 = message.payload.user) != null ? ref4.phone_user : void 0)) {
Meteor.log.error("offline AND phone user"); Meteor.log.error("offline AND phone user");
return callback(); return callback(); //return without joining the user
} else { } else {
if((dbUser != null ? dbUser.clientType : void 0) === "HTML5") { if((dbUser != null ? dbUser.clientType : void 0) === "HTML5") { // typically html5 users will be in
// the db [as a dummy user] before the joining message
status = dbUser != null ? dbUser.validated : void 0; status = dbUser != null ? dbUser.validated : void 0;
Meteor.log.info(`in user_joined_message the validStatus of the user was ${status}`); Meteor.log.info(`in user_joined_message the validStatus of the user was ${status}`);
userObj.timeOfJoining = message.header.current_time; userObj.timeOfJoining = message.header.current_time;
@ -108,8 +162,17 @@ Meteor.startup(() => {
return userJoined(meetingId, userObj, callback); return userJoined(meetingId, userObj, callback);
} }
} }
// only process if requester is nodeJSapp means only process in the case when
// we explicitly request the users
} else if(eventName === 'get_users_reply' && message.payload.requester_id === 'nodeJSapp') { } else if(eventName === 'get_users_reply' && message.payload.requester_id === 'nodeJSapp') {
users = message.payload.users; users = message.payload.users;
//TODO make the serialization be split per meeting. This will allow us to
// use N threads vs 1 and we'll take advantage of Mongo's concurrency tricks
// Processing the users recursively with a callback to notify us,
// ensuring that we update the users collection serially
processUser = function() { processUser = function() {
let user; let user;
user = users.pop(); user = users.pop();
@ -120,10 +183,11 @@ Meteor.startup(() => {
user.set_emoji_time = new Date(); user.set_emoji_time = new Date();
return userJoined(meetingId, user, processUser); return userJoined(meetingId, user, processUser);
} else { } else {
// console.error("this is not supposed to happen")
return userJoined(meetingId, user, processUser); return userJoined(meetingId, user, processUser);
} }
} else { } else {
return callback(); return callback(); // all meeting arrays (if any) have been processed
} }
}; };
return processUser(); return processUser();
@ -134,7 +198,10 @@ Meteor.startup(() => {
meetingId: meetingId meetingId: meetingId
}); });
validStatus = message.payload.valid; validStatus = message.payload.valid;
// if the user already exists in the db
if((user != null ? user.clientType : void 0) === "HTML5") { if((user != null ? user.clientType : void 0) === "HTML5") {
//if the html5 client user was validated successfully, add a flag
return Meteor.Users.update({ return Meteor.Users.update({
userId: userId, userId: userId,
meetingId: message.payload.meeting_id meetingId: message.payload.meeting_id
@ -168,11 +235,14 @@ Meteor.startup(() => {
if((userId != null) && (meetingId != null)) { if((userId != null) && (meetingId != null)) {
return markUserOffline(meetingId, userId, callback); return markUserOffline(meetingId, userId, callback);
} else { } else {
return callback(); return callback(); //TODO check how to get these cases out and reuse code
} }
// for now not handling this serially #TODO
} else if(eventName === 'presenter_assigned_message') { } else if(eventName === 'presenter_assigned_message') {
newPresenterId = message.payload.new_presenter_id; newPresenterId = message.payload.new_presenter_id;
if(newPresenterId != null) { if(newPresenterId != null) {
// reset the previous presenter
Meteor.Users.update({ Meteor.Users.update({
"user.presenter": true, "user.presenter": true,
meetingId: meetingId meetingId: meetingId
@ -183,6 +253,7 @@ Meteor.startup(() => {
}, (err, numUpdated) => { }, (err, numUpdated) => {
return Meteor.log.info(` Updating old presenter numUpdated=${numUpdated}, err=${err}`); return Meteor.log.info(` Updating old presenter numUpdated=${numUpdated}, err=${err}`);
}); });
// set the new presenter
Meteor.Users.update({ Meteor.Users.update({
"user.userid": newPresenterId, "user.userid": newPresenterId,
meetingId: meetingId meetingId: meetingId
@ -195,6 +266,8 @@ Meteor.startup(() => {
}); });
} }
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === 'user_emoji_status_message') { } else if(eventName === 'user_emoji_status_message') {
userId = message.payload.userid; userId = message.payload.userid;
meetingId = message.payload.meeting_id; meetingId = message.payload.meeting_id;
@ -213,11 +286,15 @@ Meteor.startup(() => {
}); });
} }
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === 'user_locked_message' || eventName === 'user_unlocked_message') { } else if(eventName === 'user_locked_message' || eventName === 'user_unlocked_message') {
userId = message.payload.userid; userId = message.payload.userid;
isLocked = message.payload.locked; isLocked = message.payload.locked;
setUserLockedStatus(meetingId, userId, isLocked); setUserLockedStatus(meetingId, userId, isLocked);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "meeting_ended_message" || eventName === "meeting_destroyed_event" || eventName === "end_and_kick_all_message" || eventName === "disconnect_all_users_message") { } else if(eventName === "meeting_ended_message" || eventName === "meeting_destroyed_event" || eventName === "end_and_kick_all_message" || eventName === "disconnect_all_users_message") {
Meteor.log.info(`DESTROYING MEETING ${meetingId}`); Meteor.log.info(`DESTROYING MEETING ${meetingId}`);
return removeMeetingFromCollection(meetingId, callback); return removeMeetingFromCollection(meetingId, callback);
@ -232,6 +309,8 @@ Meteor.startup(() => {
unless eventName is "disconnect_all_users_message" unless eventName is "disconnect_all_users_message"
removeMeetingFromCollection meetingId removeMeetingFromCollection meetingId
*/ */
// for now not handling this serially #TODO
} else if(eventName === "get_chat_history_reply" && message.payload.requester_id === "nodeJSapp") { } else if(eventName === "get_chat_history_reply" && message.payload.requester_id === "nodeJSapp") {
if(Meteor.Meetings.findOne({ if(Meteor.Meetings.findOne({
MeetingId: message.payload.meeting_id MeetingId: message.payload.meeting_id
@ -243,13 +322,19 @@ Meteor.startup(() => {
} }
} }
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "send_public_chat_message" || eventName === "send_private_chat_message") { } else if(eventName === "send_public_chat_message" || eventName === "send_private_chat_message") {
messageObject = message.payload.message; messageObject = message.payload.message;
// use current_time instead of message.from_time so that the chats from Flash and HTML5 have uniform times
messageObject.from_time = message.header.current_time; messageObject.from_time = message.header.current_time;
addChatToCollection(meetingId, messageObject); addChatToCollection(meetingId, messageObject);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_shared_message") { } else if(eventName === "presentation_shared_message") {
presentationId = (ref7 = message.payload.presentation) != null ? ref7.id : void 0; presentationId = (ref7 = message.payload.presentation) != null ? ref7.id : void 0;
// change the currently displayed presentation to presentation.current = false
Meteor.Presentations.update({ Meteor.Presentations.update({
"presentation.current": true, "presentation.current": true,
meetingId: meetingId meetingId: meetingId
@ -258,6 +343,8 @@ Meteor.startup(() => {
"presentation.current": false "presentation.current": false
} }
}); });
//update(if already present) entirely the presentation with the fresh data
removePresentationFromCollection(meetingId, presentationId); removePresentationFromCollection(meetingId, presentationId);
addPresentationToCollection(meetingId, message.payload.presentation); addPresentationToCollection(meetingId, message.payload.presentation);
ref9 = (ref8 = message.payload.presentation) != null ? ref8.pages : void 0; ref9 = (ref8 = message.payload.presentation) != null ? ref8.pages : void 0;
@ -273,6 +360,8 @@ Meteor.startup(() => {
} }
} }
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "get_presentation_info_reply" && message.payload.requester_id === "nodeJSapp") { } else if(eventName === "get_presentation_info_reply" && message.payload.requester_id === "nodeJSapp") {
ref11 = message.payload.presentations; ref11 = message.payload.presentations;
for(k = 0, len2 = ref11.length; k < len2; k++) { for(k = 0, len2 = ref11.length; k < len2; k++) {
@ -281,8 +370,14 @@ Meteor.startup(() => {
ref12 = presentation.pages; ref12 = presentation.pages;
for(l = 0, len3 = ref12.length; l < len3; l++) { for(l = 0, len3 = ref12.length; l < len3; l++) {
page = ref12[l]; page = ref12[l];
//add the slide to the collection
addSlideToCollection(meetingId, presentation.id, page); addSlideToCollection(meetingId, presentation.id, page);
whiteboardId = `${presentation.id}/${page.num}`;
//request for shapes
whiteboardId = `${presentation.id}/${page.num}`; // d2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1404411622872/1
//Meteor.log.info "the whiteboard_id here is:" + whiteboardId
replyTo = `${meetingId}/nodeJSapp`; replyTo = `${meetingId}/nodeJSapp`;
message = { message = {
"payload": { "payload": {
@ -304,16 +399,23 @@ Meteor.startup(() => {
} }
} }
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_page_changed_message") { } else if(eventName === "presentation_page_changed_message") {
newSlide = message.payload.page; newSlide = message.payload.page;
displayThisSlide(meetingId, newSlide != null ? newSlide.id : void 0, newSlide); displayThisSlide(meetingId, newSlide != null ? newSlide.id : void 0, newSlide);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_removed_message") { } else if(eventName === "presentation_removed_message") {
presentationId = message.payload.presentation_id; presentationId = message.payload.presentation_id;
meetingId = message.payload.meeting_id; meetingId = message.payload.meeting_id;
removePresentationFromCollection(meetingId, presentationId); removePresentationFromCollection(meetingId, presentationId);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "get_whiteboard_shapes_reply" && message.payload.requester_id === "nodeJSapp") { } else if(eventName === "get_whiteboard_shapes_reply" && message.payload.requester_id === "nodeJSapp") {
// Create a whiteboard clean status or find one for the current meeting
if(Meteor.WhiteboardCleanStatus.findOne({ if(Meteor.WhiteboardCleanStatus.findOne({
meetingId: meetingId meetingId: meetingId
}) == null) { }) == null) {
@ -329,7 +431,11 @@ Meteor.startup(() => {
addShapeToCollection(meetingId, whiteboardId, shape); addShapeToCollection(meetingId, whiteboardId, shape);
} }
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "send_whiteboard_shape_message") { } else if(eventName === "send_whiteboard_shape_message") {
//Meteor stringifies an array of JSONs (...shape.result) in this message
//parsing the String and reassigning the value
if(message.payload.shape.shape_type === "poll_result" && typeof message.payload.shape.shape.result === 'string') { if(message.payload.shape.shape_type === "poll_result" && typeof message.payload.shape.shape.result === 'string') {
message.payload.shape.shape.result = JSON.parse(message.payload.shape.shape.result); message.payload.shape.shape.result = JSON.parse(message.payload.shape.shape.result);
} }
@ -337,13 +443,19 @@ Meteor.startup(() => {
whiteboardId = shape != null ? shape.wb_id : void 0; whiteboardId = shape != null ? shape.wb_id : void 0;
addShapeToCollection(meetingId, whiteboardId, shape); addShapeToCollection(meetingId, whiteboardId, shape);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_cursor_updated_message") { } else if(eventName === "presentation_cursor_updated_message") {
cursor = { cursor = {
x: message.payload.x_percent, x: message.payload.x_percent,
y: message.payload.y_percent y: message.payload.y_percent
}; };
// update the location of the cursor on the whiteboard
updateCursorLocation(meetingId, cursor); updateCursorLocation(meetingId, cursor);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "whiteboard_cleared_message") { } else if(eventName === "whiteboard_cleared_message") {
whiteboardId = message.payload.whiteboard_id; whiteboardId = message.payload.whiteboard_id;
Meteor.WhiteboardCleanStatus.update({ Meteor.WhiteboardCleanStatus.update({
@ -355,11 +467,15 @@ Meteor.startup(() => {
}); });
removeAllShapesFromSlide(meetingId, whiteboardId); removeAllShapesFromSlide(meetingId, whiteboardId);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "undo_whiteboard_request") { } else if(eventName === "undo_whiteboard_request") {
whiteboardId = message.payload.whiteboard_id; whiteboardId = message.payload.whiteboard_id;
shapeId = message.payload.shape_id; shapeId = message.payload.shape_id;
removeShapeFromSlide(meetingId, whiteboardId, shapeId); removeShapeFromSlide(meetingId, whiteboardId, shapeId);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_page_resized_message") { } else if(eventName === "presentation_page_resized_message") {
slideId = (ref14 = message.payload.page) != null ? ref14.id : void 0; slideId = (ref14 = message.payload.page) != null ? ref14.id : void 0;
heightRatio = (ref15 = message.payload.page) != null ? ref15.height_ratio : void 0; heightRatio = (ref15 = message.payload.page) != null ? ref15.height_ratio : void 0;
@ -379,6 +495,8 @@ Meteor.startup(() => {
} }
}); });
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "recording_status_changed_message") { } else if(eventName === "recording_status_changed_message") {
intendedForRecording = message.payload.recorded; intendedForRecording = message.payload.recorded;
currentlyBeingRecorded = message.payload.recording; currentlyBeingRecorded = message.payload.recording;
@ -391,16 +509,26 @@ Meteor.startup(() => {
} }
}); });
return callback(); return callback();
// --------------------------------------------------
// lock settings ------------------------------------
// for now not handling this serially #TODO
} else if(eventName === "eject_voice_user_message") { } else if(eventName === "eject_voice_user_message") {
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "new_permission_settings") { } else if(eventName === "new_permission_settings") {
oldSettings = (ref19 = Meteor.Meetings.findOne({ oldSettings = (ref19 = Meteor.Meetings.findOne({
meetingId: meetingId meetingId: meetingId
})) != null ? ref19.roomLockSettings : void 0; })) != null ? ref19.roomLockSettings : void 0;
newSettings = (ref20 = message.payload) != null ? ref20.permissions : void 0; newSettings = (ref20 = message.payload) != null ? ref20.permissions : void 0;
// if the disableMic setting was turned on
if(!(oldSettings != null ? oldSettings.disableMic : void 0) && newSettings.disableMic) { if(!(oldSettings != null ? oldSettings.disableMic : void 0) && newSettings.disableMic) {
handleLockingMic(meetingId, newSettings); handleLockingMic(meetingId, newSettings);
} }
// substitute with the new lock settings
Meteor.Meetings.update({ Meteor.Meetings.update({
meetingId: meetingId meetingId: meetingId
}, { }, {
@ -415,6 +543,8 @@ Meteor.startup(() => {
} }
}); });
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "poll_started_message") { } else if(eventName === "poll_started_message") {
if((message.payload.meeting_id != null) && (message.payload.requester_id != null) && (message.payload.poll != null)) { if((message.payload.meeting_id != null) && (message.payload.requester_id != null) && (message.payload.poll != null)) {
if(Meteor.Meetings.findOne({ if(Meteor.Meetings.findOne({
@ -437,11 +567,15 @@ Meteor.startup(() => {
} }
} }
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "poll_stopped_message") { } else if(eventName === "poll_stopped_message") {
meetingId = message.payload.meeting_id; meetingId = message.payload.meeting_id;
poll_id = message.payload.poll_id; poll_id = message.payload.poll_id;
clearPollCollection(meetingId, poll_id); clearPollCollection(meetingId, poll_id);
return callback(); return callback();
// for now not handling this serially #TODO
} else if(eventName === "user_voted_poll_message") { } else if(eventName === "user_voted_poll_message") {
if((((ref21 = message.payload) != null ? ref21.poll : void 0) != null) && (message.payload.meeting_id != null) && (message.payload.presenter_id != null)) { if((((ref21 = message.payload) != null ? ref21.poll : void 0) != null) && (message.payload.meeting_id != null) && (message.payload.presenter_id != null)) {
pollObj = message.payload.poll; pollObj = message.payload.poll;
@ -450,6 +584,8 @@ Meteor.startup(() => {
updatePollCollection(pollObj, meetingId, requesterId); updatePollCollection(pollObj, meetingId, requesterId);
return callback(); return callback();
} }
// for now not handling this serially #TODO
} else if(eventName === "poll_show_result_message") { } else if(eventName === "poll_show_result_message") {
if((message.payload.poll.id != null) && (message.payload.meeting_id != null)) { if((message.payload.poll.id != null) && (message.payload.meeting_id != null)) {
poll_id = message.payload.poll.id; poll_id = message.payload.poll.id;
@ -457,7 +593,7 @@ Meteor.startup(() => {
clearPollCollection(meetingId, poll_id); clearPollCollection(meetingId, poll_id);
} }
return callback(); return callback();
} else { } else { // keep moving in the queue
if(indexOf.call(notLoggedEventTypes, eventName) < 0) { if(indexOf.call(notLoggedEventTypes, eventName) < 0) {
Meteor.log.info(`WARNING!!! THE JSON MESSAGE WAS NOT OF TYPE SUPPORTED BY THIS APPLICATION Meteor.log.info(`WARNING!!! THE JSON MESSAGE WAS NOT OF TYPE SUPPORTED BY THIS APPLICATION
${eventName} {JSON.stringify message}`); ${eventName} {JSON.stringify message}`);

View File

@ -2,37 +2,71 @@ let moderator, presenter, viewer;
presenter = { presenter = {
switchSlide: true, switchSlide: true,
//poll
subscribePoll: true, subscribePoll: true,
subscribeAnswers: true subscribeAnswers: true
}; };
// holds the values for whether the moderator user is allowed to perform an action (true)
// or false if not allowed. Some actions have dynamic values depending on the current lock settings
moderator = { moderator = {
// audio listen only
joinListenOnly: true, joinListenOnly: true,
leaveListenOnly: true, leaveListenOnly: true,
// join audio with mic cannot be controlled on the server side as it is
// a client side only functionality
// raising/lowering hand
raiseOwnHand: true, raiseOwnHand: true,
lowerOwnHand: true, lowerOwnHand: true,
// muting
muteSelf: true, muteSelf: true,
unmuteSelf: true, unmuteSelf: true,
logoutSelf: true, logoutSelf: true,
//subscribing
subscribeUsers: true, subscribeUsers: true,
subscribeChat: true, subscribeChat: true,
//chat
chatPublic: true, chatPublic: true,
chatPrivate: true, chatPrivate: true,
//poll
subscribePoll: true, subscribePoll: true,
subscribeAnswers: false, subscribeAnswers: false,
//emojis
setEmojiStatus: true, setEmojiStatus: true,
clearEmojiStatus: true, clearEmojiStatus: true,
//user control
kickUser: true, kickUser: true,
setPresenter: true setPresenter: true
}; };
// holds the values for whether the viewer user is allowed to perform an action (true)
// or false if not allowed. Some actions have dynamic values depending on the current lock settings
viewer = function(meetingId, userId) { viewer = function(meetingId, userId) {
let ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7; let ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7;
return { return {
// listen only
joinListenOnly: true, joinListenOnly: true,
leaveListenOnly: true, leaveListenOnly: true,
// join audio with mic cannot be controlled on the server side as it is
// a client side only functionality
// raising/lowering hand
raiseOwnHand: true, raiseOwnHand: true,
lowerOwnHand: true, lowerOwnHand: true,
// muting
muteSelf: true, muteSelf: true,
unmuteSelf: !((ref = Meteor.Meetings.findOne({ unmuteSelf: !((ref = Meteor.Meetings.findOne({
meetingId: meetingId meetingId: meetingId
@ -40,9 +74,14 @@ viewer = function(meetingId, userId) {
meetingId: meetingId, meetingId: meetingId,
userId: userId userId: userId
})) != null ? ref1.user.locked : void 0), })) != null ? ref1.user.locked : void 0),
logoutSelf: true, logoutSelf: true,
//subscribing
subscribeUsers: true, subscribeUsers: true,
subscribeChat: true, subscribeChat: true,
//chat
chatPublic: !((ref2 = Meteor.Meetings.findOne({ chatPublic: !((ref2 = Meteor.Meetings.findOne({
meetingId: meetingId meetingId: meetingId
})) != null ? ref2.roomLockSettings.disablePublicChat : void 0) || !((ref3 = Meteor.Users.findOne({ })) != null ? ref2.roomLockSettings.disablePublicChat : void 0) || !((ref3 = Meteor.Users.findOne({
@ -61,13 +100,19 @@ viewer = function(meetingId, userId) {
meetingId: meetingId, meetingId: meetingId,
userId: userId userId: userId
})) != null ? ref7.user.presenter : void 0), })) != null ? ref7.user.presenter : void 0),
//poll
subscribePoll: true, subscribePoll: true,
subscribeAnswers: false, subscribeAnswers: false,
//emojis
setEmojiStatus: true, setEmojiStatus: true,
clearEmojiStatus: true clearEmojiStatus: true
}; };
}; };
// carries out the decision making for actions affecting users. For the list of
// actions and the default value - see 'viewer' and 'moderator' in the beginning of the file
this.isAllowedTo = function(action, meetingId, userId, authToken) { this.isAllowedTo = function(action, meetingId, userId, authToken) {
let ref, ref1, ref2, ref3, user, validated; let ref, ref1, ref2, ref3, user, validated;
validated = (ref = Meteor.Users.findOne({ validated = (ref = Meteor.Users.findOne({
@ -79,14 +124,22 @@ this.isAllowedTo = function(action, meetingId, userId, authToken) {
meetingId: meetingId, meetingId: meetingId,
userId: userId userId: userId
}); });
if((user != null) && authToken === user.authToken) { // Meteor.log.info "user=" + JSON.stringify user
if((user != null) && authToken === user.authToken) { // check if the user is who he claims to be
if(user.validated && user.clientType === "HTML5") { if(user.validated && user.clientType === "HTML5") {
// PRESENTER
// check presenter specific actions or fallback to regular viewer actions
if((ref1 = user.user) != null ? ref1.presenter : void 0) { if((ref1 = user.user) != null ? ref1.presenter : void 0) {
Meteor.log.info("user permissions presenter case"); Meteor.log.info("user permissions presenter case");
return presenter[action] || viewer(meetingId, userId)[action] || false; return presenter[action] || viewer(meetingId, userId)[action] || false;
// VIEWER
} else if(((ref2 = user.user) != null ? ref2.role : void 0) === 'VIEWER') { } else if(((ref2 = user.user) != null ? ref2.role : void 0) === 'VIEWER') {
Meteor.log.info("user permissions viewer case"); Meteor.log.info("user permissions viewer case");
return viewer(meetingId, userId)[action] || false; return viewer(meetingId, userId)[action] || false;
// MODERATOR
} else if(((ref3 = user.user) != null ? ref3.role : void 0) === 'MODERATOR') { } else if(((ref3 = user.user) != null ? ref3.role : void 0) === 'MODERATOR') {
Meteor.log.info("user permissions moderator case"); Meteor.log.info("user permissions moderator case");
return moderator[action] || false; return moderator[action] || false;
@ -95,7 +148,9 @@ this.isAllowedTo = function(action, meetingId, userId, authToken) {
return false; return false;
} }
} else { } else {
// user was not validated
if(action === "logoutSelf") { if(action === "logoutSelf") {
// on unsuccessful sign-in
Meteor.log.warn("a user was successfully removed from the meeting following an unsuccessful login"); Meteor.log.warn("a user was successfully removed from the meeting following an unsuccessful login");
return true; return true;
} }