Merge branch 'master' of github.com:bigbluebutton/bigbluebutton

This commit is contained in:
Jesus Federico 2016-01-19 07:34:34 -08:00
commit 1cb3fe6601
83 changed files with 7766 additions and 6015 deletions

View File

@ -196,7 +196,7 @@ bbb.presentation.uploadcomplete = Upload completed. Please wait while we convert
bbb.presentation.uploaded = uploaded.
bbb.presentation.document.supported = The uploaded document is supported. Starting to convert...
bbb.presentation.document.converted = Successfully converted the office document.
bbb.presentation.error.document.convert.failed = Error: Failed to convert the office document.
bbb.presentation.error.document.convert.failed = Error: Unable to convert the office document.
bbb.presentation.error.io = IO Error: Please contact administrator.
bbb.presentation.error.security = Security Error: Please contact administrator.
bbb.presentation.error.convert.notsupported = Error: The uploaded document is unsupported. Please upload a compatible file.
@ -325,7 +325,7 @@ bbb.layout.combo.customName = Custom layout
bbb.layout.combo.remote = Remote
bbb.layout.save.complete = Layouts were successfully saved
bbb.layout.load.complete = Layouts were successfully loaded
bbb.layout.load.failed = Failed to load the layouts
bbb.layout.load.failed = Unable to load the layouts
bbb.layout.name.defaultlayout = Default Layout
bbb.layout.name.videochat = Video Chat
bbb.layout.name.webcamsfocus = Webcam Meeting
@ -353,7 +353,7 @@ bbb.logout.button.label = OK
bbb.logout.appshutdown = The server app has been shut down
bbb.logout.asyncerror = An Async Error occured
bbb.logout.connectionclosed = The connection to the server has been closed
bbb.logout.connectionfailed = The connection to the server has failed
bbb.logout.connectionfailed = The connection to the server has ended
bbb.logout.rejected = The connection to the server has been rejected
bbb.logout.invalidapp = The red5 app does not exist
bbb.logout.unknown = Your client has lost connection with the server

View File

@ -64,7 +64,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
} else {
LOGGER.debug("fontSize in config.xml not found: {0}", [chatOptions.fontSize]);
}
chatNoiseCheckBox.visible = Accessibility.active;
chatNoiseCheckBox.selected = Accessibility.active;
changeChatNoise();
}
public function accessibleClick(event:KeyboardEvent):void{
@ -140,5 +141,5 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
selectedIndex="1" toolTip="{ResourceUtil.getInstance().getString('bbb.chat.cmbFontSize.toolTip')}" />
</mx:HBox>
<mx:CheckBox id="chatNoiseCheckBox" label="Audible Chat Notification" labelPlacement="left"
selected="true" change="changeChatNoise()" styleName="chatOptionsLabel" />
change="changeChatNoise()" styleName="chatOptionsLabel" />
</mx:VBox>

View File

@ -71,7 +71,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private var publicWaiting:Boolean = false;
private var publicFocus:Boolean = false;
private var noticeLabel:String;
private var chatNoiseEnabled:Boolean = true;
private var chatNoiseEnabled:Boolean = false;
[Embed(source="../sounds/notice.mp3")]
private var noticeSoundClass:Class;
@ -309,7 +309,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
}
private function playSound():void {
if (Accessibility.active && chatNoiseEnabled){
if (chatNoiseEnabled){
noticeSound.play();
}
}

View File

@ -33,7 +33,7 @@ package org.bigbluebutton.modules.phone.managers
public class WebRTCCallManager
{
private static const LOGGER:ILogger = getClassLogger(WebRTCCallManager);
private const MAX_RETRIES:Number = 5;
private const MAX_RETRIES:Number = 3;
private var browserType:String = "unknown";
private var browserVersion:int = 0;

105
bigbluebutton-config/bin/bbb-conf Normal file → Executable file
View File

@ -49,7 +49,7 @@
# 2014-01-13 FFD Working on updates for 0.9.0
# 2014-03-10 GUG Enable Webrtc
# 2015-03-12 FFD Added start/stop of HTML5 server
# 2014-01-13 FFD Working on updates for 1.0.0
# 2016-01-13 FFD Updates for 1.0
#set -x
#set -e
@ -372,7 +372,12 @@ enable_webrtc(){
# Enable port 5066 in FreeSWITCH
sed -i 's/^.*<!--<param name="ws-binding" value=":5066"\/>-->.*$/\t<param name="ws-binding" value=":5066"\/>/g' /opt/freeswitch/conf/sip_profiles/external.xml
sed -i "s/proxy_pass .*/proxy_pass http:\/\/$IP:5066;/g" /etc/bigbluebutton/nginx/sip.nginx
PROTOCOL=$(cat /etc/bigbluebutton/nginx/sip.nginx | sed -n '/proxy_pass/{s/.*proxy_pass [ ]*//;s/:.*//;p}')
PORT=5066
if [[ $PROTOCOL == "https" ]]; then
PORT=7443
fi
sed -i "s/proxy_pass .*/proxy_pass $PROTOCOL:\/\/$IP:$PORT;/g" /etc/bigbluebutton/nginx/sip.nginx
echo
echo "WebRTC audio enabled. To apply settings to your server, do"
@ -395,7 +400,8 @@ disable_webrtc(){
# Disable port 5066 in FreeSWITCH
sed -i 's/^.*<param name="ws-binding" value=":5066"\/>.*$/\t<!--<param name="ws-binding" value=":5066"\/>-->/g' /opt/freeswitch/conf/sip_profiles/external.xml
sed -i "s/proxy_pass .*/proxy_pass http:\/\/127.0.0.1:5066;/g" /etc/bigbluebutton/nginx/sip.nginx
PROTOCOL=$(cat /etc/bigbluebutton/nginx/sip.nginx | sed -n '/proxy_pass/{s/.*proxy_pass [ ]*//;s/:.*//;p}')
sed -i "s/proxy_pass .*/proxy_pass $PROTOCOL:\/\/127.0.0.1:5066;/g" /etc/bigbluebutton/nginx/sip.nginx
echo
echo "WebRTC audio disabled. To apply settings to your server, do"
@ -807,8 +813,18 @@ check_configuration() {
fi
fi
libreoffice_version=`dpkg-query -W --showformat='${Version}\n' libreoffice | sed 's/.*://g' | sed 's/\.[^\.]*$//g'`
PROTOCOL=$(cat /etc/bigbluebutton/nginx/sip.nginx | sed -n '/proxy_pass/{s/.*proxy_pass [ ]*//;s/:.*//;p}')
if [[ $PROTOCOL == "https" ]]; then
if ! grep wss-binding /opt/freeswitch/conf/sip_profiles/external.xml > /dev/null; then
echo "# Warning: Websockets is using HTTPS in /etc/bigbluebutton/nginx/sip.nginx"
echo "# but no definition for wss-binding found in "
echo "#"
echo "# /opt/freeswitch/conf/sip_profiles/external.xml"
echo
fi
fi
libreoffice_version=`dpkg-query -W --showformat='${Version}\n' libreoffice | sed 's/.*://g' | sed 's/\.[^\.]*$//g'`
if [[ "$libreoffice_version" > 1.0 ]]; then
if [[ "$libreoffice_version" < 4.4 ]]; then
echo "# Warning: Detected your running an older version of LibreOffice: $libreoffice_version"
@ -1103,33 +1119,57 @@ check_state() {
fi
BBB_SIP_APP_IP=$(cat /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties | grep -v '#' | sed -n '/^bbb.sip.app.ip=/{s/.*=//;s/;//;p}')
if [ $BBB_SIP_APP_IP != "127.0.0.1" ]; then
if [ "$BBB_SIP_APP_IP" != $IP ]; then
echo "# Warning: The setting of ($BBB_SIP_APP_IP) for bbb.sip.app.ip in"
echo "#"
echo "# /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties"
echo "#"
echo "# does not match the local IP address ($IP)."
echo "# (This is OK if you've manually changed the values to an external "
echo "# FreeSWITCH server.)"
echo
fi
SIP_IP=$(netstat -ant | grep 5060 | head -n1 | awk -F" " '{print $4}' | cut -d: -f1)
if [ -z $SIP_IP ]; then
echo "# Error: Could not detect FreeSWITCH listening on port 5060"
echo
else
if [ "$BBB_SIP_APP_IP" != $SIP_IP ]; then
echo "# Error: FreeSWITCH is listening on IP address $SIP_IP for SIP calls, but "
echo "# The IP address ($BBB_SIP_APP_IP) set bbb.sip.app.ip."
echo "#"
echo
fi
fi
BBB_SIP_APP_IP=$(cat /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties | grep -v '#' | sed -n '/^bbb.sip.app.ip=/{s/.*=//;s/;//;p}')
if [ $BBB_SIP_APP_IP != "127.0.0.1" ]; then
if [ "$BBB_SIP_APP_IP" != $IP ]; then
echo "# Warning: The setting of $BBB_SIP_APP_IP for bbb.sip.app.ip in"
echo "#"
echo "# /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties"
echo "#"
echo "# does not match the local IP address ($IP)."
echo "# (This is OK if you've manually changed the values to an external "
echo "# FreeSWITCH server.)"
echo
fi
SIP_IP=$(netstat -ant | grep 5060 | head -n1 | awk -F" " '{print $4}' | cut -d: -f1)
if [ -z $SIP_IP ]; then
echo "# Error: Could not detect FreeSWITCH listening on port 5060"
echo
else
if [ "$BBB_SIP_APP_IP" != $SIP_IP ]; then
echo "# Error: FreeSWITCH is listening on IP address $SIP_IP for SIP calls, but "
echo "# The IP address ($BBB_SIP_APP_IP) set bbb.sip.app.ip."
echo "#"
echo
fi
fi
fi
SIP_NGINX_IP=$(cat /etc/bigbluebutton/nginx/sip.nginx | grep -v '#' | sed -n '/proxy_pass/{s/.*proxy_pass http[s]*:\/\///;s/:.*//;p}')
if [ "$SIP_NGINX_IP" != $IP ]; then
echo "# Warning: The setting of $SIP_NGINX_IP for proxy_pass in"
echo "#"
echo "# /etc/bigbluebutton/nginx/sip.nginx"
echo "#"
echo "# does not match the local IP address ($IP)."
echo "# (This is OK if you've manually changed the values)"
echo
fi
VARS_IP=$(cat /opt/freeswitch/conf/vars.xml | sed -n '/"local_ip_v4/{s/.*local_ip_v4=//;s/".*//;p}')
if [[ "$VARS_IP" != "127.0.0.1" ]] && [[ "$VARS_IP" != "auto" ]]; then
if [ "$VARS_IP" != $IP ]; then
echo "# Warning: The setting of $VARS_IP for local_ip_v4 in"
echo "#"
echo "# /opt/freeswitch/conf/vars.xml"
echo "#"
echo "# does not match the local IP address ($IP)."
echo "# (This is OK if you've manually changed the values)"
echo
fi
fi
if [ -d ${SERVLET_DIR}/lti ]; then
if test ${SERVLET_DIR}/lti.war -nt ${SERVLET_DIR}/lti; then
echo "# Error: The updated lti.war did not deploy. To manually deploy:"
@ -1290,8 +1330,11 @@ if [ $CHECK ]; then
NGINX_IP=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/server_name/{s/.*name[ ]*//;s/;//;p}' | cut -d' ' -f1)
echo " server name: $NGINX_IP"
PORT=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/listen/{s/.*listen[ ]*//;s/;//;p}')
echo " port: $PORT"
PORT=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/listen/{s/.*listen[ ]*//;s/;//;p}' | grep -v ssl)
echo " port: $PORT"
if cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/listen/{s/.*listen[ ]*//;s/;//;p}' | grep ssl > /dev/null; then
echo " port: 443 ssl"
fi
BBB_CLIENT_DOC_ROOT=$(cat /etc/bigbluebutton/nginx/client.nginx | grep -v '#' | grep \/client -A 1 | head -n 2 | grep root | sed -n '{s/[ \t]*root[ ]*//;s/;//;p}')
echo " bbb-client dir: $BBB_CLIENT_DOC_ROOT"

View File

@ -4,7 +4,6 @@
# but you can also edit it by hand.
standard-app-packages
coffeescript
mrt:redis@0.1.3
arunoda:npm@0.2.6
underscore
@ -32,3 +31,4 @@ cfs:power-queue
cfs:reactive-list
cfs:micro-queue
reactive-var
ecmascript

View File

@ -1,91 +0,0 @@
# Methods return a reference to itself to allow chaining
class @NotificationControl
container = '' # holds where the alerts will go
notifications = {}
constructor: (c) ->
container = if c[0] is '#' then c.substr(1) else c # prepend '#' to the identifier
$("#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>")
# 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) ->
elementId = if id[0] is '#' then id.substr(1) else id # remove prepended '#' from the identifier
notifications[elementId] = {}
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 += "#{content}"
notifications[elementId].element += '<button href="#" tabindex="0" class="close" aria-label="Close Alert">&times;</button>'
notifications[elementId].element += '</div>'
notifications[elementId].duration = nDuration or -1 # if no time is specified, it must be dismissed by the user
notifications[elementId].fadeTime = nFadeTime or 1000
@
registerShow: (elementId, nShowNotification) -> # register the method to be called when showing the notification
notifications[elementId].showNotification = nShowNotification
@
registerHide: (elementId, nHideNotification) -> # register the method called when hiding the notification
notifications[elementId].hideNotification = nHideNotification
@
display: (elementId) -> # called the registered methods
$('#'+container).append(notifications[elementId].element) # display the notification
notifications[elementId].showNotification?()
setTimeout () =>
# remove the notification if the user selected to
@hideANotification(elementId) if notifications[elementId].duration > 0
notifications[elementId].hideNotification?()
notifications[elementId] = {} # delete all notification data
, notifications[elementId].duration
@
# hides a notification that may have been left over
hideANotification: (elementId) ->
($('#'+elementId).fadeOut notifications[elementId].fadeTime, -> $('#'+elementId).remove())
notifications[elementId].hideNotification?()
notifications[elementId] = {}
# static icon members
@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'
# 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'
# 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'
}
@notification_WebRTCAudioExited = -> # used when the user can join audio
Meteor.NotificationControl.create("webRTC_AudioExited", ' ', 'You have exited audio', 2500).display("webRTC_AudioExited")
@notification_WebRTCAudioJoining = -> # 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")
# joined. Displayed joined notification and hide the joining notification
Tracker.autorun (comp) -> # wait until user is in
if BBB.amIInAudio() # display notification when you are in audio
comp.stop() # prevents computation from running twice (which can happen occassionally)
Meteor.NotificationControl.create("webRTC_AudioJoined", 'success ', '', 2500)
.registerShow("webRTC_AudioJoined", ->
Meteor.NotificationControl.hideANotification('webRTC_AudioJoining')
$("#webRTC_AudioJoined").prepend("You've joined the #{if BBB.amIListenOnlyAudio() then 'Listen Only' else ''} audio")
).display("webRTC_AudioJoined")
@notification_WebRTCNotSupported = -> # shown when the user's browser does not support WebRTC
# create a new notification at the audio button they clicked to trigger the event
Meteor.NotificationControl.create("webRTC_NotSupported", 'alert', '', -1)
.registerShow("webRTC_NotSupported", ->
if ((browserName=getBrowserName()) in ['Safari', 'IE']) or browserName="settings" # show either the browser icon or cog gears
$("#webRTC_NotSupported").prepend('<div id="browser-icon-container"></div>' +
"Sorry,<br/>#{if browserName isnt 'settings' then browserName else 'your browser'} doesn't support WebRTC")
(new Raphael('browser-icon-container', 35, 35)).path(NotificationControl.icons["#{browserName}_IconPath"]).attr({fill: "#FFF", stroke: "none"})
).display("webRTC_NotSupported")

View File

@ -0,0 +1,127 @@
// Methods return a reference to itself to allow chaining
this.NotificationControl = (() => {
let container, notifications;
container = ''; // holds where the alerts will go
notifications = {};
class NotificationControl {
constructor(c) {
container = c[0] === '#' ? c.substr(1) : c; // prepend '#' to the identifier
$("#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>"}`
);
}
// 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) {
let elementId;
elementId = id[0] === '#' ? id.substr(1) : id; // remove prepended '#' from the identifier
notifications[elementId] = {};
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 += `${content}`;
notifications[elementId].element += '<button href="#" tabindex="0" class="close" aria-label="Close Alert">&times;</button>';
notifications[elementId].element += '</div>';
notifications[elementId].duration = nDuration || -1; // if no time is specified, it must be dismissed by the user
notifications[elementId].fadeTime = nFadeTime || 1000;
return this;
}
registerShow(elementId, nShowNotification) { // register the method to be called when showing the notification
notifications[elementId].showNotification = nShowNotification;
return this;
}
registerHide(elementId, nHideNotification) { // register the method called when hiding the notification
notifications[elementId].hideNotification = nHideNotification;
return this;
}
display(elementId) { // called the registered methods
let base;
$(`#${container}`).append(notifications[elementId].element); // display the notification
if(typeof (base = notifications[elementId]).showNotification === "function") {
base.showNotification();
}
setTimeout((_this => {
// remove the notification if the user selected to
return function() {
let base1;
if(notifications[elementId].duration > 0) {
_this.hideANotification(elementId);
}
if(typeof (base1 = notifications[elementId]).hideNotification === "function") {
base1.hideNotification();
}
return notifications[elementId] = {}; // delete all notification data
};
})(this), notifications[elementId].duration);
return this;
}
// hides a notification that may have been left over
hideANotification(elementId) {
let base;
$(`#${elementId}`).fadeOut(notifications[elementId].fadeTime, () => {
return $(`#${elementId}`).remove();
});
if(typeof (base = notifications[elementId]).hideNotification === "function") {
base.hideNotification();
}
return notifications[elementId] = {};
}
}
// static icon members
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',
// 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',
// 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'
};
return NotificationControl;
})();
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");
};
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");
// joined. Displayed joined notification and hide the joining notification
return Tracker.autorun(comp => { // wait until user is in
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", () => {
Meteor.NotificationControl.hideANotification('webRTC_AudioJoining');
return $("#webRTC_AudioJoined").prepend(`You've joined the ${BBB.amIListenOnlyAudio() ? 'Listen Only' : ''} audio`);
}).display("webRTC_AudioJoined");
}
});
};
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", () => {
let browserName, ref;
if(((ref = (browserName = getBrowserName())) === 'Safari' || ref === 'IE') || (browserName = "settings")) { // show either the browser icon or cog gears
$("#webRTC_NotSupported").prepend(
`<div id="browser-icon-container"></div>${"Sorry,<br/>" + (browserName !== 'settings' ? browserName : 'your browser') + " doesn't support WebRTC"}`
);
return (new Raphael('browser-icon-container', 35, 35)).path(NotificationControl.icons[`${browserName}_IconPath`]).attr({
fill: "#FFF",
stroke: "none"
});
}
}).display("webRTC_NotSupported");
};

View File

@ -1,691 +0,0 @@
@getBuildInformation = ->
copyrightYear = Meteor.config?.copyrightYear or "DATE"
html5ClientBuild = Meteor.config?.html5ClientBuild or "VERSION"
defaultWelcomeMessage = Meteor.config?.defaultWelcomeMessage or "WELCOME MESSAGE"
defaultWelcomeMessageFooter = Meteor.config?.defaultWelcomeMessageFooter or "WELCOME MESSAGE"
link = "<a href='http://bigbluebutton.org/' target='_blank'>http://bigbluebutton.org</a>"
{
'copyrightYear': copyrightYear
'html5ClientBuild': html5ClientBuild
'defaultWelcomeMessage': defaultWelcomeMessage
'defaultWelcomeMessageFooter': defaultWelcomeMessageFooter
'link': link
}
# Convert a color `value` as integer to a hex color (e.g. 255 to #0000ff)
@colourToHex = (value) ->
hex = parseInt(value).toString(16)
hex = "0" + hex while hex.length < 6
"##{hex}"
# color can be a number (a hex converted to int) or a string (e.g. "#ffff00")
@formatColor = (color) ->
color ?= "0" # default value
if !color.toString().match(/\#.*/)
color = colourToHex(color)
color
@getInSession = (k) -> SessionAmplify.get k
@getTime = -> # returns epoch in ms
(new Date).valueOf()
# checks if the pan gesture is mostly horizontal
@isPanHorizontal = (event) ->
Math.abs(event.deltaX) > Math.abs(event.deltaY)
# helper to determine whether user has joined any type of audio
Handlebars.registerHelper "amIInAudio", ->
BBB.amIInAudio()
# helper to determine whether the user is in the listen only audio stream
Handlebars.registerHelper "amIListenOnlyAudio", ->
BBB.amIListenOnlyAudio()
# helper to determine whether the user is in the listen only audio stream
Handlebars.registerHelper "isMyMicLocked", ->
BBB.isMyMicLocked()
Handlebars.registerHelper "colourToHex", (value) =>
@window.colourToHex(value)
Handlebars.registerHelper 'equals', (a, b) -> # equals operator was dropped in Meteor's migration from Handlebars to Spacebars
a is b
Handlebars.registerHelper "getCurrentMeeting", ->
Meteor.Meetings.findOne()
Handlebars.registerHelper "getCurrentSlide", ->
result = BBB.getCurrentSlide("helper getCurrentSlide")
# console.log "result=#{JSON.stringify result}"
result
# Allow access through all templates
Handlebars.registerHelper "getInSession", (k) -> SessionAmplify.get k
Handlebars.registerHelper "getMeetingName", ->
BBB.getMeetingName()
Handlebars.registerHelper "getShapesForSlide", ->
currentSlide = BBB.getCurrentSlide("helper getShapesForSlide")
# try to reuse the lines above
Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id})
# retrieves all users in the meeting
Handlebars.registerHelper "getUsersInMeeting", ->
users = Meteor.Users.find().fetch()
if users?.length > 1
getSortedUserList(users)
else
users
Handlebars.registerHelper "getWhiteboardTitle", ->
(BBB.currentPresentationName() or "Loading presentation...")
Handlebars.registerHelper "getCurrentUserEmojiStatus", ->
BBB.getCurrentUser()?.user?.emoji_status
Handlebars.registerHelper "isCurrentUser", (userId) ->
userId is null or userId is BBB.getCurrentUser()?.userId
Handlebars.registerHelper "isCurrentUserMuted", ->
BBB.amIMuted()
#Retreives a username for a private chat tab from the database if it exists
Handlebars.registerHelper "privateChatName", ->
obj = Meteor.Users.findOne({ userId: getInSession "inChatWith" })
if obj?
obj?.user?.name
Handlebars.registerHelper "isCurrentUserEmojiStatusSet", ->
BBB.isCurrentUserEmojiStatusSet()
Handlebars.registerHelper "isCurrentUserSharingVideo", ->
BBB.amISharingVideo()
Handlebars.registerHelper "isCurrentUserTalking", ->
BBB.amITalking()
Handlebars.registerHelper "isCurrentUserPresenter", ->
BBB.isUserPresenter(getInSession('userId'))
Handlebars.registerHelper "isCurrentUserModerator", ->
BBB.getMyRole() is "MODERATOR"
Handlebars.registerHelper "isDisconnected", ->
return !Meteor.status().connected
Handlebars.registerHelper "isUserInAudio", (userId) ->
BBB.isUserInAudio(userId)
Handlebars.registerHelper "isUserListenOnlyAudio", (userId) ->
BBB.isUserListenOnlyAudio(userId)
Handlebars.registerHelper "isUserMuted", (userId) ->
BBB.isUserMuted(userId)
Handlebars.registerHelper "isUserSharingVideo", (userId) ->
BBB.isUserSharingWebcam(userId)
Handlebars.registerHelper "isUserTalking", (userId) ->
BBB.isUserTalking(userId)
Handlebars.registerHelper 'isMobile', () ->
isMobile()
Handlebars.registerHelper 'isPortraitMobile', () ->
isPortraitMobile()
Handlebars.registerHelper 'isMobileChromeOrFirefox', () ->
isMobile() and ((getBrowserName() is 'Chrome') or (getBrowserName() is 'Firefox'))
Handlebars.registerHelper "meetingIsRecording", ->
BBB.isMeetingRecording()
Handlebars.registerHelper "messageFontSize", ->
style: "font-size: #{getInSession("messageFontSize")}px;"
Handlebars.registerHelper "pointerLocation", ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlideDoc = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
pointer = Meteor.Cursor.findOne()
pointer.x = (- currentSlideDoc.slide.x_offset * 2 + currentSlideDoc.slide.width_ratio * pointer.x) / 100
pointer.y = (- currentSlideDoc.slide.y_offset * 2 + currentSlideDoc.slide.height_ratio * pointer.y) / 100
pointer
Handlebars.registerHelper "safeName", (str) ->
safeString(str)
Handlebars.registerHelper "canJoinWithMic", ->
if (BBB.isUserPresenter(getInSession('userId')) or !Meteor.config.app.listenOnly) and !BBB.isMyMicLocked()
true
else
false
###Handlebars.registerHelper "visibility", (section) ->
if getInSession "display_#{section}"
style: 'display:block;'
else
style: 'display:none;'###
Handlebars.registerHelper "visibility", (section) ->
style: 'display:block;'
Handlebars.registerHelper 'containerPosition', (section) ->
if getInSession 'display_usersList'
return 'moved-to-right'
else if getInSession 'display_menu'
return 'moved-to-left'
else
return ''
# vertically shrinks the whiteboard if the slide navigation controllers are present
Handlebars.registerHelper 'whiteboardSize', (section) ->
if BBB.isUserPresenter(getInSession('userId'))
return 'presenter-whiteboard'
else
if BBB.isPollGoing(getInSession('userId'))
return 'poll-whiteboard'
else
return 'viewer-whiteboard'
Handlebars.registerHelper "getPollQuestions", ->
polls = BBB.getCurrentPoll(getInSession('userId'))
if polls? and polls isnt undefined
number = polls.poll_info.poll.answers.length
widthStyle = "width: calc(75%/" + number + ");"
marginStyle = "margin-left: calc(25%/" + (number*2) + ");" + "margin-right: calc(25%/" + (number*2) + ");"
buttonStyle = widthStyle + marginStyle
for answer in polls.poll_info.poll.answers
answer.style = buttonStyle
return polls.poll_info.poll.answers
@getSortedUserList = (users) ->
if users?.length > 1
users.sort (a, b) ->
if a.user.role is "MODERATOR" and b.user.role is "MODERATOR"
if a.user.set_emoji_time and b.user.set_emoji_time
aTime = a.user.set_emoji_time.getTime()
bTime = b.user.set_emoji_time.getTime()
if aTime < bTime
return -1
else
return 1
else if a.user.set_emoji_time
return -1
else if b.user.set_emoji_time
return 1
else if a.user.role is "MODERATOR"
return -1
else if b.user.role is "MODERATOR"
return 1
else if a.user.set_emoji_time and b.user.set_emoji_time
aTime = a.user.set_emoji_time.getTime()
bTime = b.user.set_emoji_time.getTime()
if aTime < bTime
return -1
else
return 1
else if a.user.set_emoji_time
return -1
else if b.user.set_emoji_time
return 1
else if not a.user.phone_user and not b.user.phone_user
else if not a.user.phone_user
return -1
else if not b.user.phone_user
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
return -1
else if a.user._sort_name > b.user._sort_name
return 1
else if a.user.userid.toLowerCase() > b.user.userid.toLowerCase()
return -1
else if a.user.userid.toLowerCase() < b.user.userid.toLowerCase()
return 1
users
# transform plain text links into HTML tags compatible with Flash client
@linkify = (str) ->
str = str.replace re_weburl, "<a href='event:$&'><u>$&</u></a>"
@setInSession = (k, v) -> SessionAmplify.set k, v
@safeString = (str) ->
if typeof str is 'string'
str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
@toggleCam = (event) ->
# Meteor.Users.update {_id: context._id} , {$set:{"user.sharingVideo": !context.sharingVideo}}
# Meteor.call('userToggleCam', context._id, !context.sharingVideo)
@toggleChatbar = ->
setInSession "display_chatbar", !getInSession "display_chatbar"
if !getInSession("display_chatbar")
$('#whiteboard').css('width', '100%')
$('#whiteboard .ui-resizable-handle').css('display', 'none')
else
$('#whiteboard').css('width', '')
$('#whiteboard .ui-resizable-handle').css('display', '')
setTimeout(scaleWhiteboard, 0)
@toggleMic = (event) ->
BBB.toggleMyMic()
@toggleUsersList = ->
if $('.userlistMenu').hasClass('hiddenInLandscape')
$('.userlistMenu').removeClass('hiddenInLandscape')
else
$('.userlistMenu').addClass('hiddenInLandscape')
setTimeout(scaleWhiteboard, 0)
@populateNotifications = (msg) ->
myUserId = getInSession "userId"
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({'message.chat_type': 'PRIVATE_CHAT'}).fetch()
uniqueArray = []
for chat in myPrivateChats
if chat.message.to_userid is myUserId
uniqueArray.push({userId: chat.message.from_userid, username: chat.message.from_username})
if chat.message.from_userid is myUserId
uniqueArray.push({userId: chat.message.to_userid, username: chat.message.to_username})
#keep unique entries only
uniqueArray = uniqueArray.filter((itm, i, a) ->
i is a.indexOf(itm)
)
if msg.message.to_userid is myUserId
new_msg_userid = msg.message.from_userid
if msg.message.from_userid is myUserId
new_msg_userid = msg.message.to_userid
chats = getInSession('chats')
if chats is undefined
initChats = [
userId: "PUBLIC_CHAT"
gotMail: false
number: 0;
]
setInSession 'chats', initChats
#insert the unique entries in the collection
for u in uniqueArray
chats = getInSession('chats')
if chats.filter((chat) -> chat.userId == u.userId).length is 0 and u.userId is new_msg_userid
chats.push {userId: u.userId, gotMail: false, number: 0}
setInSession 'chats', chats
@toggleShield = ->
if parseFloat($('.shield').css('opacity')) is 0.5 # triggered during a pan gesture
$('.shield').css('opacity', '')
if !$('.shield').hasClass('darken') and !$('.shield').hasClass('animatedShield')
$('.shield').addClass('darken')
else
$('.shield').removeClass('darken')
$('.shield').removeClass('animatedShield')
@removeFullscreenStyles = ->
$('#whiteboard-paper').removeClass('vertically-centered')
$('#chat').removeClass('invisible')
$('#users').removeClass('invisible')
$('#navbar').removeClass('invisible')
$('.FABTriggerButton').removeClass('invisible')
$('.fullscreenButton').removeClass('exitFullscreenButton')
$('.fullscreenButton').addClass('whiteboardFullscreenButton')
$('.fullscreenButton i').removeClass('ion-arrow-shrink')
$('.fullscreenButton i').addClass('ion-arrow-expand')
@enterWhiteboardFullscreen = ->
element = document.getElementById('whiteboard')
if element.requestFullscreen
element.requestFullscreen()
else if element.mozRequestFullScreen
element.mozRequestFullScreen()
$('.fullscreenButton').addClass('iconFirefox') # browser-specific icon sizing
else if element.webkitRequestFullscreen
element.webkitRequestFullscreen()
$('.fullscreenButton').addClass('iconChrome') # browser-specific icon sizing
else if element.msRequestFullscreen
element.msRequestFullscreen()
$('#chat').addClass('invisible')
$('#users').addClass('invisible')
$('#navbar').addClass('invisible')
$('.FABTriggerButton').addClass('invisible')
$('.fullscreenButton').removeClass('whiteboardFullscreenButton')
$('.fullscreenButton').addClass('exitFullscreenButton')
$('.fullscreenButton i').removeClass('ion-arrow-expand')
$('.fullscreenButton i').addClass('ion-arrow-shrink')
$('#whiteboard-paper').addClass('vertically-centered')
$('#whiteboard').bind 'webkitfullscreenchange', (e) ->
if document.webkitFullscreenElement is null
$('#whiteboard').unbind('webkitfullscreenchange')
$('.fullscreenButton').removeClass('iconChrome')
removeFullscreenStyles()
scaleWhiteboard()
$(document).bind 'mozfullscreenchange', (e) -> # target is always the document in Firefox
if document.mozFullScreenElement is null
$(document).unbind('mozfullscreenchange')
$('.fullscreenButton').removeClass('iconFirefox')
removeFullscreenStyles()
scaleWhiteboard()
@closeMenus = ->
if $('.userlistMenu').hasClass('menuOut')
toggleUserlistMenu()
else if $('.settingsMenu').hasClass('menuOut')
toggleSettingsMenu()
# 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
@exitVoiceCall = (event, afterExitCall) ->
# To be called when the hangup is initiated
hangupCallback = ->
console.log "Exiting Voice Conference"
# Checks periodically until a call is established so we can successfully end the call
# clean state
getInSession("triedHangup", false)
# function to initiate call
(checkToHangupCall = (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() isnt null and !getInSession("triedHangup")
console.log "Attempting to hangup on WebRTC call"
if BBB.amIListenOnlyAudio() # notify BBB-apps we are leaving the call call if we are listen only
Meteor.call('listenOnlyRequestToggle', BBB.getMeetingId(), getInSession("userId"), getInSession("authToken"), false)
BBB.leaveVoiceConference hangupCallback
getInSession("triedHangup", true) # we have hung up, prevent retries
notification_WebRTCAudioExited()
if afterExitCall
afterExitCall this, Meteor.config.app.listenOnly
else
console.log "RETRYING hangup on WebRTC call in #{Meteor.config.app.WebRTCHangupRetryInterval} ms"
setTimeout checkToHangupCall, Meteor.config.app.WebRTCHangupRetryInterval # try again periodically
)(@) # automatically run function
return false
# close the daudio UI, then join the conference. If listen only send the request to the server
@joinVoiceCall = (event, {isListenOnly} = {}) ->
if !isWebRTCAvailable()
notification_WebRTCNotSupported()
return
isListenOnly ?= true
# create voice call params
joinCallback = (message) ->
console.log "Beginning WebRTC Conference Call"
notification_WebRTCAudioJoining()
if isListenOnly
Meteor.call('listenOnlyRequestToggle', BBB.getMeetingId(), getInSession("userId"), getInSession("authToken"), true)
BBB.joinVoiceConference joinCallback, isListenOnly # make the call #TODO should we apply role permissions to this action?
return false
# Starts the entire logout procedure.
# meeting: the meeting the user is in
# the user's userId
@userLogout = (meeting, user) ->
Meteor.call("userLogout", meeting, user, getInSession("authToken"))
console.log "logging out"
clearSessionVar(document.location = getInSession 'logoutURL') # navigate to logout
@kickUser = (meetingId, toKickUserId, requesterUserId, authToken) ->
Meteor.call("kickUser", meetingId, toKickUserId, requesterUserId, authToken)
@setUserPresenter = (meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken) ->
Meteor.call("setUserPresenter", meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken)
# Clear the local user session
@clearSessionVar = (callback) ->
amplify.store('authToken', null)
amplify.store('bbbServerVersion', null)
amplify.store('chats', null)
amplify.store('dateOfBuild', null)
amplify.store('display_chatPane', null)
amplify.store('display_chatbar', null)
amplify.store('display_navbar', null)
amplify.store('display_usersList', null)
amplify.store('display_whiteboard', null)
amplify.store('inChatWith', null)
amplify.store('logoutURL', null)
amplify.store('meetingId', null)
amplify.store('messageFontSize', null)
amplify.store('tabsRenderedTime', null)
amplify.store('userId', null)
amplify.store('display_menu', null)
if callback?
callback()
# assign the default values for the Session vars
@setDefaultSettings = ->
setInSession "display_navbar", true
setInSession "display_chatbar", true
setInSession "display_whiteboard", true
setInSession "display_chatPane", true
#if it is a desktop version of the client
if isPortraitMobile() or isLandscapeMobile()
setInSession "messageFontSize", Meteor.config.app.mobileFont
#if this is a mobile version of the client
else
setInSession "messageFontSize", Meteor.config.app.desktopFont
setInSession 'display_slidingMenu', false
setInSession 'display_hiddenNavbarSection', false
if isLandscape()
setInSession 'display_usersList', true
else
setInSession 'display_usersList', false
setInSession 'display_menu', false
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()
initChats = [
userId: "PUBLIC_CHAT"
gotMail: false
number: 0
]
setInSession 'chats', initChats
setInSession "inChatWith", 'PUBLIC_CHAT'
TimeSync.loggingEnabled = false # suppresses the log messages from timesync
#true if it is a new user, false if the client was just refreshed
@loginOrRefresh = ->
userId = getInSession 'userId'
checkId = getInSession 'checkId'
if checkId is undefined
setInSession 'checkId', userId
return true
else if userId isnt checkId
setInSession 'checkId', userId
return true
else
return false
@onLoadComplete = ->
document.title = "BigBlueButton #{BBB.getMeetingName() ? 'HTML5'}"
setDefaultSettings()
Meteor.Users.find().observe({
removed: (oldDocument) ->
if oldDocument.userId is getInSession 'userId'
document.location = getInSession 'logoutURL'
})
Meteor.Users.find().observe changed: (newUser, oldUser) ->
if Meteor.config.app.listenOnly is true and
newUser.user.presenter is false and
oldUser.user.presenter is true and
BBB.getCurrentUser().userId is newUser.userId and
oldUser.user.listenOnly is false
exitVoiceCall(@, joinVoiceCall)
# Detects a mobile device
@isMobile = ->
navigator.userAgent.match(/Android/i) or
navigator.userAgent.match(/iPhone|iPad|iPod/i) or
navigator.userAgent.match(/BlackBerry/i) or
navigator.userAgent.match(/Windows Phone/i) or
navigator.userAgent.match(/IEMobile/i) or
navigator.userAgent.match(/BlackBerry/i) or
navigator.userAgent.match(/webOS/i)
@isLandscape = ->
not isMobile() and
window.matchMedia('(orientation: landscape)').matches and # browser is landscape
window.matchMedia('(min-device-aspect-ratio: 1/1)').matches # device is landscape
@isPortrait = ->
not isMobile() and
window.matchMedia('(orientation: portrait)').matches and # 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
@isPortraitMobile = () ->
isMobile() and
window.matchMedia('(orientation: portrait)').matches and # 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
@isLandscapeMobile = () ->
isMobile() and
window.matchMedia('(orientation: landscape)').matches and # browser is landscape
window.matchMedia('(min-device-aspect-ratio: 1/1)').matches # device is landscape
@isLandscapePhone = () ->
# @phone-landscape media query:
window.matchMedia('(orientation: landscape)').matches and
window.matchMedia('(min-device-aspect-ratio: 1/1)').matches and
window.matchMedia('(max-device-width: 959px)').matches
@isPortraitPhone = () ->
# @phone-portrait media query:
(window.matchMedia('(orientation: portrait)').matches and
window.matchMedia('(max-device-aspect-ratio: 1/1)').matches and
window.matchMedia('(max-device-width: 480px)').matches) or
# @phone-portrait-with-keyboard media query:
(window.matchMedia('(orientation: landscape)').matches and
window.matchMedia('(max-device-aspect-ratio: 1/1)').matches and
window.matchMedia('(max-device-width: 480px)').matches)
@isPhone = () ->
isLandscapePhone() or isPortraitPhone()
# The webpage orientation is now landscape
@orientationBecameLandscape = ->
adjustChatInputHeight()
# The webpage orientation is now portrait
@orientationBecamePortrait = ->
adjustChatInputHeight()
# Checks if only one panel (userlist/whiteboard/chatbar) is currently open
@isOnlyOnePanelOpen = () ->
#(getInSession "display_usersList" ? 1 : 0) + (getInSession "display_whiteboard" ? 1 : 0) + (getInSession "display_chatbar" ? 1 : 0) is 1
getInSession("display_usersList") + getInSession("display_whiteboard") + getInSession("display_chatbar") is 1
# determines which browser is being used
@getBrowserName = () ->
if navigator.userAgent.match(/Chrome/i)
return 'Chrome'
else if navigator.userAgent.match(/Firefox/i)
return 'Firefox'
else if navigator.userAgent.match(/Safari/i)
return 'Safari'
else if navigator.userAgent.match(/Trident/i)
return 'IE'
else
return null
@scrollChatDown = () ->
$('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight)
# changes the height of the chat input area if needed (based on the textarea content)
@adjustChatInputHeight = () ->
if isLandscape()
$('#newMessageInput').css('height', 'auto')
projectedHeight = $('#newMessageInput')[0].scrollHeight + 23
if projectedHeight isnt $('.panel-footer').height() and
projectedHeight >= getInSession('chatInputMinHeight')
$('#newMessageInput').css('overflow', 'hidden') # prevents a scroll bar
# resizes the chat input area
$('.panel-footer').css('top', - (projectedHeight - 70) + 'px')
$('.panel-footer').css('height', projectedHeight + 'px')
$('#newMessageInput').height($('#newMessageInput')[0].scrollHeight)
# resizes the chat messages container
$('#chatbody').height($('#chat').height() - projectedHeight - 45)
$('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight)
$('#newMessageInput').css('height', '')
else if isPortrait()
$('.panel-footer').attr('style','')
$('#chatbody').attr('style','')
$('#newMessageInput').attr('style','')
@toggleEmojisFAB = () ->
if $('.FABContainer').hasClass('openedFAB')
$('.FABContainer > button:nth-child(2)').css('opacity', $('.FABContainer > button:nth-child(2)').css('opacity'))
$('.FABContainer > button:nth-child(3)').css('opacity', $('.FABContainer > button:nth-child(3)').css('opacity'))
$('.FABContainer > button:nth-child(4)').css('opacity', $('.FABContainer > button:nth-child(4)').css('opacity'))
$('.FABContainer > button:nth-child(5)').css('opacity', $('.FABContainer > button:nth-child(5)').css('opacity'))
$('.FABContainer > button:nth-child(6)').css('opacity', $('.FABContainer > button:nth-child(6)').css('opacity'))
$('.FABContainer > button:nth-child(7)').css('opacity', $('.FABContainer > button:nth-child(7)').css('opacity'))
$('.FABContainer').removeClass('openedFAB')
$('.FABContainer').addClass('closedFAB')
else
$('.FABContainer > button:nth-child(2)').css('opacity', $('.FABContainer > button:nth-child(2)').css('opacity'))
$('.FABContainer > button:nth-child(3)').css('opacity', $('.FABContainer > button:nth-child(3)').css('opacity'))
$('.FABContainer > button:nth-child(4)').css('opacity', $('.FABContainer > button:nth-child(4)').css('opacity'))
$('.FABContainer > button:nth-child(5)').css('opacity', $('.FABContainer > button:nth-child(5)').css('opacity'))
$('.FABContainer > button:nth-child(6)').css('opacity', $('.FABContainer > button:nth-child(6)').css('opacity'))
$('.FABContainer > button:nth-child(7)').css('opacity', $('.FABContainer > button:nth-child(7)').css('opacity'))
$('.FABContainer').removeClass('closedFAB')
$('.FABContainer').addClass('openedFAB')
@toggleUserlistMenu = () ->
# menu
if $('.userlistMenu').hasClass('menuOut')
$('.userlistMenu').removeClass('menuOut')
else
$('.userlistMenu').addClass('menuOut')
# icon
if $('.toggleUserlistButton').hasClass('menuToggledOn')
$('.toggleUserlistButton').removeClass('menuToggledOn')
else
$('.toggleUserlistButton').addClass('menuToggledOn')
@toggleSettingsMenu = () ->
# menu
if $('.settingsMenu').hasClass('menuOut')
$('.settingsMenu').removeClass('menuOut')
else
$('.settingsMenu').addClass('menuOut')
# icon
if $('.toggleMenuButton').hasClass('menuToggledOn')
$('.toggleMenuButton').removeClass('menuToggledOn')
else
$('.toggleMenuButton').addClass('menuToggledOn')

View File

@ -0,0 +1,910 @@
this.getBuildInformation = function() {
let copyrightYear, defaultWelcomeMessage, defaultWelcomeMessageFooter, html5ClientBuild, link, ref, ref1, ref2, ref3;
copyrightYear = ((ref = Meteor.config) != null ? ref.copyrightYear : void 0) || "DATE";
html5ClientBuild = ((ref1 = Meteor.config) != null ? ref1.html5ClientBuild : void 0) || "VERSION";
defaultWelcomeMessage = ((ref2 = Meteor.config) != null ? ref2.defaultWelcomeMessage : void 0) || "WELCOME MESSAGE";
defaultWelcomeMessageFooter = ((ref3 = Meteor.config) != null ? ref3.defaultWelcomeMessageFooter : void 0) || "WELCOME MESSAGE";
link = "<a href='http://bigbluebutton.org/' target='_blank'>http://bigbluebutton.org</a>";
return {
'copyrightYear': copyrightYear,
'html5ClientBuild': html5ClientBuild,
'defaultWelcomeMessage': defaultWelcomeMessage,
'defaultWelcomeMessageFooter': defaultWelcomeMessageFooter,
'link': link
};
};
// Convert a color `value` as integer to a hex color (e.g. 255 to #0000ff)
this.colourToHex = function(value) {
let hex;
hex = parseInt(value).toString(16);
while(hex.length < 6) {
hex = `0${hex}`;
}
return `#${hex}`;
};
// color can be a number (a hex converted to int) or a string (e.g. "#ffff00")
this.formatColor = function(color) {
if(color == null) {
color = "0"; // default value
}
if(!color.toString().match(/\#.*/)) {
color = colourToHex(color);
}
return color;
};
this.getInSession = function(k) {
return SessionAmplify.get(k);
};
// returns epoch in ms
this.getTime = function() {
return (new Date).valueOf();
};
// checks if the pan gesture is mostly horizontal
this.isPanHorizontal = function(event) {
return Math.abs(event.deltaX) > Math.abs(event.deltaY);
};
// helper to determine whether user has joined any type of audio
Handlebars.registerHelper("amIInAudio", () => {
return BBB.amIInAudio();
});
// helper to determine whether the user is in the listen only audio stream
Handlebars.registerHelper("amIListenOnlyAudio", () => {
return BBB.amIListenOnlyAudio();
});
// helper to determine whether the user is in the listen only audio stream
Handlebars.registerHelper("isMyMicLocked", () => {
return BBB.isMyMicLocked();
});
Handlebars.registerHelper("colourToHex", (_this => {
return function(value) {
return _this.window.colourToHex(value);
};
})(this));
Handlebars.registerHelper('equals', (a, b) => { // equals operator was dropped in Meteor's migration from Handlebars to Spacebars
return a === b;
});
Handlebars.registerHelper("getCurrentMeeting", () => {
return Meteor.Meetings.findOne();
});
Handlebars.registerHelper("getCurrentSlide", () => {
let result;
result = BBB.getCurrentSlide("helper getCurrentSlide");
// console.log "result=#{JSON.stringify result}"
return result;
});
// Allow access through all templates
Handlebars.registerHelper("getInSession", k => {
return SessionAmplify.get(k);
});
Handlebars.registerHelper("getMeetingName", () => {
return BBB.getMeetingName();
});
Handlebars.registerHelper("getShapesForSlide", () => {
let currentSlide, ref;
currentSlide = BBB.getCurrentSlide("helper getShapesForSlide");
// try to reuse the lines above
return Meteor.Shapes.find({
whiteboardId: currentSlide != null ? (ref = currentSlide.slide) != null ? ref.id : void 0 : void 0
});
});
// retrieves all users in the meeting
Handlebars.registerHelper("getUsersInMeeting", () => {
let users;
users = Meteor.Users.find().fetch();
if((users != null ? users.length : void 0) > 1) {
return getSortedUserList(users);
} else {
return users;
}
});
Handlebars.registerHelper("getWhiteboardTitle", () => {
return BBB.currentPresentationName() || "Loading presentation...";
});
Handlebars.registerHelper("getCurrentUserEmojiStatus", () => {
let ref, ref1;
return (ref = BBB.getCurrentUser()) != null ? (ref1 = ref.user) != null ? ref1.emoji_status : void 0 : void 0;
});
Handlebars.registerHelper("isCurrentUser", userId => {
let ref;
return userId === null || userId === ((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0);
});
Handlebars.registerHelper("isCurrentUserMuted", () => {
return BBB.amIMuted();
});
//Retreives a username for a private chat tab from the database if it exists
Handlebars.registerHelper("privateChatName", () => {
let obj, ref;
obj = Meteor.Users.findOne({
userId: getInSession("inChatWith")
});
if(obj != null) {
return obj != null ? (ref = obj.user) != null ? ref.name : void 0 : void 0;
}
});
Handlebars.registerHelper("isCurrentUserEmojiStatusSet", () => {
return BBB.isCurrentUserEmojiStatusSet();
});
Handlebars.registerHelper("isCurrentUserSharingVideo", () => {
return BBB.amISharingVideo();
});
Handlebars.registerHelper("isCurrentUserTalking", () => {
return BBB.amITalking();
});
Handlebars.registerHelper("isCurrentUserPresenter", () => {
return BBB.isUserPresenter(getInSession('userId'));
});
Handlebars.registerHelper("isCurrentUserModerator", () => {
return BBB.getMyRole() === "MODERATOR";
});
Handlebars.registerHelper("isDisconnected", () => {
return !Meteor.status().connected;
});
Handlebars.registerHelper("isUserInAudio", userId => {
return BBB.isUserInAudio(userId);
});
Handlebars.registerHelper("isUserListenOnlyAudio", userId => {
return BBB.isUserListenOnlyAudio(userId);
});
Handlebars.registerHelper("isUserMuted", userId => {
return BBB.isUserMuted(userId);
});
Handlebars.registerHelper("isUserSharingVideo", userId => {
return BBB.isUserSharingWebcam(userId);
});
Handlebars.registerHelper("isUserTalking", userId => {
return BBB.isUserTalking(userId);
});
Handlebars.registerHelper('isMobile', () => {
return isMobile();
});
Handlebars.registerHelper('isPortraitMobile', () => {
return isPortraitMobile();
});
Handlebars.registerHelper('isMobileChromeOrFirefox', () => {
return isMobile() && ((getBrowserName() === 'Chrome') || (getBrowserName() === 'Firefox'));
});
Handlebars.registerHelper("meetingIsRecording", () => {
return BBB.isMeetingRecording();
});
Handlebars.registerHelper("messageFontSize", () => {
return {
style: `font-size: ${getInSession("messageFontSize")}px;`
};
});
Handlebars.registerHelper("pointerLocation", () => {
let currentPresentation, currentSlideDoc, pointer, presentationId, ref;
currentPresentation = Meteor.Presentations.findOne({
"presentation.current": true
});
presentationId = currentPresentation != null ? (ref = currentPresentation.presentation) != null ? ref.id : void 0 : void 0;
currentSlideDoc = Meteor.Slides.findOne({
"presentationId": presentationId,
"slide.current": true
});
pointer = Meteor.Cursor.findOne();
pointer.x = (-currentSlideDoc.slide.x_offset * 2 + currentSlideDoc.slide.width_ratio * pointer.x) / 100;
pointer.y = (-currentSlideDoc.slide.y_offset * 2 + currentSlideDoc.slide.height_ratio * pointer.y) / 100;
return pointer;
});
Handlebars.registerHelper("safeName", str => {
return safeString(str);
});
Handlebars.registerHelper("canJoinWithMic", () => {
if((BBB.isUserPresenter(getInSession('userId')) || !Meteor.config.app.listenOnly) && !BBB.isMyMicLocked()) {
return true;
} else {
return false;
}
});
/*Handlebars.registerHelper "visibility", (section) ->
if getInSession "display_#{section}"
style: 'display:block;'
else
style: 'display:none;'
*/
Handlebars.registerHelper("visibility", section => {
return {
style: 'display:block;'
};
});
Handlebars.registerHelper('containerPosition', section => {
if(getInSession('display_usersList')) {
return 'moved-to-right';
} else if(getInSession('display_menu')) {
return 'moved-to-left';
} else {
return '';
}
});
// vertically shrinks the whiteboard if the slide navigation controllers are present
Handlebars.registerHelper('whiteboardSize', section => {
if(BBB.isUserPresenter(getInSession('userId'))) {
return 'presenter-whiteboard';
} else {
if(BBB.isPollGoing(getInSession('userId'))) {
return 'poll-whiteboard';
} else {
return 'viewer-whiteboard';
}
}
});
Handlebars.registerHelper("getPollQuestions", () => {
let answer, buttonStyle, j, len, marginStyle, number, polls, ref, widthStyle;
polls = BBB.getCurrentPoll(getInSession('userId'));
if((polls != null) && polls !== void 0) {
number = polls.poll_info.poll.answers.length;
widthStyle = `width: calc(75%/${number});`;
marginStyle = `margin-left: calc(25%/${number * 2});margin-right: calc(25%/${number * 2});`;
buttonStyle = widthStyle + marginStyle;
ref = polls.poll_info.poll.answers;
for(j = 0, len = ref.length; j < len; j++) {
answer = ref[j];
answer.style = buttonStyle;
}
return polls.poll_info.poll.answers;
}
});
this.getSortedUserList = function(users) {
if((users != null ? users.length : void 0) > 1) {
users.sort((a, b) => {
let aTime, bTime;
if(a.user.role === "MODERATOR" && b.user.role === "MODERATOR") {
if(a.user.set_emoji_time && b.user.set_emoji_time) {
aTime = a.user.set_emoji_time.getTime();
bTime = b.user.set_emoji_time.getTime();
if(aTime < bTime) {
return -1;
} else {
return 1;
}
} else if(a.user.set_emoji_time) {
return -1;
} else if(b.user.set_emoji_time) {
return 1;
}
} else if(a.user.role === "MODERATOR") {
return -1;
} else if(b.user.role === "MODERATOR") {
return 1;
} else if(a.user.set_emoji_time && b.user.set_emoji_time) {
aTime = a.user.set_emoji_time.getTime();
bTime = b.user.set_emoji_time.getTime();
if(aTime < bTime) {
return -1;
} else {
return 1;
}
} else if(a.user.set_emoji_time) {
return -1;
} else if(b.user.set_emoji_time) {
return 1;
} else if(!a.user.phone_user && !b.user.phone_user) {
} else if(!a.user.phone_user) {
return -1;
} else if(!b.user.phone_user) {
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) {
return -1;
} else if(a.user._sort_name > b.user._sort_name) {
return 1;
} else if(a.user.userid.toLowerCase() > b.user.userid.toLowerCase()) {
return -1;
} else if(a.user.userid.toLowerCase() < b.user.userid.toLowerCase()) {
return 1;
}
});
}
return users;
};
// transform plain text links into HTML tags compatible with Flash client
this.linkify = function(str) {
return str = str.replace(re_weburl, "<a href='event:$&'><u>$&</u></a>");
};
this.setInSession = function(k, v) {
return SessionAmplify.set(k, v);
};
this.safeString = function(str) {
if(typeof str === 'string') {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
};
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() {
setInSession("display_chatbar", !getInSession("display_chatbar"));
if(!getInSession("display_chatbar")) {
$('#whiteboard').css('width', '100%');
$('#whiteboard .ui-resizable-handle').css('display', 'none');
} else {
$('#whiteboard').css('width', '');
$('#whiteboard .ui-resizable-handle').css('display', '');
}
return setTimeout(scaleWhiteboard, 0);
};
this.toggleMic = function(event) {
return BBB.toggleMyMic();
};
this.toggleUsersList = function() {
if($('.userlistMenu').hasClass('hiddenInLandscape')) {
$('.userlistMenu').removeClass('hiddenInLandscape');
} else {
$('.userlistMenu').addClass('hiddenInLandscape');
}
return setTimeout(scaleWhiteboard, 0);
};
this.populateNotifications = function(msg) {
let chat, chats, initChats, j, l, len, len1, myPrivateChats, myUserId, new_msg_userid, results, u, uniqueArray, users;
myUserId = getInSession("userId");
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({
'message.chat_type': 'PRIVATE_CHAT'
}).fetch();
uniqueArray = [];
for(j = 0, len = myPrivateChats.length; j < len; j++) {
chat = myPrivateChats[j];
if(chat.message.to_userid === myUserId) {
uniqueArray.push({
userId: chat.message.from_userid,
username: chat.message.from_username
});
}
if(chat.message.from_userid === myUserId) {
uniqueArray.push({
userId: chat.message.to_userid,
username: chat.message.to_username
});
}
}
//keep unique entries only
uniqueArray = uniqueArray.filter((itm, i, a) => {
return i === a.indexOf(itm);
});
if(msg.message.to_userid === myUserId) {
new_msg_userid = msg.message.from_userid;
}
if(msg.message.from_userid === myUserId) {
new_msg_userid = msg.message.to_userid;
}
chats = getInSession('chats');
if(chats === void 0) {
initChats = [
{
userId: "PUBLIC_CHAT",
gotMail: false,
number: 0
}
];
setInSession('chats', initChats);
}
results = [];
//insert the unique entries in the collection
for(l = 0, len1 = uniqueArray.length; l < len1; l++) {
u = uniqueArray[l];
chats = getInSession('chats');
if(chats.filter(chat => {
return chat.userId === u.userId;
}).length === 0 && u.userId === new_msg_userid) {
chats.push({
userId: u.userId,
gotMail: false,
number: 0
});
results.push(setInSession('chats', chats));
} else {
results.push(void 0);
}
}
return results;
};
this.toggleShield = function() {
if(parseFloat($('.shield').css('opacity')) === 0.5) {
$('.shield').css('opacity', '');
}
if(!$('.shield').hasClass('darken') && !$('.shield').hasClass('animatedShield')) {
return $('.shield').addClass('darken');
} else {
$('.shield').removeClass('darken');
return $('.shield').removeClass('animatedShield');
}
};
this.removeFullscreenStyles = function() {
$('#whiteboard-paper').removeClass('vertically-centered');
$('#chat').removeClass('invisible');
$('#users').removeClass('invisible');
$('#navbar').removeClass('invisible');
$('.FABTriggerButton').removeClass('invisible');
$('.fullscreenButton').removeClass('exitFullscreenButton');
$('.fullscreenButton').addClass('whiteboardFullscreenButton');
$('.fullscreenButton i').removeClass('ion-arrow-shrink');
return $('.fullscreenButton i').addClass('ion-arrow-expand');
};
this.enterWhiteboardFullscreen = function() {
let element;
element = document.getElementById('whiteboard');
if(element.requestFullscreen) {
element.requestFullscreen();
} else if(element.mozRequestFullScreen) {
element.mozRequestFullScreen();
$('.fullscreenButton').addClass('iconFirefox');
} else if(element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
$('.fullscreenButton').addClass('iconChrome');
} else if(element.msRequestFullscreen) {
element.msRequestFullscreen();
}
$('#chat').addClass('invisible');
$('#users').addClass('invisible');
$('#navbar').addClass('invisible');
$('.FABTriggerButton').addClass('invisible');
$('.fullscreenButton').removeClass('whiteboardFullscreenButton');
$('.fullscreenButton').addClass('exitFullscreenButton');
$('.fullscreenButton i').removeClass('ion-arrow-expand');
$('.fullscreenButton i').addClass('ion-arrow-shrink');
$('#whiteboard-paper').addClass('vertically-centered');
$('#whiteboard').bind('webkitfullscreenchange', e => {
if(document.webkitFullscreenElement === null) {
$('#whiteboard').unbind('webkitfullscreenchange');
$('.fullscreenButton').removeClass('iconChrome');
removeFullscreenStyles();
return scaleWhiteboard();
}
});
return $(document).bind('mozfullscreenchange', e => {
if(document.mozFullScreenElement === null) {
$(document).unbind('mozfullscreenchange');
$('.fullscreenButton').removeClass('iconFirefox');
removeFullscreenStyles();
return scaleWhiteboard();
}
});
};
this.closeMenus = function() {
if($('.userlistMenu').hasClass('menuOut')) {
return toggleUserlistMenu();
} else if($('.settingsMenu').hasClass('menuOut')) {
return toggleSettingsMenu();
}
};
// 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) {
let checkToHangupCall, hangupCallback;
// To be called when the hangup is initiated
hangupCallback = function() {
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);
// function to initiate call
(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")) {
console.log("Attempting to hangup on WebRTC call");
if(BBB.amIListenOnlyAudio()) {
Meteor.call(
'listenOnlyRequestToggle',
BBB.getMeetingId(),
getInSession("userId"),
getInSession("authToken"),
false
);
}
BBB.leaveVoiceConference(hangupCallback);
getInSession("triedHangup", true);
notification_WebRTCAudioExited();
if(afterExitCall) {
return afterExitCall(this, Meteor.config.app.listenOnly);
}
} else {
console.log(
`RETRYING hangup on WebRTC call in ${Meteor.config.app.WebRTCHangupRetryInterval} ms`
);
return setTimeout(checkToHangupCall, Meteor.config.app.WebRTCHangupRetryInterval);
}
})(this); // automatically run function
return false;
};
// close the daudio UI, then join the conference. If listen only send the request to the server
this.joinVoiceCall = function(event, arg) {
let isListenOnly, joinCallback;
isListenOnly = (arg != null ? arg : {}).isListenOnly;
if(!isWebRTCAvailable()) {
notification_WebRTCNotSupported();
return;
}
if(isListenOnly == null) {
isListenOnly = true;
}
// create voice call params
joinCallback = function(message) {
return console.log("Beginning WebRTC Conference Call");
};
notification_WebRTCAudioJoining();
if(isListenOnly) {
Meteor.call(
'listenOnlyRequestToggle',
BBB.getMeetingId(),
getInSession("userId"),
getInSession("authToken"),
true
);
}
BBB.joinVoiceConference(joinCallback, isListenOnly); // make the call //TODO should we apply role permissions to this action?
return false;
};
// Starts the entire logout procedure.
// meeting: the meeting the user is in
// the user's userId
this.userLogout = function(meeting, user) {
Meteor.call("userLogout", meeting, user, getInSession("authToken"));
console.log("logging out");
return clearSessionVar(document.location = getInSession('logoutURL')); // navigate to logout
};
this.kickUser = function(meetingId, toKickUserId, requesterUserId, authToken) {
return Meteor.call("kickUser", meetingId, toKickUserId, requesterUserId, authToken);
};
this.setUserPresenter = function(
meetingId,
newPresenterId,
requesterSetPresenter,
newPresenterName,
authToken) {
return Meteor.call("setUserPresenter", meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken);
};
// Clear the local user session
this.clearSessionVar = function(callback) {
amplify.store('authToken', null);
amplify.store('bbbServerVersion', null);
amplify.store('chats', null);
amplify.store('dateOfBuild', null);
amplify.store('display_chatPane', null);
amplify.store('display_chatbar', null);
amplify.store('display_navbar', null);
amplify.store('display_usersList', null);
amplify.store('display_whiteboard', null);
amplify.store('inChatWith', null);
amplify.store('logoutURL', null);
amplify.store('meetingId', null);
amplify.store('messageFontSize', null);
amplify.store('tabsRenderedTime', null);
amplify.store('userId', null);
amplify.store('display_menu', null);
if(callback != null) {
return callback();
}
};
// assign the default values for the Session vars
this.setDefaultSettings = function() {
let initChats;
setInSession("display_navbar", true);
setInSession("display_chatbar", true);
setInSession("display_whiteboard", true);
setInSession("display_chatPane", true);
//if it is a desktop version of the client
if(isPortraitMobile() || isLandscapeMobile()) {
setInSession("messageFontSize", Meteor.config.app.mobileFont);
} else { //if this is a mobile version of the client
setInSession("messageFontSize", Meteor.config.app.desktopFont);
}
setInSession('display_slidingMenu', false);
setInSession('display_hiddenNavbarSection', false);
if(isLandscape()) {
setInSession('display_usersList', true);
} else {
setInSession('display_usersList', false);
}
setInSession('display_menu', false);
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()) {
initChats = [
{
userId: "PUBLIC_CHAT",
gotMail: false,
number: 0
}
];
setInSession('chats', initChats);
setInSession("inChatWith", 'PUBLIC_CHAT');
}
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() {
let checkId, userId;
userId = getInSession('userId');
checkId = getInSession('checkId');
if(checkId === void 0) {
setInSession('checkId', userId);
return true;
} else if(userId !== checkId) {
setInSession('checkId', userId);
return true;
} else {
return false;
}
};
this.onLoadComplete = function() {
let ref;
document.title = `BigBlueButton ${(ref = BBB.getMeetingName()) != null ? ref : 'HTML5'}`;
setDefaultSettings();
Meteor.Users.find().observe({
removed(oldDocument) {
if(oldDocument.userId === getInSession('userId')) {
return document.location = getInSession('logoutURL');
}
}
});
return Meteor.Users.find().observe({
changed(newUser, oldUser) {
if(Meteor.config.app.listenOnly === true && newUser.user.presenter === false && oldUser.user.presenter === true && BBB.getCurrentUser().userId === newUser.userId && oldUser.user.listenOnly === false) {
return exitVoiceCall(this, joinVoiceCall);
}
}
});
};
// Detects a mobile device
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);
};
this.isLandscape = function() {
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() {
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() {
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() {
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() {
// @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() {
// @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() {
return isLandscapePhone() || isPortraitPhone();
};
// The webpage orientation is now landscape
this.orientationBecameLandscape = function() {
return adjustChatInputHeight();
};
// The webpage orientation is now portrait
this.orientationBecamePortrait = function() {
return adjustChatInputHeight();
};
// Checks if only one panel (userlist/whiteboard/chatbar) is currently open
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;
};
// determines which browser is being used
this.getBrowserName = function() {
if(navigator.userAgent.match(/Chrome/i)) {
return 'Chrome';
} else if(navigator.userAgent.match(/Firefox/i)) {
return 'Firefox';
} else if(navigator.userAgent.match(/Safari/i)) {
return 'Safari';
} else if(navigator.userAgent.match(/Trident/i)) {
return 'IE';
} else {
return null;
}
};
this.scrollChatDown = function() {
let ref;
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() {
let projectedHeight, ref;
if(isLandscape()) {
$('#newMessageInput').css('height', 'auto');
projectedHeight = $('#newMessageInput')[0].scrollHeight + 23;
if(projectedHeight !== $('.panel-footer').height() && projectedHeight >= getInSession('chatInputMinHeight')) {
$('#newMessageInput').css('overflow', 'hidden'); // prevents a scroll bar
// resizes the chat input area
$('.panel-footer').css('top', `${-(projectedHeight - 70)}px`);
$('.panel-footer').css('height', `${projectedHeight}px`);
$('#newMessageInput').height($('#newMessageInput')[0].scrollHeight);
// resizes the chat messages container
$('#chatbody').height($('#chat').height() - projectedHeight - 45);
$('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
}
return $('#newMessageInput').css('height', '');
} else if(isPortrait()) {
$('.panel-footer').attr('style', '');
$('#chatbody').attr('style', '');
return $('#newMessageInput').attr('style', '');
}
};
this.toggleEmojisFAB = function() {
if($('.FABContainer').hasClass('openedFAB')) {
$('.FABContainer > button:nth-child(2)').css('opacity', $('.FABContainer > button:nth-child(2)').css('opacity'));
$('.FABContainer > button:nth-child(3)').css('opacity', $('.FABContainer > button:nth-child(3)').css('opacity'));
$('.FABContainer > button:nth-child(4)').css('opacity', $('.FABContainer > button:nth-child(4)').css('opacity'));
$('.FABContainer > button:nth-child(5)').css('opacity', $('.FABContainer > button:nth-child(5)').css('opacity'));
$('.FABContainer > button:nth-child(6)').css('opacity', $('.FABContainer > button:nth-child(6)').css('opacity'));
$('.FABContainer > button:nth-child(7)').css('opacity', $('.FABContainer > button:nth-child(7)').css('opacity'));
$('.FABContainer').removeClass('openedFAB');
return $('.FABContainer').addClass('closedFAB');
} else {
$('.FABContainer > button:nth-child(2)').css('opacity', $('.FABContainer > button:nth-child(2)').css('opacity'));
$('.FABContainer > button:nth-child(3)').css('opacity', $('.FABContainer > button:nth-child(3)').css('opacity'));
$('.FABContainer > button:nth-child(4)').css('opacity', $('.FABContainer > button:nth-child(4)').css('opacity'));
$('.FABContainer > button:nth-child(5)').css('opacity', $('.FABContainer > button:nth-child(5)').css('opacity'));
$('.FABContainer > button:nth-child(6)').css('opacity', $('.FABContainer > button:nth-child(6)').css('opacity'));
$('.FABContainer > button:nth-child(7)').css('opacity', $('.FABContainer > button:nth-child(7)').css('opacity'));
$('.FABContainer').removeClass('closedFAB');
return $('.FABContainer').addClass('openedFAB');
}
};
this.toggleUserlistMenu = function() {
// menu
if($('.userlistMenu').hasClass('menuOut')) {
$('.userlistMenu').removeClass('menuOut');
} else {
$('.userlistMenu').addClass('menuOut');
}
// icon
if($('.toggleUserlistButton').hasClass('menuToggledOn')) {
return $('.toggleUserlistButton').removeClass('menuToggledOn');
} else {
return $('.toggleUserlistButton').addClass('menuToggledOn');
}
};
this.toggleSettingsMenu = function() {
// menu
if($('.settingsMenu').hasClass('menuOut')) {
$('.settingsMenu').removeClass('menuOut');
} else {
$('.settingsMenu').addClass('menuOut');
}
// icon
if($('.toggleMenuButton').hasClass('menuToggledOn')) {
return $('.toggleMenuButton').removeClass('menuToggledOn');
} else {
return $('.toggleMenuButton').addClass('menuToggledOn');
}
};

View File

@ -1,492 +0,0 @@
###
This file contains the BigBlueButton client APIs that will allow 3rd-party applications
to embed the HTML5 client and interact with it through Javascript.
HOW TO USE:
Some APIs allow synchronous and asynchronous calls. When using asynchronous, the 3rd-party
JS should register as listener for events listed at the bottom of this file. For synchronous,
3rd-party JS should pass in a callback function when calling the API.
For an example on how to use these APIs, see:
https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/lib/3rd-party.js
https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/3rd-party.html
###
@BBB = (->
BBB = {}
returnOrCallback = (res, callback) ->
if callback? and typeof callback is "function"
callback res
else
res
BBB.isPollGoing = (userId) ->
if userId isnt undefined and Meteor.Polls.findOne({"poll_info.users": userId})
return true
else
return false
BBB.getCurrentPoll = (userId) ->
if userId isnt undefined and Meteor.Polls.findOne({"poll_info.users": userId})
return Meteor.Polls.findOne({"poll_info.users": userId})
BBB.sendPollResponseMessage = (key, pollAnswerId) ->
Meteor.call "publishVoteMessage", BBB.getMeetingId(), pollAnswerId, getInSession("userId"), getInSession("authToken")
BBB.getMeetingId = ->
Meteor.Meetings.findOne()?.meetingId
BBB.getInternalMeetingId = (callback) ->
###
Queryies the user object via it's id
###
BBB.getUser = (userId) ->
Meteor.Users.findOne({userId: userId})
BBB.getCurrentUser = () ->
BBB.getUser(getInSession("userId"))
###
Query if the current user is sharing webcam.
Param:
callback - function to return the result
If you want to instead receive an event with the result, register a listener
for AM_I_SHARING_CAM_RESP (see below).
###
BBB.amISharingWebcam = (callback) ->
# BBB.isUserSharingWebcam BBB.getCurrentUser()?.userId
return false
###
Query if another user is sharing her camera.
Param:
userID : the id of the user that may be sharing the camera
callback: function if you want to be informed synchronously. Don't pass a function
if you want to be informed through an event. You have to register for
IS_USER_PUBLISHING_CAM_RESP (see below).
###
BBB.isUserSharingWebcam = (userId, callback) ->
# BBB.getUser(userId)?.user?.webcam_stream?.length isnt 0
return false
# returns whether the user has joined any type of audio
BBB.amIInAudio = (callback) ->
user = BBB.getCurrentUser()
user?.user?.listenOnly or user?.user?.voiceUser?.joined
# returns true if the user has joined the listen only audio stream
BBB.amIListenOnlyAudio = (callback) ->
BBB.getCurrentUser()?.user?.listenOnly
# returns whether the user has joined the voice conference and is sharing audio through a microphone
BBB.amISharingAudio = (callback) ->
BBB.isUserSharingAudio BBB.getCurrentUser()?.userId
# returns whether the user is currently talking
BBB.amITalking = (callback) ->
BBB.isUserTalking BBB.getCurrentUser()?.userId
BBB.isUserInAudio = (userId, callback) ->
user = BBB.getUser(userId)
user?.user?.listenOnly or user?.user?.voiceUser?.joined
BBB.isUserListenOnlyAudio = (userId, callback) ->
BBB.getUser(userId)?.user?.listenOnly
BBB.isUserSharingAudio = (userId, callback) ->
BBB.getUser(userId)?.user?.voiceUser?.joined
BBB.isUserTalking = (userId, callback) ->
BBB.getUser(userId)?.user?.voiceUser?.talking
BBB.isUserPresenter = (userId, callback) ->
BBB.getUser(userId)?.user?.presenter
# returns true if the current user is marked as locked
BBB.amILocked = ->
return BBB.getCurrentUser()?.user.locked
# 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 = ->
lockedMicForRoom = Meteor.Meetings.findOne()?.roomLockSettings.disableMic
# note that voiceUser.locked is not used in BigBlueButton at this stage (April 2015)
return lockedMicForRoom and BBB.amILocked()
BBB.getCurrentSlide = (callingLocaton)->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
# console.log "trigger:#{callingLocaton} currentSlideId=#{currentSlide?._id}"
currentSlide
BBB.getMeetingName = ->
Meteor.Meetings.findOne()?.meetingName or null
BBB.getNumberOfUsers = ->
Meteor.Users.find().count()
BBB.currentPresentationName = ->
Meteor.Presentations.findOne({"presentation.current": true})?.presentation?.name
###
Raise user's hand.
Param:
###
BBB.lowerHand = (meetingId, toUserId, byUserId, byAuthToken) ->
Meteor.call('userLowerHand', meetingId, toUserId, byUserId, byAuthToken)
BBB.raiseHand = (meetingId, toUserId, byUserId, byAuthToken) ->
Meteor.call('userRaiseHand', meetingId, toUserId, byUserId, byAuthToken)
BBB.setEmojiStatus = (meetingId, toUserId, byUserId, byAuthToken, status) ->
Meteor.call('userSetEmoji', meetingId, toUserId, byUserId, byAuthToken, status)
BBB.isUserEmojiStatusSet = (userId) ->
BBB.getUser(userId)?.user?.emoji_status isnt "none" and BBB.getUser(userId)?.user?.emoji_status isnt undefined
BBB.isCurrentUserEmojiStatusSet = ->
BBB.isUserEmojiStatusSet(BBB.getCurrentUser()?.userId)
BBB.isMeetingRecording = ->
MEteor.Meetings.findOne()?.recorded
###
Issue a switch presenter command.
Param:
newPresenterUserID - the user id of the new presenter
3rd-party JS must listen for SWITCHED_PRESENTER (see below) to get notified
of switch presenter events.
###
BBB.switchPresenter = (newPresenterUserID) ->
###
Query if current user is presenter.
Params:
callback - function if you want a callback as response. Otherwise, you need to listen
for AM_I_PRESENTER_RESP (see below).
###
BBB.amIPresenter = (callback) ->
returnOrCallback false, callback
###
Eject a user.
Params:
userID - userID of the user you want to eject.
###
BBB.ejectUser = (userID) ->
###
Query who is presenter.
Params:
callback - function that gets executed for the response.
###
BBB.getPresenterUserID = (callback) ->
###
Query the current user's role.
Params:
callback - function if you want a callback as response. Otherwise, you need to listen
for GET_MY_ROLE_RESP (see below).
###
BBB.getMyRole = (callback) ->
returnOrCallback "VIEWER", callback
###
Query the current user's id.
Params:
callback - function that gets executed for the response.
###
BBB.getMyUserID = (callback) ->
returnOrCallback getInSession("userId"), callback
BBB.getMyDBID = (callback) ->
returnOrCallback Meteor.Users.findOne({userId:getInSession("userId")})?._id, callback
BBB.getMyUserName = (callback) ->
BBB.getUserName(BBB.getCurrentUser()?.userId)
BBB.getMyVoiceBridge = (callback) ->
res = Meteor.Meetings.findOne({}).voiceConf
returnOrCallback res, callback
BBB.getUserName = (userId, callback) ->
returnOrCallback BBB.getUser(userId)?.user?.name, callback
###
Query the current user's role.
Params:
callback - function if you want a callback as response. Otherwise, you need to listen
for GET_MY_ROLE_RESP (see below).
###
BBB.getMyUserInfo = (callback) ->
result =
myUserID: BBB.getMyUserID()
myUsername: BBB.getMyUserName()
myInternalUserID: BBB.getMyUserID()
myAvatarURL: null
myRole: BBB.getMyRole()
amIPresenter: BBB.amIPresenter()
voiceBridge: BBB.getMyVoiceBridge()
dialNumber: null
returnOrCallback(result, callback)
###
Query the meeting id.
Params:
callback - function that gets executed for the response.
###
###
Join the voice conference.
isListenOnly: signifies whether the user joining the conference audio requests to join the listen only stream
###
BBB.joinVoiceConference = (callback, isListenOnly) ->
if BBB.isMyMicLocked()
callIntoConference(BBB.getMyVoiceBridge(), callback, true) #true because we force isListenOnly mode
callIntoConference(BBB.getMyVoiceBridge(), callback, isListenOnly)
###
Leave the voice conference.
###
BBB.leaveVoiceConference = (callback) ->
webrtc_hangup callback # sign out of call
###
Get a hold of the object containing the call information
###
BBB.getCallStatus = ->
getCallStatus()
###
Share user's webcam.
Params:
publishInClient : (DO NOT USE - Unimplemented)
###
BBB.shareVideoCamera = (publishInClient) ->
###
Stop share user's webcam.
###
BBB.stopSharingCamera = ->
###
Indicates if a user is muted
###
BBB.isUserMuted = (id) ->
BBB.getUser(id)?.user?.voiceUser?.muted
###
Indicates if the current user is muted
###
BBB.amIMuted = ->
BBB.isUserMuted(BBB.getCurrentUser().userId)
###
Mute the current user.
###
BBB.muteMe = ->
BBB.muteUser(getInSession("userId"), getInSession("userId"), getInSession("authToken"))
###
Unmute the current user.
###
BBB.unmuteMe = ->
BBB.unmuteUser(getInSession("userId"), getInSession("userId"), getInSession("authToken"))
BBB.muteUser = (meetingId, userId, toMuteId, requesterId, requestToken) ->
Meteor.call('muteUser', meetingId, toMuteId, requesterId, getInSession("authToken"))
BBB.unmuteUser = (meetingId, userId, toMuteId, requesterId, requestToken) ->
Meteor.call('unmuteUser', meetingId, toMuteId, requesterId, getInSession("authToken"))
BBB.toggleMyMic = ->
request = if BBB.amIMuted() then "unmuteUser" else "muteUser"
Meteor.call(request, BBB.getMeetingId(), getInSession("userId"), getInSession("userId"), getInSession("authToken"))
###
Mute all the users.
###
BBB.muteAll = ->
###
Unmute all the users.
###
BBB.unmuteAll = ->
###
Switch to a new layout.
Param:
newLayout : name of the layout as defined in layout.xml (found in /var/www/bigbluebutton/client/conf/layout.xml)
###
BBB.switchLayout = (newLayout) ->
###
Lock the layout.
Locking the layout means that users will have the same layout with the moderator that issued the lock command.
Other users won't be able to move or resize the different windows.
###
BBB.lockLayout = (lock) ->
###
Request to send a public chat
fromUserID - the external user id for the sender
fontColor - the color of the font to display the message
localeLang - the 2-char locale code (e.g. en) for the sender
message - the message to send
###
BBB.sendPublicChatMessage = (fontColor, localeLang, message) ->
messageForServer = { # construct message for server
"message": message
"chat_type": "PUBLIC_CHAT"
"from_userid": getInSession("userId")
"from_username": BBB.getMyUserName()
"from_tz_offset": "240"
"to_username": "public_chat_username"
"to_userid": "public_chat_userid"
"from_lang": localeLang
"from_time": getTime()
"from_color": fontColor
}
Meteor.call "sendChatMessagetoServer", BBB.getMeetingId(), messageForServer, getInSession("userId"), getInSession("authToken")
###
Request to send a private chat
fromUserID - the external user id for the sender
fontColor - the color of the font to display the message
localeLang - the 2-char locale code (e.g. en) for the sender
message - the message to send
toUserID - the external user id of the receiver
###
BBB.sendPrivateChatMessage = (fontColor, localeLang, message, toUserID, toUserName) ->
messageForServer = { # construct message for server
"message": message
"chat_type": "PRIVATE_CHAT"
"from_userid": getInSession("userId")
"from_username": BBB.getMyUserName()
"from_tz_offset": "240"
"to_username": toUserName
"to_userid": toUserID
"from_lang": localeLang
"from_time": getTime()
"from_color": fontColor
}
Meteor.call "sendChatMessagetoServer", BBB.getMeetingId(), messageForServer, getInSession("userId"), getInSession("authToken")
###
Request to display a presentation.
presentationID - the presentation to display
###
BBB.displayPresentation = (presentationID) ->
###
Query the list of uploaded presentations.
###
BBB.queryListOfPresentations = ->
###
Request to delete a presentation.
presentationID - the presentation to delete
###
BBB.deletePresentation = (presentationID) ->
# Request to switch the presentation to the previous slide
BBB.goToPreviousPage = () ->
Meteor.call('publishSwitchToPreviousSlideMessage',
getInSession('meetingId'),
getInSession('userId'),
getInSession('authToken'))
# Request to switch the presentation to the next slide
BBB.goToNextPage = () ->
Meteor.call('publishSwitchToNextSlideMessage',
getInSession('meetingId'),
getInSession('userId'),
getInSession('authToken'))
BBB.webRTCConferenceCallStarted = ->
BBB.webRTCConferenceCallConnecting = ->
BBB.webRTCConferenceCallEnded = ->
BBB.webRTCConferenceCallFailed = (errorcode) ->
BBB.webRTCConferenceCallWaitingForICE = ->
BBB.webRTCCallProgressCallback = (progress) ->
BBB.webRTCEchoTestStarted = ->
BBB.webRTCEchoTestConnecting = ->
BBB.webRTCEchoTestFailed = (reason) ->
BBB.webRTCEchoTestWaitingForICE = ->
BBB.webRTCEchoTestEnded = ->
BBB.webRTCMediaRequest = ->
BBB.webRTCMediaSuccess = ->
BBB.webRTCMediaFail = ->
BBB.webRTCWebcamRequest = ->
BBB.webRTCWebcamRequestSuccess = ->
BBB.webRTCWebcamRequestFail = (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 **
###
listeners = {}
###
3rd-party apps should use this method to register to listen for events.
###
BBB.listen = (eventName, handler) ->
###
3rd-party app should use this method to unregister listener for a given event.
###
BBB.unlisten = (eventName, handler) ->
BBB.init = (callback) ->
BBB
)()

View File

@ -0,0 +1,532 @@
/*
This file contains the BigBlueButton client APIs that will allow 3rd-party applications
to embed the HTML5 client and interact with it through Javascript.
HOW TO USE:
Some APIs allow synchronous and asynchronous calls. When using asynchronous, the 3rd-party
JS should register as listener for events listed at the bottom of this file. For synchronous,
3rd-party JS should pass in a callback function when calling the API.
For an example on how to use these APIs, see:
https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/lib/3rd-party.js
https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/3rd-party.html
*/
this.BBB = (function() {
let BBB, listeners, returnOrCallback;
BBB = {};
returnOrCallback = function(res, callback) {
if ((callback != null) && typeof callback === "function") {
return callback(res);
} else {
return res;
}
};
BBB.isPollGoing = function(userId) {
if (userId !== void 0 && Meteor.Polls.findOne({
"poll_info.users": userId
})) {
return true;
} else {
return false;
}
};
BBB.getCurrentPoll = function(userId) {
if (userId !== void 0 && Meteor.Polls.findOne({
"poll_info.users": userId
})) {
return Meteor.Polls.findOne({
"poll_info.users": userId
});
}
};
BBB.sendPollResponseMessage = function(key, pollAnswerId) {
return Meteor.call("publishVoteMessage", BBB.getMeetingId(), pollAnswerId, getInSession("userId"), getInSession("authToken"));
};
BBB.getMeetingId = function() {
let ref;
return (ref = Meteor.Meetings.findOne()) != null ? ref.meetingId : void 0;
};
BBB.getInternalMeetingId = function(callback) {};
/*
Queryies the user object via it's id
*/
BBB.getUser = function(userId) {
return Meteor.Users.findOne({
userId: userId
});
};
BBB.getCurrentUser = function() {
return BBB.getUser(getInSession("userId"));
};
/*
Query if the current user is sharing webcam.
Param:
callback - function to return the result
If you want to instead receive an event with the result, register a listener
for AM_I_SHARING_CAM_RESP (see below).
*/
BBB.amISharingWebcam = function(callback) {
// BBB.isUserSharingWebcam BBB.getCurrentUser()?.userId
return false;
};
/*
Query if another user is sharing her camera.
Param:
userID : the id of the user that may be sharing the camera
callback: function if you want to be informed synchronously. Don't pass a function
if you want to be informed through an event. You have to register for
IS_USER_PUBLISHING_CAM_RESP (see below).
*/
BBB.isUserSharingWebcam = function(userId, callback) {
// BBB.getUser(userId)?.user?.webcam_stream?.length isnt 0
return false;
};
// returns whether the user has joined any type of audio
BBB.amIInAudio = function(callback) {
let ref, ref1, ref2, user;
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);
};
// returns true if the user has joined the listen only audio stream
BBB.amIListenOnlyAudio = function(callback) {
let ref, ref1;
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) {
let ref;
return BBB.isUserSharingAudio((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0);
};
// returns whether the user is currently talking
BBB.amITalking = function(callback) {
let ref;
return BBB.isUserTalking((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0);
};
BBB.isUserInAudio = function(userId, callback) {
let ref, ref1, ref2, user;
user = BBB.getUser(userId);
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);
};
BBB.isUserListenOnlyAudio = function(userId, callback) {
let ref, ref1;
return (ref = BBB.getUser(userId)) != null ? (ref1 = ref.user) != null ? ref1.listenOnly : void 0 : void 0;
};
BBB.isUserSharingAudio = function(userId, callback) {
let ref, ref1, ref2;
return (ref = BBB.getUser(userId)) != null ? (ref1 = ref.user) != null ? (ref2 = ref1.voiceUser) != null ? ref2.joined : void 0 : void 0 : void 0;
};
BBB.isUserTalking = function(userId, callback) {
let ref, ref1, ref2;
return (ref = BBB.getUser(userId)) != null ? (ref1 = ref.user) != null ? (ref2 = ref1.voiceUser) != null ? ref2.talking : void 0 : void 0 : void 0;
};
BBB.isUserPresenter = function(userId, callback) {
let ref, ref1;
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() {
let ref;
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() {
let lockedMicForRoom, ref;
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();
};
BBB.getCurrentSlide = function(callingLocaton) {
let currentPresentation, currentSlide, presentationId, ref;
currentPresentation = Meteor.Presentations.findOne({
"presentation.current": true
});
presentationId = currentPresentation != null ? (ref = currentPresentation.presentation) != null ? ref.id : void 0 : void 0;
currentSlide = Meteor.Slides.findOne({
"presentationId": presentationId,
"slide.current": true
});
// console.log "trigger:#{callingLocaton} currentSlideId=#{currentSlide?._id}"
return currentSlide;
};
BBB.getMeetingName = function() {
let ref;
return ((ref = Meteor.Meetings.findOne()) != null ? ref.meetingName : void 0) || null;
};
BBB.getNumberOfUsers = function() {
return Meteor.Users.find().count();
};
BBB.currentPresentationName = function() {
let ref, ref1;
return (ref = Meteor.Presentations.findOne({
"presentation.current": true
})) != null ? (ref1 = ref.presentation) != null ? ref1.name : void 0 : void 0;
};
/*
Raise user's hand.
Param:
*/
BBB.lowerHand = function(meetingId, toUserId, byUserId, byAuthToken) {
return Meteor.call('userLowerHand', meetingId, toUserId, byUserId, byAuthToken);
};
BBB.raiseHand = function(meetingId, toUserId, byUserId, byAuthToken) {
return Meteor.call('userRaiseHand', meetingId, toUserId, byUserId, byAuthToken);
};
BBB.setEmojiStatus = function(meetingId, toUserId, byUserId, byAuthToken, status) {
return Meteor.call('userSetEmoji', meetingId, toUserId, byUserId, byAuthToken, status);
};
BBB.isUserEmojiStatusSet = function(userId) {
let ref, ref1, ref2, ref3;
return ((ref = BBB.getUser(userId)) != null ? (ref1 = ref.user) != null ? ref1.emoji_status : void 0 : void 0) !== "none" && ((ref2 = BBB.getUser(userId)) != null ? (ref3 = ref2.user) != null ? ref3.emoji_status : void 0 : void 0) !== void 0;
};
BBB.isCurrentUserEmojiStatusSet = function() {
let ref;
return BBB.isUserEmojiStatusSet((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0);
};
BBB.isMeetingRecording = function() {
let ref;
return (ref = MEteor.Meetings.findOne()) != null ? ref.recorded : void 0;
};
/*
Issue a switch presenter command.
Param:
newPresenterUserID - the user id of the new presenter
3rd-party JS must listen for SWITCHED_PRESENTER (see below) to get notified
of switch presenter events.
*/
BBB.switchPresenter = function(newPresenterUserID) {};
/*
Query if current user is presenter.
Params:
callback - function if you want a callback as response. Otherwise, you need to listen
for AM_I_PRESENTER_RESP (see below).
*/
BBB.amIPresenter = function(callback) {
return returnOrCallback(false, callback);
};
/*
Eject a user.
Params:
userID - userID of the user you want to eject.
*/
BBB.ejectUser = function(userID) {};
/*
Query who is presenter.
Params:
callback - function that gets executed for the response.
*/
BBB.getPresenterUserID = function(callback) {};
/*
Query the current user's role.
Params:
callback - function if you want a callback as response. Otherwise, you need to listen
for GET_MY_ROLE_RESP (see below).
*/
BBB.getMyRole = function(callback) {
return returnOrCallback("VIEWER", callback);
};
/*
Query the current user's id.
Params:
callback - function that gets executed for the response.
*/
BBB.getMyUserID = function(callback) {
return returnOrCallback(getInSession("userId"), callback);
};
BBB.getMyDBID = function(callback) {
let ref;
return returnOrCallback((ref = Meteor.Users.findOne({
userId: getInSession("userId")
})) != null ? ref._id : void 0, callback);
};
BBB.getMyUserName = function(callback) {
let ref;
return BBB.getUserName((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0);
};
BBB.getMyVoiceBridge = function(callback) {
let res;
res = Meteor.Meetings.findOne({}).voiceConf;
return returnOrCallback(res, callback);
};
BBB.getUserName = function(userId, callback) {
let ref, ref1;
return returnOrCallback((ref = BBB.getUser(userId)) != null ? (ref1 = ref.user) != null ? ref1.name : void 0 : void 0, callback);
};
/*
Query the current user's role.
Params:
callback - function if you want a callback as response. Otherwise, you need to listen
for GET_MY_ROLE_RESP (see below).
*/
BBB.getMyUserInfo = function(callback) {
let result;
result = {
myUserID: BBB.getMyUserID(),
myUsername: BBB.getMyUserName(),
myInternalUserID: BBB.getMyUserID(),
myAvatarURL: null,
myRole: BBB.getMyRole(),
amIPresenter: BBB.amIPresenter(),
voiceBridge: BBB.getMyVoiceBridge(),
dialNumber: null
};
return returnOrCallback(result, callback);
};
/*
Query the meeting id.
Params:
callback - function that gets executed for the response.
*/
/*
Join the voice conference.
isListenOnly: signifies whether the user joining the conference audio requests to join the listen only stream
*/
BBB.joinVoiceConference = function(callback, isListenOnly) {
if (BBB.isMyMicLocked()) {
callIntoConference(BBB.getMyVoiceBridge(), callback, true);
}
return callIntoConference(BBB.getMyVoiceBridge(), callback, isListenOnly);
};
/*
Leave the voice conference.
*/
BBB.leaveVoiceConference = function(callback) {
return webrtc_hangup(callback);
};
/*
Get a hold of the object containing the call information
*/
BBB.getCallStatus = function() {
return getCallStatus();
};
/*
Share user's webcam.
Params:
publishInClient : (DO NOT USE - Unimplemented)
*/
BBB.shareVideoCamera = function(publishInClient) {};
/*
Stop share user's webcam.
*/
BBB.stopSharingCamera = function() {};
/*
Indicates if a user is muted
*/
BBB.isUserMuted = function(id) {
let ref, ref1, ref2;
return (ref = BBB.getUser(id)) != null ? (ref1 = ref.user) != null ? (ref2 = ref1.voiceUser) != null ? ref2.muted : void 0 : void 0 : void 0;
};
/*
Indicates if the current user is muted
*/
BBB.amIMuted = function() {
return BBB.isUserMuted(BBB.getCurrentUser().userId);
};
/*
Mute the current user.
*/
BBB.muteMe = function() {
return BBB.muteUser(getInSession("userId"), getInSession("userId"), getInSession("authToken"));
};
/*
Unmute the current user.
*/
BBB.unmuteMe = function() {
return BBB.unmuteUser(getInSession("userId"), getInSession("userId"), getInSession("authToken"));
};
BBB.muteUser = function(meetingId, userId, toMuteId, requesterId, requestToken) {
return Meteor.call('muteUser', meetingId, toMuteId, requesterId, getInSession("authToken"));
};
BBB.unmuteUser = function(meetingId, userId, toMuteId, requesterId, requestToken) {
return Meteor.call('unmuteUser', meetingId, toMuteId, requesterId, getInSession("authToken"));
};
BBB.toggleMyMic = function() {
let request;
request = BBB.amIMuted() ? "unmuteUser" : "muteUser";
return Meteor.call(request, BBB.getMeetingId(), getInSession("userId"), getInSession("userId"), getInSession("authToken"));
};
/*
Mute all the users.
*/
BBB.muteAll = function() {};
/*
Unmute all the users.
*/
BBB.unmuteAll = function() {};
/*
Switch to a new layout.
Param:
newLayout : name of the layout as defined in layout.xml (found in /var/www/bigbluebutton/client/conf/layout.xml)
*/
BBB.switchLayout = function(newLayout) {};
/*
Lock the layout.
Locking the layout means that users will have the same layout with the moderator that issued the lock command.
Other users won't be able to move or resize the different windows.
*/
BBB.lockLayout = function(lock) {};
/*
Request to send a public chat
fromUserID - the external user id for the sender
fontColor - the color of the font to display the message
localeLang - the 2-char locale code (e.g. en) for the sender
message - the message to send
*/
BBB.sendPublicChatMessage = function(fontColor, localeLang, message) {
let messageForServer;
messageForServer = {
"message": message,
"chat_type": "PUBLIC_CHAT",
"from_userid": getInSession("userId"),
"from_username": BBB.getMyUserName(),
"from_tz_offset": "240",
"to_username": "public_chat_username",
"to_userid": "public_chat_userid",
"from_lang": localeLang,
"from_time": getTime(),
"from_color": fontColor
};
return Meteor.call("sendChatMessagetoServer", BBB.getMeetingId(), messageForServer, getInSession("userId"), getInSession("authToken"));
};
/*
Request to send a private chat
fromUserID - the external user id for the sender
fontColor - the color of the font to display the message
localeLang - the 2-char locale code (e.g. en) for the sender
message - the message to send
toUserID - the external user id of the receiver
*/
BBB.sendPrivateChatMessage = function(fontColor, localeLang, message, toUserID, toUserName) {
let messageForServer;
messageForServer = {
"message": message,
"chat_type": "PRIVATE_CHAT",
"from_userid": getInSession("userId"),
"from_username": BBB.getMyUserName(),
"from_tz_offset": "240",
"to_username": toUserName,
"to_userid": toUserID,
"from_lang": localeLang,
"from_time": getTime(),
"from_color": fontColor
};
return Meteor.call("sendChatMessagetoServer", BBB.getMeetingId(), messageForServer, getInSession("userId"), getInSession("authToken"));
};
/*
Request to display a presentation.
presentationID - the presentation to display
*/
BBB.displayPresentation = function(presentationID) {};
/*
Query the list of uploaded presentations.
*/
BBB.queryListOfPresentations = function() {};
/*
Request to delete a presentation.
presentationID - the presentation to delete
*/
BBB.deletePresentation = function(presentationID) {};
// Request to switch the presentation to the previous slide
BBB.goToPreviousPage = function() {
return Meteor.call('publishSwitchToPreviousSlideMessage', getInSession('meetingId'), getInSession('userId'), getInSession('authToken'));
};
// Request to switch the presentation to the next slide
BBB.goToNextPage = function() {
return Meteor.call('publishSwitchToNextSlideMessage', getInSession('meetingId'), getInSession('userId'), getInSession('authToken'));
};
BBB.webRTCConferenceCallStarted = function() {};
BBB.webRTCConferenceCallConnecting = function() {};
BBB.webRTCConferenceCallEnded = function() {};
BBB.webRTCConferenceCallFailed = function(errorcode) {};
BBB.webRTCConferenceCallWaitingForICE = function() {};
BBB.webRTCCallProgressCallback = function(progress) {};
BBB.webRTCEchoTestStarted = function() {};
BBB.webRTCEchoTestConnecting = function() {};
BBB.webRTCEchoTestFailed = function(reason) {};
BBB.webRTCEchoTestWaitingForICE = function() {};
BBB.webRTCEchoTestEnded = function() {};
BBB.webRTCMediaRequest = function() {};
BBB.webRTCMediaSuccess = function() {};
BBB.webRTCMediaFail = function() {};
BBB.webRTCWebcamRequest = function() {};
BBB.webRTCWebcamRequestSuccess = function() {};
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 **
*/
listeners = {};
/*
3rd-party apps should use this method to register to listen for events.
*/
BBB.listen = function(eventName, handler) {};
/*
3rd-party app should use this method to unregister listener for a given event.
*/
BBB.unlisten = function(eventName, handler) {};
BBB.init = function(callback) {};
return BBB;
})();

View File

@ -1,325 +0,0 @@
# Helper to load javascript libraries from the BBB server
loadLib = (libname) ->
successCallback = ->
retryMessageCallback = (param) ->
#Meteor.log.info "Failed to load library", param
console.log "Failed to load library", param
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 ->
# Load SIP libraries before the application starts
loadLib('sip.js')
loadLib('bbb_webrtc_bridge_sip.js')
loadLib('bbblogger.js')
@SessionAmplify = _.extend({}, Session,
keys: _.object(_.map(amplify.store.sessionStorage(), (value, key) ->
[
key
JSON.stringify(value)
]
))
set: (key, value) ->
Session.set.apply this, arguments
amplify.store.sessionStorage key, value
return
)
#
Template.header.events
"click .chatBarIcon": (event) ->
$(".tooltip").hide()
toggleChatbar()
"click .hideNavbarIcon": (event) ->
$(".tooltip").hide()
toggleNavbar()
"click .leaveAudioButton": (event) ->
exitVoiceCall event
"click .muteIcon": (event) ->
$(".tooltip").hide()
toggleMic @
"click .hideNavbarIcon": (event) ->
$(".tooltip").hide()
toggleNavbar()
"click .videoFeedIcon": (event) ->
$(".tooltip").hide()
toggleCam @
"click .toggleUserlistButton": (event) ->
if isLandscape() or isLandscapeMobile()
toggleUsersList()
else
if $('.settingsMenu').hasClass('menuOut')
toggleSettingsMenu()
else
toggleShield()
toggleUserlistMenu()
"click .toggleMenuButton": (event) ->
if $('.userlistMenu').hasClass('menuOut')
toggleUserlistMenu()
else
toggleShield()
$('.toggleMenuButton').blur()
toggleSettingsMenu()
"click .btn": (event) ->
$(".ui-tooltip").hide()
Template.menu.events
'click .slideButton': (event) ->
toggleShield()
toggleSettingsMenu()
$('.slideButton').blur()
'click .toggleChatButton': (event) ->
toggleChatbar()
Template.main.rendered = ->
$("#dialog").dialog(
modal: true
draggable: false
resizable: false
autoOpen: false
dialogClass: 'no-close logout-dialog'
buttons: [
{
text: 'Yes'
click: () ->
userLogout BBB.getMeetingId(), getInSession("userId"), true
$(this).dialog("close")
class: 'btn btn-xs btn-primary active'
}
{
text: 'No'
click: () ->
$(this).dialog("close")
$(".tooltip").hide()
class: 'btn btn-xs btn-default'
}
]
open: (event, ui) ->
$('.ui-widget-overlay').bind 'click', () ->
if isMobile()
$("#dialog").dialog('close')
position:
my: 'right top'
at: 'right bottom'
of: '.signOutIcon'
)
# keep track of the last orientation
lastOrientationWasLandscape = isLandscape()
$(window).resize( ->
$('#dialog').dialog('close')
# when the orientation switches call the handler
if isLandscape() and not lastOrientationWasLandscape
orientationBecameLandscape()
lastOrientationWasLandscape = true
else if isPortrait() and lastOrientationWasLandscape
orientationBecamePortrait()
lastOrientationWasLandscape = false
)
$('#shield').click () ->
toggleSlidingMenu()
if Meteor.config.app.autoJoinAudio
if Meteor.config.app.skipCheck
joinVoiceCall @, isListenOnly: Meteor.config.app.listenOnly
else
$("#settingsModal").foundation('reveal', 'open')
if Meteor.config.app.listenOnly
$('#joinMicrophone').prop('disabled', true)
Template.main.events
'click .shield': (event) ->
$(".tooltip").hide()
toggleShield()
closeMenus()
'click .settingsIcon': (event) ->
$("#settingsModal").foundation('reveal', 'open');
'click .signOutIcon': (event) ->
$('.signOutIcon').blur()
$("#logoutModal").foundation('reveal', 'open');
Template.main.gestures
'panstart #container': (event, template) ->
if isPortraitMobile() and isPanHorizontal(event)
panIsValid = getInSession('panIsValid')
initTransformValue = getInSession('initTransform')
menuPanned = getInSession('menuPanned')
screenWidth = $('#container').width()
setInSession 'panStarted', true
if panIsValid and
menuPanned is 'left' and
initTransformValue + event.deltaX >= 0 and
initTransformValue + event.deltaX <= $('.left-drawer').width()
$('.left-drawer').css('transform', 'translateX(' + (initTransformValue + event.deltaX) + 'px)')
else if panIsValid and
menuPanned is 'right' and
initTransformValue + event.deltaX >= screenWidth - $('.right-drawer').width() and
initTransformValue + event.deltaX <= screenWidth
$('.right-drawer').css('transform', 'translateX(' + (initTransformValue + event.deltaX) + 'px)')
'panend #container': (event, template) ->
if isPortraitMobile()
panIsValid = getInSession('panIsValid')
menuPanned = getInSession('menuPanned')
leftDrawerWidth = $('.left-drawer').width()
screenWidth = $('#container').width()
setInSession 'panStarted', false
if panIsValid and
menuPanned is 'left' and
$('.left-drawer').css('transform') isnt 'none'
if parseInt($('.left-drawer').css('transform').split(',')[4]) < leftDrawerWidth / 2
$('.shield').removeClass('animatedShield')
$('.shield').css('opacity', '')
$('.left-drawer').removeClass('menuOut')
$('.left-drawer').css('transform', '')
$('.toggleUserlistButton').removeClass('menuToggledOn')
$('.shield').removeClass('darken') # in case it was opened by clicking a button
else
$('.left-drawer').css('transform', 'translateX(' + leftDrawerWidth + 'px)')
$('.shield').css('opacity', 0.5)
$('.left-drawer').addClass('menuOut')
$('.left-drawer').css('transform', '')
$('.toggleUserlistButton').addClass('menuToggledOn')
if panIsValid and
menuPanned is 'right' and
parseInt($('.right-drawer').css('transform').split(',')[4]) isnt leftDrawerWidth
if parseInt($('.right-drawer').css('transform').split(',')[4]) > screenWidth - $('.right-drawer').width() / 2
$('.shield').removeClass('animatedShield')
$('.shield').css('opacity', '')
$('.right-drawer').css('transform', 'translateX(' + screenWidth + 'px)')
$('.right-drawer').removeClass('menuOut')
$('.right-drawer').css('transform', '')
$('.toggleMenuButton').removeClass('menuToggledOn')
$('.shield').removeClass('darken') # in case it was opened by clicking a button
else
$('.shield').css('opacity', 0.5)
$('.right-drawer').css('transform', 'translateX(' + (screenWidth - $('.right-drawer').width()) + 'px)')
$('.right-drawer').addClass('menuOut')
$('.right-drawer').css('transform', '')
$('.toggleMenuButton').addClass('menuToggledOn')
$('.left-drawer').addClass('userlistMenu')
$('.userlistMenu').removeClass('left-drawer')
$('.right-drawer').addClass('settingsMenu')
$('.settingsMenu').removeClass('right-drawer')
'panright #container, panleft #container': (event, template) ->
if isPortraitMobile() and isPanHorizontal(event)
# panright/panleft is always triggered once right before panstart
if !getInSession('panStarted')
# opening the left-hand menu
if event.type is 'panright' and
event.center.x <= $('#container').width() * 0.1
setInSession 'panIsValid', true
setInSession 'menuPanned', 'left'
# closing the left-hand menu
else if event.type is 'panleft' and
event.center.x < $('#container').width() * 0.9
setInSession 'panIsValid', true
setInSession 'menuPanned', 'left'
# opening the right-hand menu
else if event.type is 'panleft' and
event.center.x >= $('#container').width() * 0.9
setInSession 'panIsValid', true
setInSession 'menuPanned', 'right'
# closing the right-hand menu
else if event.type is 'panright' and
event.center.x > $('#container').width() * 0.1
setInSession 'panIsValid', true
setInSession 'menuPanned', 'right'
else
setInSession 'panIsValid', false
setInSession 'eventType', event.type
if getInSession('menuPanned') is 'left'
if $('.userlistMenu').css('transform') isnt 'none' # menu is already transformed
setInSession 'initTransform', parseInt($('.userlistMenu').css('transform').split(',')[4]) # translateX value
else if $('.userlistMenu').hasClass('menuOut')
setInSession 'initTransform', $('.userlistMenu').width()
else
setInSession 'initTransform', 0
$('.userlistMenu').addClass('left-drawer')
$('.left-drawer').removeClass('userlistMenu') # to prevent animations from Sled library
else if getInSession('menuPanned') is 'right'
if $('.settingsMenu').css('transform') isnt 'none' # menu is already transformed
setInSession 'initTransform', parseInt($('.settingsMenu').css('transform').split(',')[4]) # translateX value
else if $('.settingsMenu').hasClass('menuOut')
setInSession 'initTransform', $('.settingsMenu').width()
else
setInSession 'initTransform', 0
$('.settingsMenu').addClass('right-drawer')
$('.right-drawer').removeClass('settingsMenu') # to prevent animations from Sled library
initTransformValue = getInSession('initTransform')
panIsValid = getInSession('panIsValid')
menuPanned = getInSession('menuPanned')
leftDrawerWidth = $('.left-drawer').width()
rightDrawerWidth = $('.right-drawer').width()
screenWidth = $('#container').width()
# moving the left-hand menu
if panIsValid and
menuPanned is 'left' and
initTransformValue + event.deltaX >= 0 and
initTransformValue + event.deltaX <= leftDrawerWidth
if $('.settingsMenu').hasClass('menuOut')
toggleSettingsMenu()
$('.left-drawer').css('transform', 'translateX(' + (initTransformValue + event.deltaX) + 'px)')
if !getInSession('panStarted')
$('.shield').addClass('animatedShield')
$('.shield').css('opacity',
0.5 * (initTransformValue + event.deltaX) / leftDrawerWidth)
# moving the right-hand menu
else if panIsValid and
menuPanned is 'right' and
initTransformValue + event.deltaX >= screenWidth - rightDrawerWidth and
initTransformValue + event.deltaX <= screenWidth
if $('.userlistMenu').hasClass('menuOut')
toggleUserlistMenu()
$('.right-drawer').css('transform', 'translateX(' + (initTransformValue + event.deltaX) + 'px)')
if !getInSession('panStarted')
$('.shield').addClass('animatedShield')
$('.shield').css('opacity',
0.5 * (screenWidth - initTransformValue - event.deltaX) / rightDrawerWidth)
Template.makeButton.rendered = ->
$('button[rel=tooltip]').tooltip()

View File

@ -0,0 +1,336 @@
let loadLib;
// Helper to load javascript libraries from the BBB server
loadLib = function(libname) {
let retryMessageCallback, successCallback;
successCallback = function() {};
retryMessageCallback = function(param) {
//return(Meteor.log.info("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);
};
// These settings can just be stored locally in session, created at start up
Meteor.startup(() => {
// Load SIP libraries before the application starts
loadLib('sip.js');
loadLib('bbb_webrtc_bridge_sip.js');
loadLib('bbblogger.js');
return this.SessionAmplify = _.extend({}, Session, {
keys: _.object(_.map(amplify.store.sessionStorage(), (value, key) => {
return [key, JSON.stringify(value)];
})),
set(key, value) {
Session.set.apply(this, arguments);
amplify.store.sessionStorage(key, value);
}
});
});
//
Template.header.events({
"click .chatBarIcon"(event) {
$(".tooltip").hide();
return toggleChatbar();
},
"click .hideNavbarIcon"(event) {
$(".tooltip").hide();
return toggleNavbar();
},
"click .leaveAudioButton"(event) {
return exitVoiceCall(event);
},
"click .muteIcon"(event) {
$(".tooltip").hide();
return toggleMic(this);
},
"click .hideNavbarIcon"(event) {
$(".tooltip").hide();
return toggleNavbar();
},
"click .videoFeedIcon"(event) {
$(".tooltip").hide();
return toggleCam(this);
},
"click .toggleUserlistButton"(event) {
if(isLandscape() || isLandscapeMobile()) {
return toggleUsersList();
} else {
if($('.settingsMenu').hasClass('menuOut')) {
toggleSettingsMenu();
} else {
toggleShield();
}
return toggleUserlistMenu();
}
},
"click .toggleMenuButton"(event) {
if($('.userlistMenu').hasClass('menuOut')) {
toggleUserlistMenu();
} else {
toggleShield();
}
$('.toggleMenuButton').blur();
return toggleSettingsMenu();
},
"click .btn"(event) {
return $(".ui-tooltip").hide();
}
});
Template.menu.events({
'click .slideButton'(event) {
toggleShield();
toggleSettingsMenu();
return $('.slideButton').blur();
},
'click .toggleChatButton'(event) {
return toggleChatbar();
}
});
Template.main.rendered = function() {
let lastOrientationWasLandscape;
$("#dialog").dialog({
modal: true,
draggable: false,
resizable: false,
autoOpen: false,
dialogClass: 'no-close logout-dialog',
buttons: [
{
text: 'Yes',
click: function() {
userLogout(BBB.getMeetingId(), getInSession("userId"), true);
return $(this).dialog("close");
},
"class": 'btn btn-xs btn-primary active'
}, {
text: 'No',
click: function() {
$(this).dialog("close");
return $(".tooltip").hide();
},
"class": 'btn btn-xs btn-default'
}
],
open(event, ui) {
return $('.ui-widget-overlay').bind('click', () => {
if(isMobile()) {
return $("#dialog").dialog('close');
}
});
},
position: {
my: 'right top',
at: 'right bottom',
of: '.signOutIcon'
}
});
lastOrientationWasLandscape = isLandscape();
$(window).resize(() => {
$('#dialog').dialog('close');
// when the orientation switches call the handler
if(isLandscape() && !lastOrientationWasLandscape) {
orientationBecameLandscape();
return lastOrientationWasLandscape = true;
} else if(isPortrait() && lastOrientationWasLandscape) {
orientationBecamePortrait();
return lastOrientationWasLandscape = false;
}
});
$('#shield').click(() => {
return toggleSlidingMenu();
});
if(Meteor.config.app.autoJoinAudio) {
if(Meteor.config.app.skipCheck) {
return joinVoiceCall(this, {
isListenOnly: Meteor.config.app.listenOnly
});
} else {
$("#settingsModal").foundation('reveal', 'open');
if(Meteor.config.app.listenOnly) {
return $('#joinMicrophone').prop('disabled', true);
}
}
}
};
Template.main.events({
'click .shield'(event) {
$(".tooltip").hide();
toggleShield();
return closeMenus();
},
'click .settingsIcon'(event) {
return $("#settingsModal").foundation('reveal', 'open');
},
'click .signOutIcon'(event) {
$('.signOutIcon').blur();
return $("#logoutModal").foundation('reveal', 'open');
}
});
Template.main.gestures({
'panstart #container'(event, template) {
let initTransformValue, menuPanned, panIsValid, screenWidth;
if(isPortraitMobile() && isPanHorizontal(event)) {
panIsValid = getInSession('panIsValid');
initTransformValue = getInSession('initTransform');
menuPanned = getInSession('menuPanned');
screenWidth = $('#container').width();
setInSession('panStarted', true);
if(panIsValid && menuPanned === 'left' && initTransformValue + event.deltaX >= 0 && initTransformValue + event.deltaX <= $('.left-drawer').width()) {
return $('.left-drawer').css('transform', `translateX(${initTransformValue + event.deltaX}px)`);
} else if(panIsValid && menuPanned === 'right' && initTransformValue + event.deltaX >= screenWidth - $('.right-drawer').width() && initTransformValue + event.deltaX <= screenWidth) {
return $('.right-drawer').css('transform', `translateX(${initTransformValue + event.deltaX}px)`);
}
}
},
'panend #container'(event, template) {
let leftDrawerWidth, menuPanned, panIsValid, screenWidth;
if(isPortraitMobile()) {
panIsValid = getInSession('panIsValid');
menuPanned = getInSession('menuPanned');
leftDrawerWidth = $('.left-drawer').width();
screenWidth = $('#container').width();
setInSession('panStarted', false);
if(panIsValid && menuPanned === 'left' && $('.left-drawer').css('transform') !== 'none') {
if(parseInt($('.left-drawer').css('transform').split(',')[4]) < leftDrawerWidth / 2) {
$('.shield').removeClass('animatedShield');
$('.shield').css('opacity', '');
$('.left-drawer').removeClass('menuOut');
$('.left-drawer').css('transform', '');
$('.toggleUserlistButton').removeClass('menuToggledOn');
$('.shield').removeClass('darken');
} else {
$('.left-drawer').css('transform', `translateX(${leftDrawerWidth}px)`);
$('.shield').css('opacity', 0.5);
$('.left-drawer').addClass('menuOut');
$('.left-drawer').css('transform', '');
$('.toggleUserlistButton').addClass('menuToggledOn');
}
}
if(panIsValid && menuPanned === 'right' && parseInt($('.right-drawer').css('transform').split(',')[4]) !== leftDrawerWidth) {
if(parseInt($('.right-drawer').css('transform').split(',')[4]) > screenWidth - $('.right-drawer').width() / 2) {
$('.shield').removeClass('animatedShield');
$('.shield').css('opacity', '');
$('.right-drawer').css('transform', `translateX(${screenWidth}px)`);
$('.right-drawer').removeClass('menuOut');
$('.right-drawer').css('transform', '');
$('.toggleMenuButton').removeClass('menuToggledOn');
$('.shield').removeClass('darken'); // in case it was opened by clicking a button
} else {
$('.shield').css('opacity', 0.5);
$('.right-drawer').css('transform', `translateX(${screenWidth - $('.right-drawer').width()}px)`);
$('.right-drawer').addClass('menuOut');
$('.right-drawer').css('transform', '');
$('.toggleMenuButton').addClass('menuToggledOn');
}
}
$('.left-drawer').addClass('userlistMenu');
$('.userlistMenu').removeClass('left-drawer');
$('.right-drawer').addClass('settingsMenu');
return $('.settingsMenu').removeClass('right-drawer');
}
},
'panright #container, panleft #container'(event, template) {
let initTransformValue, leftDrawerWidth, menuPanned, panIsValid, rightDrawerWidth, screenWidth;
if(isPortraitMobile() && isPanHorizontal(event)) {
// panright/panleft is always triggered once right before panstart
if(!getInSession('panStarted')) {
// opening the left-hand menu
if(event.type === 'panright' && event.center.x <= $('#container').width() * 0.1) {
setInSession('panIsValid', true);
setInSession('menuPanned', 'left');
// closing the left-hand menu
} else if(event.type === 'panleft' && event.center.x < $('#container').width() * 0.9) {
setInSession('panIsValid', true);
setInSession('menuPanned', 'left');
// opening the right-hand menu
} else if(event.type === 'panleft' && event.center.x >= $('#container').width() * 0.9) {
setInSession('panIsValid', true);
setInSession('menuPanned', 'right');
// closing the right-hand menu
} else if(event.type === 'panright' && event.center.x > $('#container').width() * 0.1) {
setInSession('panIsValid', true);
setInSession('menuPanned', 'right');
} else {
setInSession('panIsValid', false);
}
setInSession('eventType', event.type);
if(getInSession('menuPanned') === 'left') {
if($('.userlistMenu').css('transform') !== 'none') { // menu is already transformed
setInSession(
'initTransform',
parseInt($('.userlistMenu').css('transform').split(',')[4])
); // translateX value
} else if($('.userlistMenu').hasClass('menuOut')) {
setInSession('initTransform', $('.userlistMenu').width());
} else {
setInSession('initTransform', 0);
}
$('.userlistMenu').addClass('left-drawer');
$('.left-drawer').removeClass('userlistMenu');
} else if(getInSession('menuPanned') === 'right') {
if($('.settingsMenu').css('transform') !== 'none') { // menu is already transformed
setInSession(
'initTransform',
parseInt($('.settingsMenu').css('transform').split(',')[4])
); // translateX value
} else if($('.settingsMenu').hasClass('menuOut')) {
setInSession('initTransform', $('.settingsMenu').width());
} else {
setInSession('initTransform', 0);
}
$('.settingsMenu').addClass('right-drawer');
$('.right-drawer').removeClass('settingsMenu');
}
}
initTransformValue = getInSession('initTransform');
panIsValid = getInSession('panIsValid');
menuPanned = getInSession('menuPanned');
leftDrawerWidth = $('.left-drawer').width();
rightDrawerWidth = $('.right-drawer').width();
screenWidth = $('#container').width();
// moving the left-hand menu
if(panIsValid &&
menuPanned === 'left' &&
initTransformValue + event.deltaX >= 0 &&
initTransformValue + event.deltaX <= leftDrawerWidth) {
if($('.settingsMenu').hasClass('menuOut')) {
toggleSettingsMenu();
}
$('.left-drawer').css('transform', `translateX(${initTransformValue + event.deltaX}px)`);
if(!getInSession('panStarted')) {
$('.shield').addClass('animatedShield');
}
return $('.shield').css('opacity', 0.5 * (initTransformValue + event.deltaX) / leftDrawerWidth);
} else if(panIsValid &&
menuPanned === 'right' &&
initTransformValue + event.deltaX >= screenWidth - rightDrawerWidth &&
initTransformValue + event.deltaX <= screenWidth) { // moving the right-hand menu
if($('.userlistMenu').hasClass('menuOut')) {
toggleUserlistMenu();
}
$('.right-drawer').css('transform', `translateX(${initTransformValue + event.deltaX}px)`);
if(!getInSession('panStarted')) {
$('.shield').addClass('animatedShield');
}
return $('.shield').css('opacity', 0.5 * (screenWidth - initTransformValue - event.deltaX) / rightDrawerWidth);
}
}
}
});
Template.makeButton.rendered = function() {
return $('button[rel=tooltip]').tooltip();
};

View File

@ -1,216 +0,0 @@
# --------------------------------------------------------------------------------------------------------------------
# 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.
# --------------------------------------------------------------------------------------------------------------------
@activateBreakLines = (str) ->
if typeof str is 'string'
# turn '\r' carriage return characters into '<br/>' break lines
res = str.replace(new RegExp(CARRIAGE_RETURN, 'g'), BREAK_LINE)
res
@detectUnreadChat = ->
#if the current tab is not the same as the tab we just published in
Meteor.Chat.find({}).observe({
added: (chatMessage) =>
findDestinationTab = ->
if chatMessage.message?.chat_type is "PUBLIC_CHAT"
"PUBLIC_CHAT"
else
chatMessage.message?.from_userid
Tracker.autorun (comp) ->
tabsTime = getInSession('userListRenderedTime')
if tabsTime? and chatMessage.message.from_userid isnt "SYSTEM_MESSAGE" and chatMessage.message.from_time - tabsTime > 0
populateNotifications(chatMessage) # check if we need to show a new notification
destinationTab = findDestinationTab()
if destinationTab isnt getInSession "inChatWith"
setInSession 'chats', getInSession('chats').map((tab) ->
if tab.userId is destinationTab
tab.gotMail = true
tab.number++
tab
)
comp.stop()
})
# 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
@getFormattedMessagesForChat = ->
chattingWith = getInSession('inChatWith')
if chattingWith is 'PUBLIC_CHAT' # find all public and system messages
return Meteor.Chat.find({'message.chat_type': $in: ["SYSTEM_MESSAGE","PUBLIC_CHAT"]},{sort: {'message.from_time': 1}}).fetch()
else
return Meteor.Chat.find({'message.chat_type': 'PRIVATE_CHAT', $or: [{'message.to_userid': chattingWith},{'message.from_userid': chattingWith}]}).fetch()
# Scrolls the message container to the bottom. The number of pixels to scroll down is the height of the container
Handlebars.registerHelper "autoscroll", ->
$('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight)
false
# true if the lock settings limit public chat and the current user is locked
Handlebars.registerHelper "publicChatDisabled", ->
userIsLocked = Meteor.Users.findOne({userId:getInSession 'userId'})?.user.locked
publicChatIsDisabled = Meteor.Meetings.findOne({})?.roomLockSettings.disablePublicChat
presenter = Meteor.Users.findOne({userId:getInSession 'userId'})?.user.presenter
return userIsLocked and publicChatIsDisabled and !presenter
# true if the lock settings limit private chat and the current user is locked
Handlebars.registerHelper "privateChatDisabled", ->
userIsLocked = Meteor.Users.findOne({userId:getInSession 'userId'})?.user.locked
privateChatIsDisabled = Meteor.Meetings.findOne({})?.roomLockSettings.disablePrivateChat
presenter = Meteor.Users.findOne({userId:getInSession 'userId'})?.user.presenter
return userIsLocked and privateChatIsDisabled and !presenter
# return whether the user's chat pane is open in Private chat
Handlebars.registerHelper "inPrivateChat", ->
return (getInSession 'inChatWith') isnt 'PUBLIC_CHAT'
@sendMessage = ->
message = linkify $('#newMessageInput').val() # get the message from the input box
unless (message?.length > 0 and (/\S/.test(message))) # check the message has content and it is not whitespace
return # do nothing if invalid message
color = "0x000000" #"0x#{getInSession("messageColor")}"
if (chattingWith = getInSession('inChatWith')) isnt "PUBLIC_CHAT"
toUsername = Meteor.Users.findOne(userId: chattingWith)?.user.name
BBB.sendPrivateChatMessage(color, "en", message, chattingWith, toUsername)
else
BBB.sendPublicChatMessage(color, "en", message)
$('#newMessageInput').val '' # Clear message box
Template.chatbar.helpers
getCombinedMessagesForChat: ->
msgs = getFormattedMessagesForChat()
len = msgs?.length # get length of messages
i = 0
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 isnt 'System' # skip system messages
j = i+1 # Start looking at messages right after the current one
while j < len
deleted = false
if msgs[j].message.from_userid isnt 'System' # Ignore system messages
# Check if the time discrepancy between the two messages exceeds window for grouping
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 is msgs[j].message.from_userid # Both messages are from the same user
# insert a '\r' carriage return character between messages to put them on a new line
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
else break # Messages are from different people, move on
#
else break # This is the break point in the chat, don't merge
#
len = msgs.length
++j if not deleted
#
++i
len = msgs.length
msgs
userExists: ->
if getInSession('inChatWith') is "PUBLIC_CHAT"
return true
else
return Meteor.Users.findOne({userId: getInSession('inChatWith')})?
# When chatbar gets rendered, launch the auto-check for unread chat
Template.chatbar.rendered = ->
detectUnreadChat()
# When "< Public" is clicked, go to public chat
Template.chatbar.events
'click .toPublic': (event) ->
setInSession 'inChatWith', 'PUBLIC_CHAT'
setInSession 'chats', getInSession('chats').map((chat) ->
if chat.userId is "PUBLIC_CHAT"
chat.gotMail = false
chat.number = 0
chat
)
Template.privateChatTab.rendered = ->
if isLandscape() or isPortrait()
$("#newMessageInput").focus()
# When message gets rendered, scroll to the bottom
Template.message.rendered = ->
$('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight)
false
Template.chatInput.rendered = ->
$('.panel-footer').resizable
handles: 'n'
minHeight: 70
resize: (event, ui) ->
if $('.panel-footer').css('top') is '0px'
$('.panel-footer').height(70) # prevents the element from shrinking vertically for 1-2 px
else
$('.panel-footer').css('top', parseInt($('.panel-footer').css('top')) + 1 + 'px')
$('#chatbody').height($('#chat').height() - $('.panel-footer').height() - 45)
$('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight)
start: (event, ui) ->
$('#newMessageInput').css('overflow', '')
$('.panel-footer').resizable('option', 'maxHeight', Math.max($('.panel-footer').height(), $('#chat').height() / 2))
stop: (event, ui) ->
setInSession 'chatInputMinHeight', $('.panel-footer').height() + 1
Template.chatInput.events
'click #sendMessageButton': (event) ->
$('#sendMessageButton').blur()
sendMessage()
adjustChatInputHeight()
'keypress #newMessageInput': (event) -> # user pressed a button inside the chatbox
key = (if event.charCode then event.charCode else (if event.keyCode then event.keyCode else 0))
if event.shiftKey and (key is 13)
event.preventDefault()
# 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
if key is 13 # Check for pressing enter to submit message
event.preventDefault()
sendMessage()
$('#newMessageInput').val("")
return false
Template.chatInputControls.rendered = ->
$('#newMessageInput').on('keydown paste cut', () -> setTimeout(() ->
adjustChatInputHeight()
, 0))
Template.message.helpers
sanitizeAndFormat: (str) ->
if typeof str is '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 = toClickable res
res = activateBreakLines res
toClockTime: (epochTime) ->
if epochTime is null
return ""
local = new Date()
offset = local.getTimezoneOffset()
epochTime = epochTime - offset * 60000 # 1 min = 60 s = 60,000 ms
dateObj = new Date(epochTime)
hours = dateObj.getUTCHours()
minutes = dateObj.getUTCMinutes()
if minutes < 10
minutes = "0" + minutes
hours + ":" + minutes
# make links received from Flash client clickable in HTML
@toClickable = (str) ->
if typeof str is 'string'
res = str.replace /<a href='event:/gim, "<a target='_blank' href='"
res = res.replace /<a href="event:/gim, '<a target="_blank" href="'

View File

@ -0,0 +1,309 @@
// --------------------------------------------------------------------------------------------------------------------
// 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) {
let res;
if(typeof str === 'string') { // turn '\r' carriage return characters into '<br/>' break lines
res = str.replace(new RegExp(CARRIAGE_RETURN, 'g'), BREAK_LINE);
return res;
}
};
this.detectUnreadChat = function() {
//if the current tab is not the same as the tab we just published in
return Meteor.Chat.find({}).observe({
added: (_this => {
return function(chatMessage) {
let findDestinationTab;
findDestinationTab = function() {
let ref, ref1;
if(((ref = chatMessage.message) != null ? ref.chat_type : void 0) === "PUBLIC_CHAT") {
return "PUBLIC_CHAT";
} else {
return (ref1 = chatMessage.message) != null ? ref1.from_userid : void 0;
}
};
return Tracker.autorun(comp => {
let destinationTab, tabsTime;
tabsTime = getInSession('userListRenderedTime');
if((tabsTime != null) && chatMessage.message.from_userid !== "SYSTEM_MESSAGE" && chatMessage.message.from_time - tabsTime > 0) {
populateNotifications(chatMessage); // check if we need to show a new notification
destinationTab = findDestinationTab();
if(destinationTab !== getInSession("inChatWith")) {
setInSession('chats', getInSession('chats').map(tab => {
if(tab.userId === destinationTab) {
tab.gotMail = true;
tab.number++;
}
return tab;
}));
}
}
return comp.stop();
});
};
})(this)
});
};
// 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() {
let chattingWith;
chattingWith = getInSession('inChatWith');
if(chattingWith === 'PUBLIC_CHAT') { // find all public and system messages
return Meteor.Chat.find({
'message.chat_type': {
$in: ["SYSTEM_MESSAGE", "PUBLIC_CHAT"]
}
}, {
sort: {
'message.from_time': 1
}
}).fetch();
} else {
return Meteor.Chat.find({
'message.chat_type': 'PRIVATE_CHAT',
$or: [
{
'message.to_userid': chattingWith
}, {
'message.from_userid': chattingWith
}
]
}).fetch();
}
};
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;
$('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
return false;
});
// true if the lock settings limit public chat and the current user is locked
Handlebars.registerHelper("publicChatDisabled", () => {
let presenter, publicChatIsDisabled, ref, ref1, ref2, userIsLocked;
userIsLocked = (ref = Meteor.Users.findOne({
userId: getInSession('userId')
})) != null ? ref.user.locked : void 0;
publicChatIsDisabled = (ref1 = Meteor.Meetings.findOne({})) != null ? ref1.roomLockSettings.disablePublicChat : void 0;
presenter = (ref2 = Meteor.Users.findOne({
userId: getInSession('userId')
})) != null ? ref2.user.presenter : void 0;
return userIsLocked && publicChatIsDisabled && !presenter;
});
// true if the lock settings limit private chat and the current user is locked
Handlebars.registerHelper("privateChatDisabled", () => {
let presenter, privateChatIsDisabled, ref, ref1, ref2, userIsLocked;
userIsLocked = (ref = Meteor.Users.findOne({
userId: getInSession('userId')
})) != null ? ref.user.locked : void 0;
privateChatIsDisabled = (ref1 = Meteor.Meetings.findOne({})) != null ? ref1.roomLockSettings.disablePrivateChat : void 0;
presenter = (ref2 = Meteor.Users.findOne({
userId: getInSession('userId')
})) != null ? ref2.user.presenter : void 0;
return userIsLocked && privateChatIsDisabled && !presenter;
});
// return whether the user's chat pane is open in Private chat
Handlebars.registerHelper("inPrivateChat", () => {
return (getInSession('inChatWith')) !== 'PUBLIC_CHAT';
});
this.sendMessage = function() {
let chattingWith, color, message, ref, toUsername;
message = linkify($('#newMessageInput').val()); // get the message from the input box
if(!((message != null ? message.length : void 0) > 0 && (/\S/.test(message)))) { // check the message has content and it is not whitespace
return; // do nothing if invalid message
}
color = "0x000000"; //"0x#{getInSession("messageColor")}"
if((chattingWith = getInSession('inChatWith')) !== "PUBLIC_CHAT") {
toUsername = (ref = Meteor.Users.findOne({
userId: chattingWith
})) != null ? ref.user.name : void 0;
BBB.sendPrivateChatMessage(color, "en", message, chattingWith, toUsername);
} else {
BBB.sendPublicChatMessage(color, "en", message);
}
return $('#newMessageInput').val(''); // Clear message box
};
Template.chatbar.helpers({
getCombinedMessagesForChat() {
let deleted, i, j, len, msgs;
msgs = getFormattedMessagesForChat();
len = msgs != null ? msgs.length : void 0; // get length of messages
i = 0;
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') { // skip system messages
j = i + 1; // Start looking at messages right after the current one
while(j < len) {
deleted = false;
if(msgs[j].message.from_userid !== 'System') { // Ignore system messages
// Check if the time discrepancy between the two messages exceeds window for grouping
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) { // Both messages are from the same user
// insert a '\r' carriage return character between messages to put them on a new line
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;
} else {
break; // Messages are from different people, move on
}
} else {
break; // This is the break point in the chat, don't merge
}
len = msgs.length;
if(!deleted) {
++j;
}
}
}
++i;
len = msgs.length;
}
return msgs;
},
userExists() {
if(getInSession('inChatWith') === "PUBLIC_CHAT") {
return true;
} else {
return Meteor.Users.findOne({
userId: getInSession('inChatWith')
}) != null;
}
}
});
// When chatbar gets rendered, launch the auto-check for unread chat
Template.chatbar.rendered = function() {
return detectUnreadChat();
};
// When "< Public" is clicked, go to public chat
Template.chatbar.events({
'click .toPublic'(event) {
setInSession('inChatWith', 'PUBLIC_CHAT');
return setInSession('chats', getInSession('chats').map(chat => {
if(chat.userId === "PUBLIC_CHAT") {
chat.gotMail = false;
chat.number = 0;
}
return chat;
}));
}
});
Template.privateChatTab.rendered = function() {
if(isLandscape() || isPortrait()) {
return $("#newMessageInput").focus();
}
};
// When message gets rendered, scroll to the bottom
Template.message.rendered = function() {
let ref;
$('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
return false;
};
Template.chatInput.rendered = function() {
return $('.panel-footer').resizable({
handles: 'n',
minHeight: 70,
resize(event, ui) {
let ref;
if($('.panel-footer').css('top') === '0px') {
$('.panel-footer').height(70); // prevents the element from shrinking vertically for 1-2 px
} else {
$('.panel-footer').css('top', `${parseInt($('.panel-footer').css('top'))}${1}px`);
}
$('#chatbody').height($('#chat').height() - $('.panel-footer').height() - 45);
return $('#chatbody').scrollTop((ref = $('#chatbody')[0]) != null ? ref.scrollHeight : void 0);
},
start(event, ui) {
$('#newMessageInput').css('overflow', '');
return $('.panel-footer').resizable('option', 'maxHeight', Math.max($('.panel-footer').height(), $('#chat').height() / 2));
},
stop(event, ui) {
return setInSession('chatInputMinHeight', $('.panel-footer').height() + 1);
}
});
};
Template.chatInput.events({
'click #sendMessageButton'(event) {
$('#sendMessageButton').blur();
sendMessage();
return adjustChatInputHeight();
},
'keypress #newMessageInput'(event) { // user pressed a button inside the chatbox
let key;
key = event.charCode ? event.charCode : (event.keyCode ? event.keyCode : 0);
if(event.shiftKey && (key === 13)) {
event.preventDefault();
// 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;
}
if(key === 13) { // Check for pressing enter to submit message
event.preventDefault();
sendMessage();
$('#newMessageInput').val("");
return false;
}
}
});
Template.chatInputControls.rendered = function() {
return $('#newMessageInput').on('keydown paste cut', () => {
return setTimeout(() => {
return adjustChatInputHeight();
}, 0);
});
};
Template.message.helpers({
sanitizeAndFormat(str) {
let res;
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 = toClickable(res);
return res = activateBreakLines(res);
}
},
toClockTime(epochTime) {
let dateObj, hours, local, minutes, offset;
if(epochTime === null) {
return "";
}
local = new Date();
offset = local.getTimezoneOffset();
epochTime = epochTime - offset * 60000; // 1 min = 60 s = 60,000 ms
dateObj = new Date(epochTime);
hours = dateObj.getUTCHours();
minutes = dateObj.getUTCMinutes();
if(minutes < 10) {
minutes = `0${minutes}`;
}
return `${hours}:${minutes}`;
}
});
// make links received from Flash client clickable in HTML
this.toClickable = function(str) {
let res;
if(typeof str === 'string') {
res = str.replace(/<a href='event:/gim, "<a target='_blank' href='");
return res = res.replace(/<a href="event:/gim, '<a target="_blank" href="');
}
};

View File

@ -1,49 +0,0 @@
Template.settingsModal.helpers
getBBBSettingsInfo: ->
info = getBuildInformation()
result = "(c) #{info.copyrightYear} BigBlueButton Inc. [build #{info.html5ClientBuild}] - For more information visit #{info.link}"
Template.logoutModal.events
"click #yes": -> userLogout(getInSession("meetingId"), getInSession("userId"))
"click #no": -> $("#logoutModal").foundation('reveal', 'close')
"click .logoutButton": -> $(".tooltip").hide()
Template.settingsAudio.events
"click #exitAudio": -> exitVoiceCall()
"click .joinAudioButton": (event) -> $("#settingsModal").foundation('reveal', 'close')
"click #joinListenOnly": (event) -> joinVoiceCall @, isListenOnly: true
"click #joinMicrophone": (event) -> joinVoiceCall @, isListenOnly: false
Template.settingsModal.events
"click #closeSettings": -> $("#settingsModal").foundation('reveal', 'close');
Template.optionsFontSize.events
"click #decreaseFontSize": (event) ->
if getInSession("messageFontSize") is 8 # min
$('#decreaseFontSize').disabled = true
$('#decreaseFontSize').removeClass('icon fi-minus')
$('#decreaseFontSize').html('MIN')
else
setInSession "messageFontSize", getInSession("messageFontSize") - 2
adjustChatInputHeight()
setTimeout(scrollChatDown, 0)
if $('#increaseFontSize').html() is 'MAX'
$('#increaseFontSize').html('')
$('#increaseFontSize').addClass('icon fi-plus')
"click #increaseFontSize": (event) ->
if getInSession("messageFontSize") is 40 # max
$('#increaseFontSize').disabled = true
$('#increaseFontSize').removeClass('icon fi-plus')
$('#increaseFontSize').html('MAX')
else
setInSession "messageFontSize", getInSession("messageFontSize") + 2
adjustChatInputHeight()
setTimeout(scrollChatDown, 0)
if $('#decreaseFontSize').html() is 'MIN'
$('#decreaseFontSize').html('')
$('#decreaseFontSize').addClass('icon fi-minus')

View File

@ -0,0 +1,77 @@
Template.settingsModal.helpers({
getBBBSettingsInfo() {
let info, result;
info = getBuildInformation();
return result = `(c) ${info.copyrightYear} BigBlueButton Inc. [build ${info.html5ClientBuild}] - For more information visit ${info.link}`;
}
});
Template.logoutModal.events({
"click #yes"() {
return userLogout(getInSession("meetingId"), getInSession("userId"));
},
"click #no"() {
return $("#logoutModal").foundation('reveal', 'close');
},
"click .logoutButton"() {
return $(".tooltip").hide();
}
});
Template.settingsAudio.events({
"click #exitAudio"() {
return exitVoiceCall();
},
"click .joinAudioButton"(event) {
return $("#settingsModal").foundation('reveal', 'close');
},
"click #joinListenOnly"(event) {
return joinVoiceCall(this, {
isListenOnly: true
});
},
"click #joinMicrophone"(event) {
return joinVoiceCall(this, {
isListenOnly: false
});
}
});
Template.settingsModal.events({
"click #closeSettings"() {
return $("#settingsModal").foundation('reveal', 'close');
}
});
Template.optionsFontSize.events({
"click #decreaseFontSize"(event) {
if(getInSession("messageFontSize") === 8) { // min
$('#decreaseFontSize').disabled = true;
$('#decreaseFontSize').removeClass('icon fi-minus');
return $('#decreaseFontSize').html('MIN');
} else {
setInSession("messageFontSize", getInSession("messageFontSize") - 2);
adjustChatInputHeight();
setTimeout(scrollChatDown, 0);
if($('#increaseFontSize').html() === 'MAX') {
$('#increaseFontSize').html('');
return $('#increaseFontSize').addClass('icon fi-plus');
}
}
},
"click #increaseFontSize"(event) {
if(getInSession("messageFontSize") === 40) { // max
$('#increaseFontSize').disabled = true;
$('#increaseFontSize').removeClass('icon fi-plus');
return $('#increaseFontSize').html('MAX');
} else {
setInSession("messageFontSize", getInSession("messageFontSize") + 2);
adjustChatInputHeight();
setTimeout(scrollChatDown, 0);
if($('#decreaseFontSize').html() === 'MIN') {
$('#decreaseFontSize').html('');
return $('#decreaseFontSize').addClass('icon fi-minus');
}
}
}
});

View File

@ -1,33 +0,0 @@
Template.makeButton.helpers
hasGotUnreadMail: (userId) ->
chats = getInSession('chats')
if chats isnt undefined
if userId is "all_chats"
for tabs in chats
if tabs.gotMail is true
return true
else if userId is "PUBLIC_CHAT"
for tabs in chats
if tabs.userId is userId and tabs.gotMail is true
return true
return false
getNumberOfUnreadMessages: (userId) ->
if userId is "all_chats"
return
else
chats = getInSession('chats')
if chats isnt undefined
for chat in chats
if chat.userId is userId and chat.gotMail
if chat.number > 9
return "9+"
else
return chat.number
return
getNotificationClass: (userId) ->
if userId is "all_chats"
return "unreadChat"
if userId is "PUBLIC_CHAT"
return "unreadChatNumber"

View File

@ -0,0 +1,52 @@
Template.makeButton.helpers({
hasGotUnreadMail(userId) {
let chats, i, j, len, len1, tabs;
chats = getInSession('chats');
if(chats !== void 0) {
if(userId === "all_chats") {
for(i = 0, len = chats.length; i < len; i++) {
tabs = chats[i];
if(tabs.gotMail === true) {
return true;
}
}
} else if(userId === "PUBLIC_CHAT") {
for(j = 0, len1 = chats.length; j < len1; j++) {
tabs = chats[j];
if(tabs.userId === userId && tabs.gotMail === true) {
return true;
}
}
}
}
return false;
},
getNumberOfUnreadMessages(userId) {
let chat, chats, i, len;
if(userId === "all_chats") {
return;
} else {
chats = getInSession('chats');
if(chats !== void 0) {
for(i = 0, len = chats.length; i < len; i++) {
chat = chats[i];
if(chat.userId === userId && chat.gotMail) {
if(chat.number > 9) {
return "9+";
} else {
return chat.number;
}
}
}
}
}
},
getNotificationClass(userId) {
if(userId === "all_chats") {
return "unreadChat";
}
if(userId === "PUBLIC_CHAT") {
return "unreadChatNumber";
}
}
});

View File

@ -1,81 +0,0 @@
Template.displayUserIcons.events
'click .muteIcon': (event) ->
toggleMic @
'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
BBB.lowerHand(getInSession("meetingId"), @userId, getInSession("userId"), getInSession("authToken"))
'click .kickUser': (event) ->
kickUser BBB.getMeetingId(), @.userId, getInSession("userId"), getInSession("authToken")
Template.displayUserIcons.helpers
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
locked = BBB.getUser(userId)?.user.locked
settings = Meteor.Meetings.findOne()?.roomLockSettings
lockInAction = settings.disablePrivateChat or
settings.disableCam or
settings.disableMic or
settings.lockedLayout or
settings.disablePublicChat
return locked and lockInAction
# Opens a private chat tab when a username from the userlist is clicked
Template.usernameEntry.events
'click .usernameEntry': (event) ->
userIdSelected = @.userId
unless userIdSelected is null
if userIdSelected is BBB.getCurrentUser()?.userId
setInSession "inChatWith", "PUBLIC_CHAT"
else
setInSession "inChatWith", userIdSelected
if isPortrait() or isPortraitMobile()
toggleUserlistMenu()
toggleShield()
setTimeout () -> # waits until the end of execution queue
$("#newMessageInput").focus()
, 0
'click .gotUnreadMail': (event) ->
_this = @
currentId = getInSession('userId')
if currentId isnt undefined and currentId is _this.userId
_id = "PUBLIC_CHAT"
else
_id = _this.userId
chats = getInSession('chats')
if chats isnt undefined
for chat in chats
if chat.userId is _id
chat.gotMail = false
chat.number = 0
break
setInSession 'chats', chats
'click .setPresenter': (event) ->
setUserPresenter BBB.getMeetingId(), @.userId, getInSession('userId'), @.user.name, getInSession('authToken')
Template.usernameEntry.helpers
hasGotUnreadMailClass: (userId) ->
chats = getInSession('chats')
if chats isnt undefined
for chat in chats
if chat.userId is userId and chat.gotMail
return true
return false
getNumberOfUnreadMessages: (userId) ->
chats = getInSession('chats')
if chats isnt undefined
for chat in chats
if chat.userId is userId and chat.gotMail
if chat.number > 9
return "9+"
else
return chat.number
return

View File

@ -0,0 +1,106 @@
Template.displayUserIcons.events({
'click .muteIcon'(event) {
return toggleMic(this);
},
'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"));
},
'click .kickUser'(event) {
return kickUser(BBB.getMeetingId(), this.userId, getInSession("userId"), getInSession("authToken"));
}
});
Template.displayUserIcons.helpers({
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;
locked = (ref = BBB.getUser(userId)) != null ? ref.user.locked : void 0;
settings = (ref1 = Meteor.Meetings.findOne()) != null ? ref1.roomLockSettings : void 0;
lockInAction = settings.disablePrivateChat || settings.disableCam || settings.disableMic || settings.lockedLayout || settings.disablePublicChat;
return locked && lockInAction;
}
});
// Opens a private chat tab when a username from the userlist is clicked
Template.usernameEntry.events({
'click .usernameEntry'(event) {
let ref, userIdSelected;
userIdSelected = this.userId;
if(userIdSelected !== null) {
if(userIdSelected === ((ref = BBB.getCurrentUser()) != null ? ref.userId : void 0)) {
setInSession("inChatWith", "PUBLIC_CHAT");
} else {
setInSession("inChatWith", userIdSelected);
}
}
if(isPortrait() || isPortraitMobile()) {
toggleUserlistMenu();
toggleShield();
}
return setTimeout(() => { // waits until the end of execution queue
return $("#newMessageInput").focus();
}, 0);
},
'click .gotUnreadMail'(event) {
let _id, _this, chat, chats, currentId, i, len;
_this = this;
currentId = getInSession('userId');
if(currentId !== void 0 && currentId === _this.userId) {
_id = "PUBLIC_CHAT";
} else {
_id = _this.userId;
}
chats = getInSession('chats');
if(chats !== void 0) {
for(i = 0, len = chats.length; i < len; i++) {
chat = chats[i];
if(chat.userId === _id) {
chat.gotMail = false;
chat.number = 0;
break;
}
}
return setInSession('chats', chats);
}
},
'click .setPresenter'(event) {
return setUserPresenter(BBB.getMeetingId(), this.userId, getInSession('userId'), this.user.name, getInSession('authToken'));
}
});
Template.usernameEntry.helpers({
hasGotUnreadMailClass(userId) {
let chat, chats, i, len;
chats = getInSession('chats');
if(chats !== void 0) {
for(i = 0, len = chats.length; i < len; i++) {
chat = chats[i];
if(chat.userId === userId && chat.gotMail) {
return true;
}
}
}
return false;
},
getNumberOfUnreadMessages(userId) {
let chat, chats, i, len;
chats = getInSession('chats');
if(chats !== void 0) {
for(i = 0, len = chats.length; i < len; i++) {
chat = chats[i];
if(chat.userId === userId && chat.gotMail) {
if(chat.number > 9) {
return "9+";
} else {
return chat.number;
}
}
}
}
}
});

View File

@ -1,22 +0,0 @@
Template.usersList.helpers
getInfoNumberOfUsers: ->
numberUsers = BBB.getNumberOfUsers()
if numberUsers > 8
return "Users: #{numberUsers}"
# do not display the label if there are just a few users
Template.usersList.rendered = ->
$('.userlistMenu').resizable
handles: 'e'
maxWidth: 600
minWidth: 200
resize: () ->
adjustChatInputHeight()
Tracker.autorun (comp) ->
setInSession 'userListRenderedTime', TimeSync.serverTime()
if getInSession('userListRenderedTime') isnt undefined
comp.stop()
if isPhone()
$('.userlistMenu').addClass('hiddenInLandscape')

View File

@ -0,0 +1,30 @@
Template.usersList.helpers({
getInfoNumberOfUsers() {
let numberUsers;
numberUsers = BBB.getNumberOfUsers();
if (numberUsers > 8) {
return `Users: ${numberUsers}`;
}
// do not display the label if there are just a few users
}
});
Template.usersList.rendered = function() {
$('.userlistMenu').resizable({
handles: 'e',
maxWidth: 600,
minWidth: 200,
resize() {
return adjustChatInputHeight();
}
});
Tracker.autorun(comp => {
setInSession('userListRenderedTime', TimeSync.serverTime());
if (getInSession('userListRenderedTime') !== void 0) {
return comp.stop();
}
});
if (isPhone()) {
return $('.userlistMenu').addClass('hiddenInLandscape');
}
};

View File

@ -1,120 +0,0 @@
Template.slide.rendered = ->
reactOnSlideChange(@)
@reactOnSlideChange = =>
currentSlide = BBB.getCurrentSlide("slide.rendered")
pic = new Image()
pic.onload = ->
setInSession 'slideOriginalWidth', @width
setInSession 'slideOriginalHeight', @height
$(window).resize( ->
# 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
scaleWhiteboard()
)
if currentSlide?.slide?.img_uri?
createWhiteboardPaper (wpm) ->
displaySlide wpm
pic.src = currentSlide?.slide?.img_uri
return ""
@createWhiteboardPaper = (callback) =>
# console.log "CREATING WPM"
@whiteboardPaperModel = new Meteor.WhiteboardPaperModel('whiteboard-paper')
callback(@whiteboardPaperModel)
@displaySlide = (wpm) ->
currentSlide = BBB.getCurrentSlide("displaySlide")
wpm.create()
adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
wpm._displayPage(currentSlide?.slide?.img_uri, getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
manuallyDisplayShapes()
wpm.scale(adjustedDimensions.width, adjustedDimensions.height)
@manuallyDisplayShapes = ->
return if Meteor.WhiteboardCleanStatus.findOne({in_progress: true})?
currentSlide = BBB.getCurrentSlide("manuallyDisplayShapes")
wpm = @whiteboardPaperModel
shapes = Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id}).fetch()
for s in shapes
shapeInfo = s.shape?.shape or s?.shape
shapeType = shapeInfo?.type
if shapeType isnt "text"
len = shapeInfo.points.length
for num in [0..len] # the coordinates must be in the range 0 to 1
shapeInfo?.points[num] = shapeInfo?.points[num] / 100
wpm?.makeShape(shapeType, shapeInfo)
wpm?.updateShape(shapeType, shapeInfo)
# calculates and returns the best fitting {width, height} pair
# based on the image's original width and height
@scaleSlide = (originalWidth, originalHeight) ->
# set the size of the whiteboard space (frame) where the slide will be displayed
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()
boardHeight = $("#whiteboard-container").height()
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()
boardHeight = 1.4 * $("#whiteboard-container").width() # A4 paper size
# this is the best fitting pair
adjustedWidth = null
adjustedHeight = null
# the slide image is in portrait orientation
if originalWidth <= originalHeight
adjustedWidth = boardHeight * originalWidth / originalHeight
if boardWidth < adjustedWidth
adjustedHeight = boardHeight * boardWidth / adjustedWidth
adjustedWidth = boardWidth
else
adjustedHeight = boardHeight
# ths slide image is in landscape orientation
else
adjustedHeight = boardWidth * originalHeight / originalWidth
if boardHeight < adjustedHeight
adjustedWidth = boardWidth * boardHeight / adjustedHeight
adjustedHeight = boardHeight
else
adjustedWidth = boardWidth
{ width: adjustedWidth, height: adjustedHeight, boardWidth: boardWidth, boardHeight: boardHeight }
Template.slide.helpers
updatePointerLocation: (pointer) ->
whiteboardPaperModel?.moveCursor(pointer.x, pointer.y)
#### SHAPE ####
Template.shape.rendered = ->
# @data is the shape object coming from the {{#each}} in the html file
shapeInfo = @data.shape?.shape or @data.shape
shapeType = shapeInfo?.type
if shapeType isnt "text"
len = shapeInfo.points.length
for num in [0..len] # the coordinates must be in the range 0 to 1
shapeInfo.points[num] = shapeInfo.points[num] / 100
if whiteboardPaperModel?
wpm = whiteboardPaperModel
wpm?.makeShape(shapeType, shapeInfo)
wpm?.updateShape(shapeType, shapeInfo)
Template.shape.destroyed = ->
if whiteboardPaperModel?
wpm = whiteboardPaperModel
wpm.clearShapes()
manuallyDisplayShapes()

View File

@ -0,0 +1,171 @@
Template.slide.rendered = function() {
return reactOnSlideChange(this);
};
this.reactOnSlideChange = (_this => {
return function() {
let currentSlide, pic, ref;
currentSlide = BBB.getCurrentSlide("slide.rendered");
pic = new Image();
pic.onload = function() {
let ref;
setInSession('slideOriginalWidth', this.width);
setInSession('slideOriginalHeight', this.height);
$(window).resize(() => {
// 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();
}
});
if((currentSlide != null ? (ref = currentSlide.slide) != null ? ref.img_uri : void 0 : void 0) != null) {
return createWhiteboardPaper(wpm => {
return displaySlide(wpm);
});
}
};
pic.src = currentSlide != null ? (ref = currentSlide.slide) != null ? ref.img_uri : void 0 : void 0;
return "";
};
})(this);
this.createWhiteboardPaper = (_this => {
// console.log "CREATING WPM"
return function(callback) {
_this.whiteboardPaperModel = new Meteor.WhiteboardPaperModel('whiteboard-paper');
return callback(_this.whiteboardPaperModel);
};
})(this);
this.displaySlide = function(wpm) {
let adjustedDimensions, currentSlide, ref;
currentSlide = BBB.getCurrentSlide("displaySlide");
wpm.create();
adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'));
wpm._displayPage(
currentSlide != null ? (ref = currentSlide.slide) != null ? ref.img_uri : void 0 : void 0,
getInSession('slideOriginalWidth'),
getInSession('slideOriginalHeight')
);
manuallyDisplayShapes();
return wpm.scale(adjustedDimensions.width, adjustedDimensions.height);
};
this.manuallyDisplayShapes = function() {
let currentSlide, i, j, len, len1, num, ref, ref1, ref2, results, s, shapeInfo, shapeType, shapes, wpm;
if(Meteor.WhiteboardCleanStatus.findOne({
in_progress: true
}) != null) {
return;
}
currentSlide = BBB.getCurrentSlide("manuallyDisplayShapes");
wpm = this.whiteboardPaperModel;
shapes = Meteor.Shapes.find({
whiteboardId: currentSlide != null ? (ref = currentSlide.slide) != null ? ref.id : void 0 : void 0
}).fetch();
results = [];
for(i = 0, len1 = shapes.length; i < len1; i++) {
s = shapes[i];
shapeInfo = ((ref1 = s.shape) != null ? ref1.shape : void 0) || (s != null ? s.shape : void 0);
shapeType = shapeInfo != null ? shapeInfo.type : void 0;
if(shapeType !== "text") {
len = shapeInfo.points.length;
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) {
shapeInfo.points[num] = (shapeInfo != null ? shapeInfo.points[num] : void 0) / 100;
}
}
}
if(wpm != null) {
wpm.makeShape(shapeType, shapeInfo);
}
results.push(wpm != null ? wpm.updateShape(shapeType, shapeInfo) : void 0);
}
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) {
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) {
// 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();
boardHeight = $("#whiteboard-container").height();
} 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();
boardHeight = 1.4 * $("#whiteboard-container").width();
}
// this is the best fitting pair
adjustedWidth = null;
adjustedHeight = null;
// the slide image is in portrait orientation
if(originalWidth <= originalHeight) {
adjustedWidth = boardHeight * originalWidth / originalHeight;
if (boardWidth < adjustedWidth) {
adjustedHeight = boardHeight * boardWidth / adjustedWidth;
adjustedWidth = boardWidth;
} else {
adjustedHeight = boardHeight;
}
// ths slide image is in landscape orientation
} else {
adjustedHeight = boardWidth * originalHeight / originalWidth;
if (boardHeight < adjustedHeight) {
adjustedWidth = boardWidth * boardHeight / adjustedHeight;
adjustedHeight = boardHeight;
} else {
adjustedWidth = boardWidth;
}
}
return {
width: adjustedWidth,
height: adjustedHeight,
boardWidth: boardWidth,
boardHeight: boardHeight
};
};
Template.slide.helpers({
updatePointerLocation(pointer) {
return typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null ? whiteboardPaperModel.moveCursor(pointer.x, pointer.y) : void 0;
}
});
//// SHAPE ////
Template.shape.rendered = function() {
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;
shapeType = shapeInfo != null ? shapeInfo.type : void 0;
if(shapeType !== "text") {
len = shapeInfo.points.length;
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;
}
}
if(typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null) {
wpm = whiteboardPaperModel;
if(wpm != null) {
wpm.makeShape(shapeType, shapeInfo);
}
return wpm != null ? wpm.updateShape(shapeType, shapeInfo) : void 0;
}
};
Template.shape.destroyed = function() {
let wpm;
if(typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null) {
wpm = whiteboardPaperModel;
wpm.clearShapes();
return manuallyDisplayShapes();
}
};

View File

@ -1,217 +0,0 @@
# scale the whiteboard to adapt to the resized window
@scaleWhiteboard = (callback) ->
adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
if whiteboardPaperModel?
whiteboardPaperModel.scale(adjustedDimensions.width, adjustedDimensions.height)
if callback
callback()
isPollStarted: ->
if BBB.isPollGoing(getInSession('userId'))
return true
else
return false
hasNoPresentation: ->
Meteor.Presentations.findOne({'presentation.current':true})
forceSlideShow: ->
reactOnSlideChange()
clearSlide: ->
#clear the slide
whiteboardPaperModel?.removeAllImagesFromPaper()
# hide the cursor
whiteboardPaperModel?.cursor?.remove()
Template.whiteboard.events
'click .whiteboardFullscreenButton': (event, template) ->
enterWhiteboardFullscreen()
'click .exitFullscreenButton': (event, template) ->
if document.exitFullscreen
document.exitFullscreen()
else if document.mozCancelFullScreen
document.mozCancelFullScreen()
else if document.webkitExitFullscreen
document.webkitExitFullscreen()
'click .sadEmojiButton.inactiveEmojiButton': (event) ->
if $('.sadEmojiButton').css('opacity') is '1'
BBB.setEmojiStatus(BBB.getMeetingId(), getInSession('userId'), getInSession('userId'), getInSession('authToken'), "sad")
$('.FABTriggerButton').blur()
toggleEmojisFAB()
'click .happyEmojiButton.inactiveEmojiButton': (event) ->
if $('.happyEmojiButton').css('opacity') is '1'
BBB.setEmojiStatus(BBB.getMeetingId(), getInSession('userId'), getInSession('userId'), getInSession('authToken'), "happy")
$('.FABTriggerButton').blur()
toggleEmojisFAB()
'click .confusedEmojiButton.inactiveEmojiButton': (event) ->
if $('.confusedEmojiButton').css('opacity') is '1'
BBB.setEmojiStatus(BBB.getMeetingId(), getInSession('userId'), getInSession('userId'), getInSession('authToken'), "confused")
$('.FABTriggerButton').blur()
toggleEmojisFAB()
'click .neutralEmojiButton.inactiveEmojiButton': (event) ->
if $('.neutralEmojiButton').css('opacity') is '1'
BBB.setEmojiStatus(BBB.getMeetingId(), getInSession('userId'), getInSession('userId'), getInSession('authToken'), "neutral")
$('.FABTriggerButton').blur()
toggleEmojisFAB()
'click .awayEmojiButton.inactiveEmojiButton': (event) ->
if $('.awayEmojiButton').css('opacity') is '1'
BBB.setEmojiStatus(BBB.getMeetingId(), getInSession('userId'), getInSession('userId'), getInSession('authToken'), "away")
$('.FABTriggerButton').blur()
toggleEmojisFAB()
'click .raiseHandEmojiButton.inactiveEmojiButton': (event) ->
if $('.raiseHandEmojiButton').css('opacity') is '1'
BBB.setEmojiStatus(BBB.getMeetingId(), getInSession('userId'), getInSession('userId'), getInSession('authToken'), "raiseHand")
$('.FABTriggerButton').blur()
toggleEmojisFAB()
'click .activeEmojiButton': (event) ->
if $('.activeEmojiButton').css('opacity') is '1'
BBB.setEmojiStatus(BBB.getMeetingId(), getInSession('userId'), getInSession('userId'), getInSession('authToken'), "none")
$('.FABTriggerButton').blur()
toggleEmojisFAB()
'click .FABTriggerButton': (event) ->
$('.FABTriggerButton').blur()
toggleEmojisFAB()
Template.whiteboardControls.helpers
presentationProgress: ->
currentPresentation = Meteor.Presentations.findOne({'presentation.current':true})
currentSlideNum = Meteor.Slides.findOne({'presentationId': currentPresentation?.presentation.id, 'slide.current':true})?.slide.num
totalSlideNum = Meteor.Slides.find({'presentationId': currentPresentation?.presentation.id})?.count()
console.log('slide', currentSlideNum)
if currentSlideNum isnt undefined
return "#{currentSlideNum}/#{totalSlideNum}"
else
return ''
Template.whiteboardControls.events
'click .whiteboard-buttons-slide > .prev':(event) ->
BBB.goToPreviousPage()
'click .whiteboard-buttons-slide > .next':(event) ->
BBB.goToNextPage()
'click .switchSlideButton': (event) ->
$('.tooltip').hide()
Template.polling.events
'click .pollButtons': (event) ->
_key = @.label
_id = @.answer
BBB.sendPollResponseMessage(_key, _id)
Template.polling.rendered = ->
scaleWhiteboard()
Template.polling.destroyed = ->
setTimeout(scaleWhiteboard, 0)
Template.whiteboardControls.rendered = ->
scaleWhiteboard()
Template.whiteboardControls.destroyed = ->
setTimeout(scaleWhiteboard, 0)
Template.whiteboard.rendered = ->
$('#whiteboard').resizable
handles: 'e'
minWidth: 150
resize: () ->
adjustChatInputHeight()
start: () ->
if $('#chat').width() / $('#panels').width() > 0.2 # chat shrinking can't make it smaller than one fifth of the whiteboard-chat area
$('#whiteboard').resizable('option', 'maxWidth', $('#panels').width() - 200) # gives the chat enough space (200px)
else
$('#whiteboard').resizable('option', 'maxWidth', $('#whiteboard').width())
stop: () ->
$('#whiteboard').css('width', 100 * $('#whiteboard').width() / $('#panels').width() + '%') # transforms width to %
$('#whiteboard').resizable('option', 'maxWidth', null)
# whiteboard element needs to be available
Meteor.NotificationControl = new NotificationControl('notificationArea')
$(document).foundation() # initialize foundation javascript
Template.presenterUploaderControl.created = ->
this.isOpen = new ReactiveVar(false);
this.files = new ReactiveList({
sort: (a, b) ->
# Put the ones who still uploading first
(a.isUploading == b.isUploading) ? 0 : a.isUploading ? -1 : 1;
});
this.presentations = Meteor.Presentations.find({},
{
sort: {'presentation.current': -1, 'presentation.name': 1}
fields: {'presentation': 1}
});
fakeUpload = (file, list) ->
setTimeout (->
file.uploadedSize = file.uploadedSize + (Math.floor(Math.random() * file.size + file.uploadedSize)/10)
unless file.size > file.uploadedSize
file.uploadedSize = file.size
file.isUploading = false
list.update(file.name, file)
if file.isUploading == true
fakeUpload(file, list)
else
# TODO: Here we should remove and update te presentation on mongo
list.remove(file.name)
), 200
Template.presenterUploaderControl.events
'click .js-open':(event, template) ->
template.isOpen.set(true);
'click .js-close':(event, template) ->
template.isOpen.set(false);
'dragover [data-dropzone]':(e) ->
e.preventDefault()
$(e.currentTarget).addClass('hover')
'dragleave [data-dropzone]':(e) ->
e.preventDefault()
$(e.currentTarget).removeClass('hover')
'drop [data-dropzone], change [data-dropzone] > input[type="file"]':(e, template) ->
e.preventDefault();
files = (e.originalEvent.dataTransfer || e.originalEvent.target).files
_.each(files, (file) ->
file.isUploading = true;
file.uploadedSize = 0;
file.percUploaded = 0;
template.files.insert(file.name, file)
fakeUpload(file, template.files)
)
Template.presenterUploaderControl.helpers
isOpen: -> Template.instance().isOpen.get()
files: -> Template.instance().files.fetch()
presentations: -> Template.instance().presentations.fetch().map((x) -> x.presentation)
Template.presenterUploaderControlFileListItem.helpers
percUploaded: -> Math.round((this.uploadedSize / this.size) * 100)
Template.presenterUploaderControlPresentationListItem.events
'click [data-action-show]':(event, template) ->
console.info('Should show the file `' + this.name + '`');
'click [data-action-delete]':(event, template) ->
console.info('Should delete the file `' + this.name + '`');

View File

@ -0,0 +1,334 @@
let fakeUpload;
// scale the whiteboard to adapt to the resized window
this.scaleWhiteboard = function(callback) {
let adjustedDimensions;
adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'));
if(typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null) {
whiteboardPaperModel.scale(adjustedDimensions.width, adjustedDimensions.height);
}
if(callback) {
callback();
}
return {
isPollStarted() {
if(BBB.isPollGoing(getInSession('userId'))) {
return true;
} else {
return false;
}
},
hasNoPresentation() {
return Meteor.Presentations.findOne({
'presentation.current': true
});
},
forceSlideShow() {
return reactOnSlideChange();
},
clearSlide() {
let ref;
//clear the slide
if(typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null) {
whiteboardPaperModel.removeAllImagesFromPaper();
}
// hide the cursor
return typeof whiteboardPaperModel !== "undefined" && whiteboardPaperModel !== null ? (ref = whiteboardPaperModel.cursor) != null ? ref.remove() : void 0 : void 0;
}
};
};
Template.whiteboard.events({
'click .whiteboardFullscreenButton'(event, template) {
return enterWhiteboardFullscreen();
},
'click .exitFullscreenButton'(event, template) {
if(document.exitFullscreen) {
return document.exitFullscreen();
} else if(document.mozCancelFullScreen) {
return document.mozCancelFullScreen();
} else if(document.webkitExitFullscreen) {
return document.webkitExitFullscreen();
}
},
'click .sadEmojiButton.inactiveEmojiButton'(event) {
if($('.sadEmojiButton').css('opacity') === '1') {
BBB.setEmojiStatus(
BBB.getMeetingId(),
getInSession('userId'),
getInSession('userId'),
getInSession('authToken'),
"sad"
);
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
},
'click .happyEmojiButton.inactiveEmojiButton'(event) {
if($('.happyEmojiButton').css('opacity') === '1') {
BBB.setEmojiStatus(
BBB.getMeetingId(),
getInSession('userId'),
getInSession('userId'),
getInSession('authToken'),
"happy"
);
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
},
'click .confusedEmojiButton.inactiveEmojiButton'(event) {
if($('.confusedEmojiButton').css('opacity') === '1') {
BBB.setEmojiStatus(
BBB.getMeetingId(),
getInSession('userId'),
getInSession('userId'),
getInSession('authToken'),
"confused"
);
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
},
'click .neutralEmojiButton.inactiveEmojiButton'(event) {
if($('.neutralEmojiButton').css('opacity') === '1') {
BBB.setEmojiStatus(
BBB.getMeetingId(),
getInSession('userId'),
getInSession('userId'),
getInSession('authToken'),
"neutral"
);
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
},
'click .awayEmojiButton.inactiveEmojiButton'(event) {
if($('.awayEmojiButton').css('opacity') === '1') {
BBB.setEmojiStatus(
BBB.getMeetingId(),
getInSession('userId'),
getInSession('userId'),
getInSession('authToken'),
"away"
);
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
},
'click .raiseHandEmojiButton.inactiveEmojiButton'(event) {
if($('.raiseHandEmojiButton').css('opacity') === '1') {
BBB.setEmojiStatus(
BBB.getMeetingId(),
getInSession('userId'),
getInSession('userId'),
getInSession('authToken'),
"raiseHand"
);
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
},
'click .activeEmojiButton'(event) {
if($('.activeEmojiButton').css('opacity') === '1') {
BBB.setEmojiStatus(
BBB.getMeetingId(),
getInSession('userId'),
getInSession('userId'),
getInSession('authToken'),
"none"
);
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
},
'click .FABTriggerButton'(event) {
$('.FABTriggerButton').blur();
return toggleEmojisFAB();
}
});
Template.whiteboardControls.helpers({
presentationProgress() {
let currentPresentation, currentSlideNum, ref, ref1, totalSlideNum;
currentPresentation = Meteor.Presentations.findOne({
'presentation.current': true
});
currentSlideNum = (ref = Meteor.Slides.findOne({
'presentationId': currentPresentation != null ? currentPresentation.presentation.id : void 0,
'slide.current': true
})) != null ? ref.slide.num : void 0;
totalSlideNum = (ref1 = Meteor.Slides.find({
'presentationId': currentPresentation != null ? currentPresentation.presentation.id : void 0
})) != null ? ref1.count() : void 0;
console.log('slide', currentSlideNum);
if(currentSlideNum !== void 0) {
return `${currentSlideNum}/${totalSlideNum}`;
} else {
return '';
}
}
});
Template.whiteboardControls.events({
'click .whiteboard-buttons-slide > .prev'(event) {
return BBB.goToPreviousPage();
},
'click .whiteboard-buttons-slide > .next'(event) {
return BBB.goToNextPage();
},
'click .switchSlideButton'(event) {
return $('.tooltip').hide();
}
});
Template.polling.events({
'click .pollButtons'(event) {
let _id, _key;
_key = this.label;
_id = this.answer;
return BBB.sendPollResponseMessage(_key, _id);
}
});
Template.polling.rendered = function() {
return scaleWhiteboard();
};
Template.polling.destroyed = function() {
return setTimeout(scaleWhiteboard, 0);
};
Template.whiteboardControls.rendered = function() {
return scaleWhiteboard();
};
Template.whiteboardControls.destroyed = function() {
return setTimeout(scaleWhiteboard, 0);
};
Template.whiteboard.rendered = function() {
$('#whiteboard').resizable({
handles: 'e',
minWidth: 150,
resize() {
return adjustChatInputHeight();
},
start() {
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); // gives the chat enough space (200px)
} else {
return $('#whiteboard').resizable('option', 'maxWidth', $('#whiteboard').width());
}
},
stop() {
$('#whiteboard').css('width', `${100 * $('#whiteboard').width() / $('#panels').width()}%`); // transforms width to %
return $('#whiteboard').resizable('option', 'maxWidth', null);
}
});
// whiteboard element needs to be available
Meteor.NotificationControl = new NotificationControl('notificationArea');
return $(document).foundation(); // initialize foundation javascript
};
Template.presenterUploaderControl.created = function() {
this.isOpen = new ReactiveVar(false);
this.files = new ReactiveList({
sort(a, b) {
// Put the ones who still uploading first
let ref, ref1;
return (ref = a.isUploading === b.isUploading) != null ? ref : {
0: (ref1 = a.isUploading) != null ? ref1 : -{
1: 1
}
};
}
});
return this.presentations = Meteor.Presentations.find({}, {
sort: {
'presentation.current': -1,
'presentation.name': 1
},
fields: {
'presentation': 1
}
});
};
fakeUpload = function(file, list) {
return setTimeout((() => {
file.uploadedSize = file.uploadedSize + (Math.floor(Math.random() * file.size + file.uploadedSize) / 10);
if (!(file.size > file.uploadedSize)) {
file.uploadedSize = file.size;
file.isUploading = false;
}
list.update(file.name, file);
if(file.isUploading === true) {
return fakeUpload(file, list);
} else {
return list.remove(file.name); // TODO: Here we should remove and update te presentation on mongo
}
}), 200);
};
Template.presenterUploaderControl.events({
'click .js-open'(event, template) {
return template.isOpen.set(true);
},
'click .js-close'(event, template) {
return template.isOpen.set(false);
},
'dragover [data-dropzone]'(e) {
e.preventDefault();
return $(e.currentTarget).addClass('hover');
},
'dragleave [data-dropzone]'(e) {
e.preventDefault();
return $(e.currentTarget).removeClass('hover');
},
'drop [data-dropzone], change [data-dropzone] > input[type="file"]'(e, template) {
let files;
e.preventDefault();
files = (e.originalEvent.dataTransfer || e.originalEvent.target).files;
return _.each(files, file => {
file.isUploading = true;
file.uploadedSize = 0;
file.percUploaded = 0;
template.files.insert(file.name, file);
return fakeUpload(file, template.files);
});
}
});
Template.presenterUploaderControl.helpers({
isOpen() {
return Template.instance().isOpen.get();
},
files() {
return Template.instance().files.fetch();
},
presentations() {
return Template.instance().presentations.fetch().map(x => {
return x.presentation;
});
}
});
Template.presenterUploaderControlFileListItem.helpers({
percUploaded() {
return Math.round((this.uploadedSize / this.size) * 100);
}
});
Template.presenterUploaderControlPresentationListItem.events({
'click [data-action-show]'(event, template) {
return console.info('Should show the file `' + this.name + '`');
},
'click [data-action-delete]'(event, template) {
return console.info('Should delete the file `' + this.name + '`');
}
});

View File

@ -1,26 +0,0 @@
# A base class for whiteboard tools
class @WhiteboardToolModel
initialize: (@paper) ->
console.log "paper:" + @paper
@gh = 0
@gw = 0
@obj = 0
# the defintion of this shape, kept so we can redraw the shape whenever needed
@definition = []
#set the size of the paper
# @param {number} @gh gh parameter
# @param {number} @gw gw parameter
setPaperSize: (@gh, @gw) ->
setOffsets: (@xOffset, @yOffset) ->
setPaperDimensions: (@paperWidth, @paperHeight) ->
# TODO: can't we simply take the width and the height from `@paper`?
getDefinition: () ->
@definition
hide: () ->
@obj.hide() if @obj?

View File

@ -0,0 +1,47 @@
// A base class for whiteboard tools
this.WhiteboardToolModel = (function() {
class WhiteboardToolModel {
constructor() {}
initialize(paper) {
this.paper = paper;
console.log(`paper:${this.paper}`);
this.gh = 0;
this.gw = 0;
this.obj = 0;
// the defintion of this shape, kept so we can redraw the shape whenever needed
return this.definition = [];
}
//set the size of the paper
// @param {number} @gh gh parameter
// @param {number} @gw gw parameter
setPaperSize(gh, gw) {
this.gh = gh;
this.gw = gw;
}
setOffsets(xOffset, yOffset) {
this.xOffset = xOffset;
this.yOffset = yOffset;
}
setPaperDimensions(paperWidth, paperHeight) {
// TODO: can't we simply take the width and the height from `@paper`?
this.paperWidth = paperWidth;
this.paperHeight = paperHeight;
}
getDefinition() {
return this.definition;
}
hide() {
if(this.obj != null) {
return this.obj.hide();
}
}
}
return WhiteboardToolModel;
})();

View File

@ -1,38 +0,0 @@
# General utility 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) ->
method = method or "post"
form = $("<form></form>")
form.attr
"method" : method,
"action" : path
for key of params
if params.hasOwnProperty(key)
$hiddenField = $ "input"
$hiddenField.attr
"type" : "hidden",
"name" : key,
"value" : params[key]
form.append $hiddenField
$('body').append form
form.submit()
# thickness can be a number (e.g. "2") or a string (e.g. "2px")
@formatThickness = (thickness) ->
thickness ?= "1" # default value
if !thickness.toString().match(/.*px$/)
"#" + thickness + "px" # leading "#" - to be compatible with Firefox
thickness
# applies zooming to the stroke thickness
@zoomStroke = (thickness) ->
currentSlide = BBB.getCurrentSlide("zoomStroke")
ratio = (currentSlide?.slide.width_ratio + currentSlide?.slide.height_ratio) / 2
thickness * 100 / ratio

View File

@ -0,0 +1,49 @@
// General utility 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") {
let $hiddenField, form, key;
form = $("<form></form>");
form.attr({
"method": method,
"action": path
});
for(key in params) {
if(params.hasOwnProperty(key)) {
$hiddenField = $("input");
$hiddenField.attr({
"type": "hidden",
"name": key,
"value": params[key]
});
form.append($hiddenField);
}
}
$('body').append(form);
return form.submit();
}
});
// thickness can be a number (e.g. "2") or a string (e.g. "2px")
this.formatThickness = function(thickness) {
if(thickness == null) {
thickness = "1"; // default value
}
if(!thickness.toString().match(/.*px$/)) {
`#${thickness}px`; // leading "#" - to be compatible with Firefox
}
return thickness;
};
// applies zooming to the stroke thickness
this.zoomStroke = function(thickness) {
let currentSlide, ratio;
currentSlide = BBB.getCurrentSlide("zoomStroke");
ratio = ((currentSlide != null ? currentSlide.slide.width_ratio : void 0) + (currentSlide != null ? currentSlide.slide.height_ratio : void 0)) / 2;
return thickness * 100 / ratio;
};

View File

@ -1,43 +0,0 @@
# The cursor/pointer in the whiteboard
class @WhiteboardCursorModel
constructor: (@paper, @radius=null, @color=null) ->
@radius ?= 6
@color ?= "#ff6666" # a pinkish red
@cursor = null
@paper
draw: () =>
@cursor = @paper.circle(0, 0, @radius)
@cursor.attr
"fill": @color
"stroke": @color
"opacity": "0.8"
$(@cursor.node).on "mousewheel", _.bind(@_onMouseWheel, @)
toFront: () ->
@cursor.toFront() if @cursor?
setRadius: (value) ->
if @cursor?
@cursor.attr r: value
setPosition: (x, y) ->
if @cursor?
@cursor.attr
cx: x
cy: y
undrag: () ->
@cursor.undrag() if @cursor?
drag: (onMove, onStart, onEnd, target=null) ->
if @cursor?
target or= @
@cursor.drag _.bind(onMove, target), _.bind(onStart, target), _.bind(onEnd, target)
_onMouseWheel: (e, delta) ->
@trigger("cursor:mousewheel", e, delta)
remove: () ->
@cursor.remove()

View File

@ -0,0 +1,83 @@
const bind = function(fn, me) {
return function() {
return fn.apply(me, arguments);
};
};
// The cursor/pointer in the whiteboard
this.WhiteboardCursorModel = (function() {
class WhiteboardCursorModel {
constructor(paper, radius, color) {
this.paper = paper;
this.radius = radius != null ? radius : null;
this.color = color != null ? color : null;
this.draw = bind(this.draw, this);
if(this.radius == null) {
this.radius = 6;
}
if(this.color == null) {
this.color = "#ff6666"; // a pinkish red
}
this.cursor = null;
}
draw() {
this.cursor = this.paper.circle(0, 0, this.radius);
this.cursor.attr({
"fill": this.color,
"stroke": this.color,
"opacity": "0.8"
});
return $(this.cursor.node).on("mousewheel", _.bind(this._onMouseWheel, this));
}
toFront() {
if(this.cursor != null) {
return this.cursor.toFront();
}
}
setRadius(value) {
if(this.cursor != null) {
return this.cursor.attr({
r: value
});
}
}
setPosition(x, y) {
if(this.cursor != null) {
return this.cursor.attr({
cx: x,
cy: y
});
}
}
undrag() {
if(this.cursor != null) {
return this.cursor.undrag();
}
}
drag(onMove, onStart, onEnd, target) {
if(target == null) {
target = null;
}
if(this.cursor != null) {
target || (target = this);
return this.cursor.drag(_.bind(onMove, target), _.bind(onStart, target), _.bind(onEnd, target));
}
}
_onMouseWheel(e, delta) {
return this.trigger("cursor:mousewheel", e, delta);
}
remove() {
return this.cursor.remove();
}
}
return WhiteboardCursorModel;
})();

View File

@ -1,151 +0,0 @@
# An ellipse in the whiteboard
class @WhiteboardEllipseModel extends WhiteboardToolModel
constructor: (@paper) ->
super @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
@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) ->
#console.log "Whiteboard - Making ellipse: "
#console.log info
if info?.points?
x = info.points[0]
y = info.points[1]
color = info.color
thickness = info.thickness
@obj = @paper.ellipse(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0)
@obj.attr "stroke", formatColor(color)
@obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
@definition = [x, y, y, x, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@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) ->
#console.log info
if info?.points?
x1 = info.points[0]
y1 = info.points[1]
x2 = info.points[2]
y2 = info.points[3]
circle = info.square
if @obj?
[x1, x2] = [x2, x1] if x2 < x1
if y2 < y1
[y1, y2] = [y2, y1]
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 reversed # if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * @gw / @gh
else
y2 = y1 + (x2 - x1) * @gw / @gh
coords =
x1: x1
x2: x2
y1: y1
y2: y2
#console.log(coords)
rx = (x2 - x1) / 2
ry = (y2 - y1) / 2
r =
rx: rx * @gw
ry: ry * @gh
cx: (rx + x1) * @gw + @xOffset
cy: (ry + y1) * @gh + @yOffset
@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
@definition[0] = x1
@definition[1] = y1
@definition[2] = x2
@definition[3] = y2
# 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) ->
[x1, x2] = [x2, x1] if x2 < x1
[y1, y2] = [y2, y1] if y2 < y1
rx = (x2 - x1) / 2
ry = (y2 - y1) / 2
x = (rx + x1) * @gw + @xOffset
y = (ry + y1) * @gh + @yOffset
elip = @paper.ellipse(x, y, rx * @gw, ry * @gh)
elip.attr Utils.strokeAndThickness(colour, thickness)
elip
# When first starting drawing the ellipse
# @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
# 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

View File

@ -0,0 +1,171 @@
// An ellipse in the whiteboard
this.WhiteboardEllipseModel = (function() {
class WhiteboardEllipseModel extends WhiteboardToolModel {
constructor(paper) {
super(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"];
}
// 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) {
//console.log "Whiteboard - Making ellipse: "
//console.log info
let color, thickness, x, y;
if((info != null ? info.points : void 0) != null) {
x = info.points[0];
y = info.points[1];
color = info.color;
thickness = info.thickness;
this.obj = this.paper.ellipse(x * this.gw + this.xOffset, y * this.gh + this.yOffset, 0, 0);
this.obj.attr("stroke", formatColor(color));
this.obj.attr("stroke-width", zoomStroke(formatThickness(thickness)));
this.definition = [x, y, y, x, this.obj.attrs["stroke"], this.obj.attrs["stroke-width"]];
}
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) {
//console.log info
let circle, coords, r, ref, ref1, reversed, rx, ry, x1, x2, y1, y2;
if((info != null ? info.points : void 0) != null) {
x1 = info.points[0];
y1 = info.points[1];
x2 = info.points[2];
y2 = info.points[3];
circle = info.square;
if(this.obj != null) {
if(x2 < x1) {
ref = [x2, x1], x1 = ref[0], x2 = ref[1];
}
if(y2 < y1) {
ref1 = [y2, y1], y1 = ref1[0], y2 = ref1[1];
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(reversed) { // if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * this.gw / this.gh;
} else {
y2 = y1 + (x2 - x1) * this.gw / this.gh;
}
}
coords = {
x1: x1,
x2: x2,
y1: y1,
y2: y2
};
//console.log(coords)
rx = (x2 - x1) / 2;
ry = (y2 - y1) / 2;
r = {
rx: rx * this.gw,
ry: ry * this.gh,
cx: (rx + x1) * this.gw + this.xOffset,
cy: (ry + y1) * this.gh + this.yOffset
};
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[1] = y1;
this.definition[2] = x2;
return this.definition[3] = y2;
}
}
}
// 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) {
let elip, ref, ref1, rx, ry, x, y;
if(x2 < x1) {
ref = [x2, x1], x1 = ref[0], x2 = ref[1];
}
if(y2 < y1) {
ref1 = [y2, y1], y1 = ref1[0], y2 = ref1[1];
}
rx = (x2 - x1) / 2;
ry = (y2 - y1) / 2;
x = (rx + x1) * this.gw + this.xOffset;
y = (ry + y1) * this.gh + this.yOffset;
elip = this.paper.ellipse(x, y, rx * this.gw, ry * this.gh);
elip.attr(Utils.strokeAndThickness(colour, thickness));
return elip;
}
// When first starting drawing the ellipse
// @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
// 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;
})();

View File

@ -1,192 +0,0 @@
MAX_PATHS_IN_SEQUENCE = 30
# 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
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: svg path, stroke color, thickness
@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) ->
if info?.points?
x = info.points[0]
y = info.points[1]
color = info.color
thickness = info.thickness
x1 = x * @gw + @xOffset
y1 = y * @gh + @yOffset
path = "M" + x1 + " " + y1 + " L" + x1 + " " + y1
pathPercent = "M" + x + " " + y + " L" + x + " " + y
@obj = @paper.path(path)
@obj.attr "stroke", formatColor(color)
@obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
@obj.attr({"stroke-linejoin": "round"})
@obj.attr "stroke-linecap", "round"
@definition = [pathPercent, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@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) ->
if info?.points?
x1 = info.points[0]
y1 = info.points[1]
x2 = info.points[2]
y2 = info.points[3]
if @obj?
path = @_buildPath(info.points)
@definition[0] = path
path = @_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset)
@obj.attr path: path
# Draw a line on the paper
# @param {number,string} x1 1) the x value of the first point
# 2) the string path
# @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) ->
path = ""
if points and points.length >= 2
path += "M #{points[0]} #{points[1]}"
i = 2
while i < points.length
path += "L#{points[i]} #{points[i + 1]}"
i += 2
path += "Z"
path
# 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=0, yOffset=0) ->
path = null
points = string.match(/(\d+[.]?\d*)/g)
len = points.length
j = 0
# go through each point and multiply it by the new height and width
while j < len
if j isnt 0
path += "L" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
else
path = "M" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
j += 2
path

View File

@ -0,0 +1,216 @@
let MAX_PATHS_IN_SEQUENCE = 30;
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 {
constructor(paper) {
super(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"];
}
// 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) {
let color, path, pathPercent, thickness, x, x1, y, y1;
if((info != null ? info.points : void 0) != null) {
x = info.points[0];
y = info.points[1];
color = info.color;
thickness = info.thickness;
x1 = x * this.gw + this.xOffset;
y1 = y * this.gh + this.yOffset;
path = `M${x1} ${y1} L${x1} ${y1}`;
pathPercent = `M${x} ${y} L${x} ${y}`;
this.obj = this.paper.path(path);
this.obj.attr("stroke", formatColor(color));
this.obj.attr("stroke-width", zoomStroke(formatThickness(thickness)));
this.obj.attr({
"stroke-linejoin": "round"
});
this.obj.attr("stroke-linecap", "round");
this.definition = [pathPercent, this.obj.attrs["stroke"], this.obj.attrs["stroke-width"]];
}
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) {
let path, x1, x2, y1, y2;
if((info != null ? info.points : void 0) != null) {
x1 = info.points[0];
y1 = info.points[1];
x2 = info.points[2];
y2 = info.points[3];
if(this.obj != null) {
path = this._buildPath(info.points);
this.definition[0] = path;
path = this._scaleLinePath(path, this.gw, this.gh, this.xOffset, this.yOffset);
return this.obj.attr({
path: path
});
}
}
}
// Draw a line on the paper
// @param {number,string} x1 1) the x value of the first point
// 2) the string path
// @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) {
let i, path;
path = "";
if(points && points.length >= 2) {
path += `M ${points[0]} ${points[1]}`;
i = 2;
while(i < points.length) {
path += `${i}${1}L${points[i]} ${points[i + 1]}`;
i += 2;
}
path += "Z";
return path;
}
}
// 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) {
let j, len, path, points;
if(xOffset == null) {
xOffset = 0;
}
if(yOffset == null) {
yOffset = 0;
}
path = null;
points = string.match(/(\d+[.]?\d*)/g);
len = points.length;
j = 0;
// go through each point and multiply it by the new height and width
while(j < len) {
if(j !== 0) {
path += `${points[j + 1] * h}${yOffset}L${points[j] * w + xOffset},${points[j + 1] * h + yOffset}`;
} else {
path = `${points[j + 1] * h}${yOffset}M${points[j] * w + xOffset},${points[j + 1] * h + yOffset}`;
}
j += 2;
}
return path;
}
}
return WhiteboardLineModel;
})();

View File

@ -1,360 +0,0 @@
# "Paper" which is the Raphael term for the entire SVG object on the webpage.
# This class deals with this SVG component only.
class Meteor.WhiteboardPaperModel
# Container must be a DOM element
constructor: (@container) ->
# a WhiteboardCursorModel
@cursor = null
# all slides in the presentation indexed by url
@slides = {}
@panX = null
@panY = null
@current = {}
# the slide being shown
@current.slide = null
# a raphaeljs set with all the shapes in the current slide
@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)
@current.shapeDefinitions = []
@zoomLevel = 1
@shiftPressed = false
@currentPathCount = 0
@_updateContainerDimensions()
@zoomObserver = null
@adjustedWidth = 0
@adjustedHeight = 0
@widthRatio = 100
@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: ->
# paper is embedded within the div#slide of the page.
# @raphaelObj ?= ScaleRaphael(@container, "900", "500")
h = $("#"+@container).height()
w = $("#"+@container).width()
@raphaelObj ?= ScaleRaphael(@container, w, h)
@raphaelObj ?= ScaleRaphael(@container, $container.innerHeight(), $container.innerWidth())
@raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice"
@createCursor()
if @slides
@rebuild()
else
@slides = {} # if previously loaded
unless navigator.userAgent.indexOf("Firefox") is -1
@raphaelObj.renderfix()
@raphaelObj
# Re-add the images to the paper that are found
# in the slides array (an object of urls and dimensions).
rebuild: ->
@current.slide = null
for url of @slides
if @slides.hasOwnProperty(url)
@addImageToPaper url, @slides[url].getWidth(), @slides[url].getHeight()
scale: (width, height) ->
@raphaelObj?.changeSize(width, height)
# 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) ->
@_updateContainerDimensions()
# solve for the ratio of what length is going to fit more than the other
max = Math.max(width / @containerWidth, height / @containerHeight)
# fit it all in appropriately
url = @_slideUrl(url)
sw = width / max
sh = height / max
#cx = (@containerWidth / 2) - (width / 2)
#cy = (@containerHeight / 2) - (height / 2)
img = @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
@slides[url] = new WhiteboardSlideModel(img.id, url, img, width, height, sw, sh, cx, cy)
unless @current.slide?
img.toBack()
@current.slide = @slides[url]
else if @current.slide.url is url
img.toBack()
else
img.hide()
# TODO: other places might also required an update in these dimensions
@_updateContainerDimensions()
@_updateZoomRatios()
if @raphaelObj.w is 100 # on first load: Raphael object is initially tiny
@cursor.setRadius(0.65 * @widthRatio / 100)
else
@cursor.setRadius(6 * @widthRatio / 100)
img
# Removes all the images from the Raphael paper.
removeAllImagesFromPaper: ->
for url of @slides
if @slides.hasOwnProperty(url)
@raphaelObj.getById(@slides[url]?.getId())?.remove()
#@trigger('paper:image:removed', @slides[url].getId()) # Removes the previous image preventing images from being redrawn over each other repeatedly
@slides = {}
@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) ->
@currentTool = tool
console.log "setting current tool to", tool
switch tool
when "line"
@cursor.undrag()
@current.line = @_createTool(tool)
@cursor.drag(@current.line.dragOnMove, @current.line.dragOnStart, @current.line.dragOnEnd)
when "rectangle"
@cursor.undrag()
@current.rectangle = @_createTool(tool)
@cursor.drag(@current.rectangle.dragOnMove, @current.rectangle.dragOnStart, @current.rectangle.dragOnEnd)
else
console.log "ERROR: Cannot set invalid tool:", tool
# Clear all shapes from this paper.
clearShapes: ->
if @current.shapes?
@current.shapes.forEach (element) ->
element.remove()
@current.shapeDefinitions = []
@current.shapes.clear()
@clearCursor()
@createCursor()
clearCursor: ->
@cursor?.remove()
createCursor: ->
if @raphaelObj.w is 100 # on first load: Raphael object is initially tiny
@cursor = new WhiteboardCursorModel(@raphaelObj, 0.65)
@cursor.setRadius(0.65 * @widthRatio / 100)
else
@cursor = new WhiteboardCursorModel(@raphaelObj)
@cursor.setRadius(6 * @widthRatio / 100)
@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) ->
@current[shape].update(data)
# Make a shape `shape` with the data in `data`.
makeShape: (shape, data) ->
data.thickness *= @adjustedWidth / 1000
tool = null
#TODO pay attention to this array, data in this array slows down the whiteboard
#console.log @current
#console.log @
@current[shape] = @_createTool(shape)
toolModel = @current[shape]
tool = @current[shape].make(data)
if tool? and shape isnt "poll_result"
@current.shapes ?= @raphaelObj.set()
@current.shapes.push(tool)
@current.shapeDefinitions.push(toolModel.getDefinition())
#We have a separate case for Poll as it returns an array instead of just one object
if tool? and shape is "poll_result"
@current.shapes ?= @raphaelObj.set()
for obj in tool
@current.shapes.push(obj)
@current.shapeDefinitions.push(toolModel.getDefinition())
# 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) ->
[cx, cy] = @_currentSlideOffsets()
[slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
@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 @viewBoxXpos? && @viewBoxYPos? && @viewBoxWidth? && @viewBoxHeight?
@cursor.setPosition( @viewBoxXpos + x * @viewBoxWidth, @viewBoxYPos + y * @viewBoxHeight )
zoomAndPan: (widthRatio, heightRatio, xOffset, yOffset) ->
# console.log "zoomAndPan #{widthRatio} #{heightRatio} #{xOffset} #{yOffset}"
newX = - xOffset * 2 * @adjustedWidth / 100
newY = - yOffset * 2 * @adjustedHeight / 100
newWidth = @adjustedWidth * widthRatio / 100
newHeight = @adjustedHeight * heightRatio / 100
@raphaelObj.setViewBox(newX, newY, newWidth, newHeight) # zooms and pans
setAdjustedDimensions: (width, height) ->
@adjustedWidth = width
@adjustedHeight = height
# Update the dimensions of the container.
_updateContainerDimensions: ->
$container = $('#whiteboard-paper')
containerDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
if($container.innerWidth() is 0)
@containerWidth = containerDimensions.boardWidth
else
@containerWidth = $container.innerWidth()
if($container.innerHeight() is 0)
@containerHeight = containerDimensions.boardHeight
else
@containerHeight = $container.innerHeight()
@containerOffsetLeft = $container.offset()?.left
@containerOffsetTop = $container.offset()?.top
_updateZoomRatios: ->
currentSlideDoc = BBB.getCurrentSlide("_updateZoomRatios")
@widthRatio = currentSlideDoc?.slide.width_ratio
@heightRatio = currentSlideDoc?.slide.height_ratio
# 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) ->
if @slides[url]
id = @slides[url].getId()
return @raphaelObj.getById(id) if id?
null
_currentSlideDimensions: ->
if @current.slide? then @current.slide.getDimensions() else [0, 0]
_currentSlideOriginalDimensions: ->
if @current.slide? then @current.slide.getOriginalDimensions() else [0, 0]
_currentSlideOffsets: ->
if @current.slide? then @current.slide.getOffsets() else [0, 0]
# Wrapper method to create a tool for the whiteboard
_createTool: (type) ->
switch type
when "pencil"
model = WhiteboardLineModel
when "path", "line"
model = WhiteboardLineModel
when "rectangle"
model = WhiteboardRectModel
when "ellipse"
model = WhiteboardEllipseModel
when "triangle"
model = WhiteboardTriangleModel
when "text"
model = WhiteboardTextModel
when "poll_result"
model = WhiteboardPollModel
if model?
[slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
[xOffset, yOffset] = @_currentSlideOffsets()
[width, height] = @_currentSlideDimensions()
tool = new model(@raphaelObj)
# TODO: why are the parameters inverted and it works?
tool.setPaperSize(slideHeight, slideWidth)
tool.setOffsets(xOffset, yOffset)
tool.setPaperDimensions(width,height)
tool
else
null
# Adds the base url (the protocol+server part) to `url` if needed.
_slideUrl: (url) ->
if url?.match(/http[s]?:/)
url
else
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) ->
@removeAllImagesFromPaper()
@_updateContainerDimensions()
boardWidth = @containerWidth
boardHeight = @containerHeight
currentSlide = BBB.getCurrentSlide("_displayPage")
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlideCursor = Meteor.Slides.find({"presentationId": presentationId, "slide.current": true})
if @zoomObserver isnt null
@zoomObserver.stop()
_this = this
@zoomObserver = currentSlideCursor.observe # watching the current slide changes
changed: (newDoc, oldDoc) ->
if originalWidth <= originalHeight
@adjustedWidth = boardHeight * originalWidth / originalHeight
@adjustedHeight = boardHeight
else
@adjustedHeight = boardWidth * originalHeight / originalWidth
@adjustedWidth = boardWidth
_this.zoomAndPan(newDoc.slide.width_ratio, newDoc.slide.height_ratio,
newDoc.slide.x_offset, newDoc.slide.y_offset)
oldRatio = (oldDoc.slide.width_ratio + oldDoc.slide.height_ratio) / 2
newRatio = (newDoc.slide.width_ratio + newDoc.slide.height_ratio) / 2
_this?.current?.shapes?.forEach (shape) ->
shape.attr "stroke-width", shape.attr('stroke-width') * oldRatio / newRatio
if _this.raphaelObj is 100 # on first load: Raphael object is initially tiny
_this.cursor.setRadius(0.65 * newDoc.slide.width_ratio / 100)
else
_this.cursor.setRadius(6 * newDoc.slide.width_ratio / 100)
if originalWidth <= originalHeight
# square => boardHeight is the shortest side
@adjustedWidth = boardHeight * originalWidth / originalHeight
$('#whiteboard-paper').width(@adjustedWidth)
@addImageToPaper(data, @adjustedWidth, boardHeight)
@adjustedHeight = boardHeight
else
@adjustedHeight = boardWidth * originalHeight / originalWidth
$('#whiteboard-paper').height(@adjustedHeight)
@addImageToPaper(data, boardWidth, @adjustedHeight)
@adjustedWidth = boardWidth
if currentSlide?
@zoomAndPan(currentSlide.slide.width_ratio, currentSlide.slide.height_ratio,
currentSlide.slide.x_offset, currentSlide.slide.y_offset)

View File

@ -0,0 +1,459 @@
// "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() {
class WhiteboardPaperModel {
// Container must be a DOM element
constructor(container) {
this.container = container;
// a WhiteboardCursorModel
this.cursor = null;
// all slides in the presentation indexed by url
this.slides = {};
this.panX = null;
this.panY = null;
this.current = {};
// the slide being shown
this.current.slide = null;
// a raphaeljs set with all the shapes in the current slide
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.zoomLevel = 1;
this.shiftPressed = false;
this.currentPathCount = 0;
this._updateContainerDimensions();
this.zoomObserver = null;
this.adjustedWidth = 0;
this.adjustedHeight = 0;
this.widthRatio = 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() {
// paper is embedded within the div#slide of the page.
// @raphaelObj ?= ScaleRaphael(@container, "900", "500")
let h, w;
h = $(`#${this.container}`).height();
w = $(`#${this.container}`).width();
if(this.raphaelObj == null) {
this.raphaelObj = ScaleRaphael(this.container, w, h);
}
if(this.raphaelObj == null) {
this.raphaelObj = ScaleRaphael(this.container, $container.innerHeight(), $container.innerWidth());
}
this.raphaelObj.canvas.setAttribute("preserveAspectRatio", "xMinYMin slice");
this.createCursor();
if(this.slides) {
this.rebuild();
} else {
this.slides = {}; // if previously loaded
}
if(navigator.userAgent.indexOf("Firefox") !== -1) {
this.raphaelObj.renderfix();
}
return this.raphaelObj;
}
// Re-add the images to the paper that are found
// in the slides array (an object of urls and dimensions).
rebuild() {
let results, url;
this.current.slide = null;
results = [];
for(url in this.slides) {
if(this.slides.hasOwnProperty(url)) {
results.push(
this.addImageToPaper(url, this.slides[url].getWidth(), this.slides[url].getHeight())
);
} else {
results.push(void 0);
}
}
return results;
}
scale(width, height) {
let ref;
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) {
let cx, cy, img, max, sh, sw;
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);
// fit it all in appropriately
url = this._slideUrl(url);
sw = width / 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);
// 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);
if(this.current.slide == null) {
img.toBack();
this.current.slide = this.slides[url];
} else if(this.current.slide.url === url) {
img.toBack();
} else {
img.hide();
}
// TODO: other places might also required an update in these dimensions
this._updateContainerDimensions();
this._updateZoomRatios();
if(this.raphaelObj.w === 100) { // on first load: Raphael object is initially tiny
this.cursor.setRadius(0.65 * this.widthRatio / 100);
} else {
this.cursor.setRadius(6 * this.widthRatio / 100);
}
return img;
}
// Removes all the images from the Raphael paper.
removeAllImagesFromPaper() {
let ref, ref1, url;
for(url in this.slides) {
if (this.slides.hasOwnProperty(url)) {
if ((ref = this.raphaelObj.getById((ref1 = this.slides[url]) != null ? ref1.getId() : void 0)) != null) {
ref.remove();
}
//@trigger('paper:image:removed', @slides[url].getId()) # Removes the previous image preventing images from being redrawn over each other repeatedly
}
}
this.slides = {};
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) {
this.currentTool = tool;
console.log("setting current tool to", tool);
switch(tool) {
case "line":
this.cursor.undrag();
this.current.line = this._createTool(tool);
return this.cursor.drag(this.current.line.dragOnMove, this.current.line.dragOnStart, this.current.line.dragOnEnd);
case "rectangle":
this.cursor.undrag();
this.current.rectangle = this._createTool(tool);
return this.cursor.drag(this.current.rectangle.dragOnMove, this.current.rectangle.dragOnStart, this.current.rectangle.dragOnEnd);
default:
return console.log("ERROR: Cannot set invalid tool:", tool);
}
}
// Clear all shapes from this paper.
clearShapes() {
if(this.current.shapes != null) {
this.current.shapes.forEach(element => {
return element.remove();
});
this.current.shapeDefinitions = [];
this.current.shapes.clear();
}
this.clearCursor();
return this.createCursor();
}
clearCursor() {
let ref;
return (ref = this.cursor) != null ? ref.remove() : void 0;
}
createCursor() {
if(this.raphaelObj.w === 100) { // on first load: Raphael object is initially tiny
this.cursor = new WhiteboardCursorModel(this.raphaelObj, 0.65);
this.cursor.setRadius(0.65 * this.widthRatio / 100);
} else {
this.cursor = new WhiteboardCursorModel(this.raphaelObj);
this.cursor.setRadius(6 * this.widthRatio / 100);
}
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) {
return this.current[shape].update(data);
}
// Make a shape `shape` with the data in `data`.
makeShape(shape, data) {
let base, base1, i, len, obj, tool, toolModel;
data.thickness *= this.adjustedWidth / 1000;
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);
toolModel = this.current[shape];
tool = this.current[shape].make(data);
if((tool != null) && shape !== "poll_result") {
if((base = this.current).shapes == null) {
base.shapes = this.raphaelObj.set();
}
this.current.shapes.push(tool);
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((base1 = this.current).shapes == null) {
base1.shapes = this.raphaelObj.set();
}
for(i = 0, len = tool.length; i < len; i++) {
obj = tool[i];
this.current.shapes.push(obj);
}
return this.current.shapeDefinitions.push(toolModel.getDefinition());
}
}
// 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) {
let cx, cy, ref, ref1, slideHeight, slideWidth;
ref = this._currentSlideOffsets(), cx = ref[0], cy = ref[1];
ref1 = this._currentSlideOriginalDimensions(), slideWidth = ref1[0], slideHeight = ref1[1];
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)) {
return this.cursor.setPosition(this.viewBoxXpos + x * this.viewBoxWidth, this.viewBoxYPos + y * this.viewBoxHeight);
}
}
zoomAndPan(widthRatio, heightRatio, xOffset, yOffset) {
// console.log "zoomAndPan #{widthRatio} #{heightRatio} #{xOffset} #{yOffset}"
let newHeight, newWidth, newX, newY;
newX = -xOffset * 2 * this.adjustedWidth / 100;
newY = -yOffset * 2 * this.adjustedHeight / 100;
newWidth = this.adjustedWidth * widthRatio / 100;
newHeight = this.adjustedHeight * heightRatio / 100;
return this.raphaelObj.setViewBox(newX, newY, newWidth, newHeight);
}
setAdjustedDimensions(width, height) {
this.adjustedWidth = width;
return this.adjustedHeight = height;
}
// Update the dimensions of the container.
_updateContainerDimensions() {
let $container, containerDimensions, ref, ref1;
$container = $('#whiteboard-paper');
containerDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'));
if($container.innerWidth() === 0) {
this.containerWidth = containerDimensions.boardWidth;
} else {
this.containerWidth = $container.innerWidth();
}
if($container.innerHeight() === 0) {
this.containerHeight = containerDimensions.boardHeight;
} else {
this.containerHeight = $container.innerHeight();
}
this.containerOffsetLeft = (ref = $container.offset()) != null ? ref.left : void 0;
return this.containerOffsetTop = (ref1 = $container.offset()) != null ? ref1.top : void 0;
}
_updateZoomRatios() {
let currentSlideDoc;
currentSlideDoc = BBB.getCurrentSlide("_updateZoomRatios");
this.widthRatio = currentSlideDoc != null ? currentSlideDoc.slide.width_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) {
let id;
if(this.slides[url]) {
id = this.slides[url].getId();
if (id != null) {
return this.raphaelObj.getById(id);
}
}
return null;
}
_currentSlideDimensions() {
if(this.current.slide != null) {
return this.current.slide.getDimensions();
} else {
return [0, 0];
}
}
_currentSlideOriginalDimensions() {
if(this.current.slide != null) {
return this.current.slide.getOriginalDimensions();
} else {
return [0, 0];
}
}
_currentSlideOffsets() {
if(this.current.slide != null) {
return this.current.slide.getOffsets();
} else {
return [0, 0];
}
}
// Wrapper method to create a tool for the whiteboard
_createTool(type) {
let height, model, ref, ref1, ref2, slideHeight, slideWidth, tool, width, xOffset, yOffset;
switch(type) {
case "pencil":
model = WhiteboardLineModel;
break;
case "path":
case "line":
model = WhiteboardLineModel;
break;
case "rectangle":
model = WhiteboardRectModel;
break;
case "ellipse":
model = WhiteboardEllipseModel;
break;
case "triangle":
model = WhiteboardTriangleModel;
break;
case "text":
model = WhiteboardTextModel;
break;
case "poll_result":
model = WhiteboardPollModel;
}
if(model != null) {
ref = this._currentSlideOriginalDimensions(), slideWidth = ref[0], slideHeight = ref[1];
ref1 = this._currentSlideOffsets(), xOffset = ref1[0], yOffset = ref1[1];
ref2 = this._currentSlideDimensions(), width = ref2[0], height = ref2[1];
tool = new model(this.raphaelObj);
// TODO: why are the parameters inverted and it works?
tool.setPaperSize(slideHeight, slideWidth);
tool.setOffsets(xOffset, yOffset);
tool.setPaperDimensions(width, height);
return tool;
} else {
return null;
}
}
// Adds the base url (the protocol+server part) to `url` if needed.
_slideUrl(url) {
if(url != null ? url.match(/http[s]?:/) : void 0) {
return url;
} else {
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) {
let _this, boardHeight, boardWidth, currentPresentation, currentSlide, currentSlideCursor, presentationId, ref;
this.removeAllImagesFromPaper();
this._updateContainerDimensions();
boardWidth = this.containerWidth;
boardHeight = this.containerHeight;
currentSlide = BBB.getCurrentSlide("_displayPage");
currentPresentation = Meteor.Presentations.findOne({
"presentation.current": true
});
presentationId = currentPresentation != null ? (ref = currentPresentation.presentation) != null ? ref.id : void 0 : void 0;
currentSlideCursor = Meteor.Slides.find({
"presentationId": presentationId,
"slide.current": true
});
if(this.zoomObserver !== null) {
this.zoomObserver.stop();
}
_this = this;
this.zoomObserver = currentSlideCursor.observe({ // watching the current slide changes
changed(newDoc, oldDoc) {
let newRatio, oldRatio, ref1, ref2;
if(originalWidth <= originalHeight) {
this.adjustedWidth = boardHeight * originalWidth / originalHeight;
this.adjustedHeight = boardHeight;
} else {
this.adjustedHeight = boardWidth * originalHeight / originalWidth;
this.adjustedWidth = boardWidth;
}
_this.zoomAndPan(
newDoc.slide.width_ratio,
newDoc.slide.height_ratio,
newDoc.slide.x_offset,
newDoc.slide.y_offset
);
oldRatio = (oldDoc.slide.width_ratio + oldDoc.slide.height_ratio) / 2;
newRatio = (newDoc.slide.width_ratio + newDoc.slide.height_ratio) / 2;
if(_this != null) {
if((ref1 = _this.current) != null) {
if((ref2 = ref1.shapes) != null) {
ref2.forEach(shape => {
return shape.attr("stroke-width", shape.attr('stroke-width') * oldRatio / newRatio);
});
}
}
}
if(_this.raphaelObj === 100) { // on first load: Raphael object is initially tiny
return _this.cursor.setRadius(0.65 * newDoc.slide.width_ratio / 100);
} else {
return _this.cursor.setRadius(6 * newDoc.slide.width_ratio / 100);
}
}
});
if(originalWidth <= originalHeight) {
// square => boardHeight is the shortest side
this.adjustedWidth = boardHeight * originalWidth / originalHeight;
$('#whiteboard-paper').width(this.adjustedWidth);
this.addImageToPaper(data, this.adjustedWidth, boardHeight);
this.adjustedHeight = boardHeight;
} else {
this.adjustedHeight = boardWidth * originalHeight / originalWidth;
$('#whiteboard-paper').height(this.adjustedHeight);
this.addImageToPaper(data, boardWidth, this.adjustedHeight);
this.adjustedWidth = boardWidth;
}
if(currentSlide != null) {
return this.zoomAndPan(currentSlide.slide.width_ratio, currentSlide.slide.height_ratio, currentSlide.slide.x_offset, currentSlide.slide.y_offset);
}
}
}
return WhiteboardPaperModel;
})();

View File

@ -1,299 +0,0 @@
# A poll in the whiteboard
class @WhiteboardPollModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: x1, y1, x2, y2, stroke color, thickness, fill
@definition = [0, 0, 0, 0, "#333333", "2px", "#ffffff"]
@paper
# 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) =>
#data needed to create the first base rectangle filled with white color
x1 = startingData.points[0]
y1 = startingData.points[1]
x2 = startingData.points[2] + startingData.points[0] - 0.001
y2 = startingData.points[3] + startingData.points[1] - 0.001
thickness = 2
backgroundColor = "#ffffff"
verticalPadding = 0
horizontalPadding = 0
calcFontSize = 30
votesTotal = 0
maxNumVotes = 0
textArray = []
#creating an array of text objects for the labels, percentages and number inside line bars
if startingData.result?
#counting the total number of votes and finding the biggest number of votes
for i in [0..startingData.result.length-1]
votesTotal += startingData.result[i].num_votes
if maxNumVotes < startingData.result[i].num_votes
maxNumVotes = startingData.result[i].num_votes
textArray[i] = []
#filling the array with proper text objects to display
for i in [0..startingData.result.length-1]
textArray[i].push(startingData.result[i].key, startingData.result[i].num_votes+"")
if votesTotal is 0
textArray[i].push("0%")
else
percResult = startingData.result[i].num_votes/votesTotal*100;
textArray[i].push(Math.round(percResult) + "%")
#if coordinates are reversed - change them back
if x2 < x1
[x1, x2] = [x2, x1]
if y2 < y1
[y1, y2] = [y2, y1]
#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 * @gw + @xOffset
y = y1 * @gh + @yOffset
width = (x2 * @gw + @xOffset) - x
height = (y2 * @gh + @yOffset) - y
#creating a base outer rectangle
@obj = @paper.rect(x, y, width, height, 0)
@obj.attr "fill", backgroundColor
@obj.attr "stroke-width", 0
@definition =
shape: "poll_result"
data: [x1, y1, x2, y2, @obj.attrs["stroke"], @obj.attrs["stroke-width"], @obj.attrs["fill"]]
#recalculated coordinates, width and height for the inner rectangle
width = width*0.95
height = height - width*0.05
x = x+width*0.025
y = y+width*0.025
#creating a base inner rectangle
@obj1 = @paper.rect(x, y, width, height, 0)
@obj1.attr "stroke", "#333333"
@obj1.attr "fill", backgroundColor
@obj1.attr "stroke-width", zoomStroke(formatThickness(thickness))
@definition =
shape: "poll_result"
data: [x1, y1, x2, y2, @obj.attrs["stroke"], @obj1.attrs["stroke-width"], @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)
calcFontSize = calculatedData[0]
maxLeftWidth = calculatedData[1]
maxRightWidth = calculatedData[2]
maxLineHeight = calculatedData[3]
maxDigitWidth = calculatedData[4]
maxBarWidth = width*0.9-maxLeftWidth-maxRightWidth
barHeight = height*0.75/textArray.length
svgNSi = "http://www.w3.org/2000/svg"
#Initializing a text element for further calculations and for the left column of keys
@obj2 = @paper.text(x, y, "")
@obj2.attr
"fill": "#333333"
"font-family": "Arial"
"font-size": calcFontSize
@obj2.node.style["text-anchor"] = "start" # force left align
@obj2.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different
leftCell = @obj2.node
while leftCell? and leftCell.hasChildNodes()
leftCell.removeChild(leftCell.firstChild)
#Initializing a text element for the right column of percentages
@obj3 = @paper.text(x, y, "")
@obj3.attr
"fill": "#333333"
"font-family": "Arial"
"font-size": calcFontSize
@obj3.node.style["text-anchor"] = "end" # force right align
@obj3.node.style["textAnchor"] = "end" # for firefox, 'cause they like to be different
rightCell = @obj3.node
while rightCell? and rightCell.hasChildNodes()
rightCell.removeChild(rightCell.firstChild)
#setting a font size for the text elements on the left and on the right
leftCell.style['font-size'] = calcFontSize
rightCell.style['font-size'] = calcFontSize
#Horizontal padding
horizontalPadding = width*0.1/4
#Vertical padding
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
#*****************************************************************************************************
#Initial coordinates of the key column
yLeft = y+verticalPadding+barHeight/2 - magicNumber
xLeft = x + horizontalPadding + 1
#Initial coordinates of the line bar column
xBar = x+maxLeftWidth+horizontalPadding*2
yBar = y + verticalPadding
#Initial coordinates of the percentage column
yRight = y+verticalPadding+barHeight/2 - magicNumber
xRight = x + horizontalPadding*3 + maxLeftWidth + maxRightWidth + maxBarWidth + 1
objects = [@obj, @obj1, @obj2, @obj3]
for i in [0..textArray.length-1]
#Adding an element to the left column
tempSpanEl = document.createElementNS(svgNSi, "tspan")
tempSpanEl.setAttributeNS null, "x", xLeft
tempSpanEl.setAttributeNS null, "y", yLeft
tempSpanEl.setAttributeNS null, "dy", maxLineHeight/2
tempTextNode = document.createTextNode(textArray[i][0])
tempSpanEl.appendChild tempTextNode
leftCell.appendChild tempSpanEl
#drawing a black graph bar
if maxNumVotes is 0 or startingData.result[i].num_votes is 0
barWidth = 2
else
barWidth = startingData.result[i].num_votes / maxNumVotes * maxBarWidth
@obj4 = @paper.rect(xBar, yBar, barWidth, barHeight, 0)
@obj4.attr "stroke", "#333333"
@obj4.attr "fill", "#333333"
@obj4.attr "stroke-width", zoomStroke(formatThickness(0))
objects.push @obj4
#Adding an element to the right column
tempSpanEl = document.createElementNS(svgNSi, "tspan")
tempSpanEl.setAttributeNS null, "x", xRight
tempSpanEl.setAttributeNS null, "y", yRight
tempSpanEl.setAttributeNS null, "dy", maxLineHeight/2
tempTextNode = document.createTextNode(textArray[i][2])
tempSpanEl.appendChild tempTextNode
rightCell.appendChild tempSpanEl
#changing the Y coordinate for all the objects
yBar = yBar + barHeight + verticalPadding
yLeft = yLeft + barHeight + verticalPadding
yRight = yRight + barHeight + verticalPadding
#Initializing a text element for the number of votes text field inside the line bar
@obj5 = @paper.text(x, y, "")
@obj5.attr
"fill": "#333333"
"font-family": "Arial"
"font-size": calcFontSize
centerCell = @obj5.node
while centerCell? and centerCell.hasChildNodes()
centerCell.removeChild(centerCell.firstChild)
#Initial coordinates of the text inside the bar column
xNumVotesDefault = x+maxLeftWidth+horizontalPadding*2
xNumVotesMovedRight = xNumVotesDefault + barWidth/2 + horizontalPadding + maxDigitWidth/2
yNumVotes = y + verticalPadding - magicNumber
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 in [0..textArray.length-1]
if maxNumVotes is 0 or startingData.result[i].num_votes is 0
barWidth = 2
else
barWidth = startingData.result[i].num_votes / maxNumVotes * maxBarWidth
if barWidth < maxDigitWidth + 8
xNumVotes = xNumVotesMovedRight
color = "#333333"
else
xNumVotes = xNumVotesDefault
color = "white"
tempSpanEl = document.createElementNS(svgNSi, "tspan")
tempSpanEl.setAttributeNS null, "x", xNumVotes + barWidth/2
tempSpanEl.setAttributeNS null, "y", yNumVotes + barHeight/2
tempSpanEl.setAttributeNS null, "dy", maxLineHeight/2
tempSpanEl.setAttributeNS null, "fill", color
tempTextNode = document.createTextNode(startingData.result[i].num_votes)
tempSpanEl.appendChild tempTextNode
centerCell.appendChild tempSpanEl
yNumVotes = yNumVotes + barHeight + verticalPadding
objects.push @obj5
objects
# Update the poll dimensions. Does nothing.
update: (startingData) ->
calculateFontAndWidth = (textArray, calcFontSize, width, height, x, y) ->
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
maxLineHeight = height*0.75/textArray?.length
#calculating a proper font-size
flag = true
while flag
flag = false
for i in [0..textArray.length-1]
for j in [0..textArray[i].length-1]
test = getRenderedTextSize(textArray[i][j], calcFontSize)
spanWidth = test[0]
spanHeight = test[1]
if spanWidth > maxLineWidth or spanHeight > maxLineHeight
calcFontSize -= 1
flag = true
calculatedData.push calcFontSize
#looking for a maximum width and height of the left and right text elements
maxLeftWidth = 0
maxRightWidth = 0
maxLineHeight = 0
for line in textArray
test = getRenderedTextSize(line[0], calcFontSize)
spanWidth = test[0]
spanHeight = test[1]
if spanWidth > maxLeftWidth
maxLeftWidth = spanWidth
if spanHeight > maxLineHeight
maxLineHeight = spanHeight
test = getRenderedTextSize(line[2], calcFontSize)
spanWidth = test[0]
spanHeight = test[1]
if spanWidth > maxRightWidth
maxRightWidth = spanWidth
if spanHeight > maxLineHeight
maxLineHeight = spanHeight
test = getRenderedTextSize("0", calcFontSize)
spanWidth = test[0]
spanHeight = test[1]
maxDigitWidth = spanWidth
calculatedData.push maxLeftWidth, maxRightWidth, maxLineHeight, maxDigitWidth
calculatedData
getRenderedTextSize = (string, fontSize) ->
paper = Raphael(0, 0, 0, 0)
paper.canvas.style.visibility = 'hidden'
el = paper.text(0, 0, string)
el.attr "font-family", "Arial"
el.attr "font-size", fontSize
bBox = el.getBBox()
paper.remove()
arrayTest = []
arrayTest.push bBox.width
arrayTest.push bBox.height
paper.remove()
arrayTest

View File

@ -0,0 +1,342 @@
let calculateFontAndWidth, getRenderedTextSize;
let bind = function(fn, me) {
return function() {
return fn.apply(me, arguments);
};
};
// A poll in the whiteboard
this.WhiteboardPollModel = (function() {
class WhiteboardPollModel extends WhiteboardToolModel {
constructor(paper1) {
super(paper1);
this.paper = paper1;
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"];
}
// 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) {
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];
y1 = startingData.points[1];
x2 = startingData.points[2] + startingData.points[0] - 0.001;
y2 = startingData.points[3] + startingData.points[1] - 0.001;
thickness = 2;
backgroundColor = "#ffffff";
verticalPadding = 0;
horizontalPadding = 0;
calcFontSize = 30;
votesTotal = 0;
maxNumVotes = 0;
textArray = [];
//creating an array of text objects for the labels, percentages and number inside line bars
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) {
votesTotal += startingData.result[i].num_votes;
if(maxNumVotes < startingData.result[i].num_votes) {
maxNumVotes = startingData.result[i].num_votes;
}
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) {
textArray[i].push(startingData.result[i].key, `${startingData.result[i].num_votes}`);
if(votesTotal === 0) {
textArray[i].push("0%");
} else {
percResult = startingData.result[i].num_votes / votesTotal * 100;
textArray[i].push(`${Math.round(percResult)}%`);
}
}
}
//if coordinates are reversed - change them back
if(x2 < x1) {
ref2 = [x2, x1], x1 = ref2[0], x2 = ref2[1];
}
if(y2 < y1) {
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;
y = y1 * this.gh + this.yOffset;
width = (x2 * this.gw + this.xOffset) - x;
height = (y2 * this.gh + this.yOffset) - y;
//creating a base outer rectangle
this.obj = this.paper.rect(x, y, width, height, 0);
this.obj.attr("fill", backgroundColor);
this.obj.attr("stroke-width", 0);
this.definition = {
shape: "poll_result",
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;
height = height - width * 0.05;
x = x + 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.attr("stroke", "#333333");
this.obj1.attr("fill", backgroundColor);
this.obj1.attr("stroke-width", zoomStroke(formatThickness(thickness)));
this.definition = {
shape: "poll_result",
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);
calcFontSize = calculatedData[0];
maxLeftWidth = calculatedData[1];
maxRightWidth = calculatedData[2];
maxLineHeight = calculatedData[3];
maxDigitWidth = calculatedData[4];
maxBarWidth = width * 0.9 - maxLeftWidth - maxRightWidth;
barHeight = height * 0.75 / textArray.length;
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.attr({
"fill": "#333333",
"font-family": "Arial",
"font-size": calcFontSize
});
this.obj2.node.style["text-anchor"] = "start";
this.obj2.node.style["textAnchor"] = "start";
leftCell = this.obj2.node;
while((leftCell != null) && leftCell.hasChildNodes()) {
leftCell.removeChild(leftCell.firstChild);
}
//Initializing a text element for the right column of percentages
this.obj3 = this.paper.text(x, y, "");
this.obj3.attr({
"fill": "#333333",
"font-family": "Arial",
"font-size": calcFontSize
});
this.obj3.node.style["text-anchor"] = "end";
this.obj3.node.style["textAnchor"] = "end";
rightCell = this.obj3.node;
while((rightCell != null) && rightCell.hasChildNodes()) {
rightCell.removeChild(rightCell.firstChild);
}
//setting a font size for the text elements on the left and on the right
leftCell.style['font-size'] = calcFontSize;
rightCell.style['font-size'] = calcFontSize;
//Horizontal padding
horizontalPadding = width * 0.1 / 4;
//Vertical padding
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;
//*****************************************************************************************************
//Initial coordinates of the key column
yLeft = y + verticalPadding + barHeight / 2 - magicNumber;
xLeft = x + horizontalPadding + 1;
//Initial coordinates of the line bar column
xBar = x + maxLeftWidth + horizontalPadding * 2;
yBar = y + verticalPadding;
//Initial coordinates of the percentage column
yRight = y + verticalPadding + barHeight / 2 - magicNumber;
xRight = x + horizontalPadding * 3 + maxLeftWidth + maxRightWidth + maxBarWidth + 1;
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) {
//Adding an element to the left column
tempSpanEl = document.createElementNS(svgNSi, "tspan");
tempSpanEl.setAttributeNS(null, "x", xLeft);
tempSpanEl.setAttributeNS(null, "y", yLeft);
tempSpanEl.setAttributeNS(null, "dy", maxLineHeight / 2);
tempTextNode = document.createTextNode(textArray[i][0]);
tempSpanEl.appendChild(tempTextNode);
leftCell.appendChild(tempSpanEl);
//drawing a black graph bar
if(maxNumVotes === 0 || startingData.result[i].num_votes === 0) {
barWidth = 2;
} else {
barWidth = startingData.result[i].num_votes / maxNumVotes * maxBarWidth;
}
this.obj4 = this.paper.rect(xBar, yBar, barWidth, barHeight, 0);
this.obj4.attr("stroke", "#333333");
this.obj4.attr("fill", "#333333");
this.obj4.attr("stroke-width", zoomStroke(formatThickness(0)));
objects.push(this.obj4);
//Adding an element to the right column
tempSpanEl = document.createElementNS(svgNSi, "tspan");
tempSpanEl.setAttributeNS(null, "x", xRight);
tempSpanEl.setAttributeNS(null, "y", yRight);
tempSpanEl.setAttributeNS(null, "dy", maxLineHeight / 2);
tempTextNode = document.createTextNode(textArray[i][2]);
tempSpanEl.appendChild(tempTextNode);
rightCell.appendChild(tempSpanEl);
//changing the Y coordinate for all the objects
yBar = yBar + barHeight + verticalPadding;
yLeft = yLeft + 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.attr({
"fill": "#333333",
"font-family": "Arial",
"font-size": calcFontSize
});
centerCell = this.obj5.node;
while((centerCell != null) && centerCell.hasChildNodes()) {
centerCell.removeChild(centerCell.firstChild);
}
//Initial coordinates of the text inside the bar column
xNumVotesDefault = x + maxLeftWidth + horizontalPadding * 2;
xNumVotesMovedRight = xNumVotesDefault + barWidth / 2 + horizontalPadding + maxDigitWidth / 2;
yNumVotes = y + verticalPadding - magicNumber;
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) {
if(maxNumVotes === 0 || startingData.result[i].num_votes === 0) {
barWidth = 2;
} else {
barWidth = startingData.result[i].num_votes / maxNumVotes * maxBarWidth;
}
if(barWidth < maxDigitWidth + 8) {
xNumVotes = xNumVotesMovedRight;
color = "#333333";
} else {
xNumVotes = xNumVotesDefault;
color = "white";
}
tempSpanEl = document.createElementNS(svgNSi, "tspan");
tempSpanEl.setAttributeNS(null, "x", xNumVotes + barWidth / 2);
tempSpanEl.setAttributeNS(null, "y", yNumVotes + barHeight / 2);
tempSpanEl.setAttributeNS(null, "dy", maxLineHeight / 2);
tempSpanEl.setAttributeNS(null, "fill", color);
tempTextNode = document.createTextNode(startingData.result[i].num_votes);
tempSpanEl.appendChild(tempTextNode);
centerCell.appendChild(tempSpanEl);
yNumVotes = yNumVotes + barHeight + verticalPadding;
}
objects.push(this.obj5);
return objects;
}
// Update the poll dimensions. Does nothing.
update(startingData) {}
}
return WhiteboardPollModel;
})();
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;
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;
maxLineHeight = height * 0.75 / (textArray != null ? textArray.length : void 0);
//calculating a proper font-size
flag = true;
while(flag) {
flag = false;
for(i = k = 0, ref = textArray.length - 1; 0 <= ref ? k <= ref : k >= ref; i = 0 <= ref ? ++k : --k) {
for(j = l = 0, ref1 = textArray[i].length - 1; 0 <= ref1 ? l <= ref1 : l >= ref1; j = 0 <= ref1 ? ++l : --l) {
test = getRenderedTextSize(textArray[i][j], calcFontSize);
spanWidth = test[0];
spanHeight = test[1];
if(spanWidth > maxLineWidth || spanHeight > maxLineHeight) {
calcFontSize -= 1;
flag = true;
}
}
}
}
calculatedData.push(calcFontSize);
//looking for a maximum width and height of the left and right text elements
maxLeftWidth = 0;
maxRightWidth = 0;
maxLineHeight = 0;
for(m = 0, len = textArray.length; m < len; m++) {
line = textArray[m];
test = getRenderedTextSize(line[0], calcFontSize);
spanWidth = test[0];
spanHeight = test[1];
if(spanWidth > maxLeftWidth) {
maxLeftWidth = spanWidth;
}
if(spanHeight > maxLineHeight) {
maxLineHeight = spanHeight;
}
test = getRenderedTextSize(line[2], calcFontSize);
spanWidth = test[0];
spanHeight = test[1];
if(spanWidth > maxRightWidth) {
maxRightWidth = spanWidth;
}
if(spanHeight > maxLineHeight) {
maxLineHeight = spanHeight;
}
}
test = getRenderedTextSize("0", calcFontSize);
spanWidth = test[0];
spanHeight = test[1];
maxDigitWidth = spanWidth;
calculatedData.push(maxLeftWidth, maxRightWidth, maxLineHeight, maxDigitWidth);
return calculatedData;
};
getRenderedTextSize = function(string, fontSize) {
let arrayTest, bBox, el, paper;
paper = Raphael(0, 0, 0, 0);
paper.canvas.style.visibility = 'hidden';
el = paper.text(0, 0, string);
el.attr("font-family", "Arial");
el.attr("font-size", fontSize);
bBox = el.getBBox();
paper.remove();
arrayTest = [];
arrayTest.push(bBox.width);
arrayTest.push(bBox.height);
paper.remove();
return arrayTest;
};

View File

@ -1,145 +0,0 @@
# A rectangle in the whiteboard
class @WhiteboardRectModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: x1, y1, x2, y2, stroke color, thickness
@definition = [0, 0, 0, 0, "#000", "0px"]
@paper
# 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) =>
x = startingData.points[0]
y = startingData.points[1]
color = startingData.color
thickness = startingData.thickness
@obj = @paper.rect(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0, 1)
@obj.attr "stroke", formatColor(color)
@obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
@definition =
shape: "rect"
data: [x, y, 0, 0, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@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) ->
x1 = startingData.points[0]
y1 = startingData.points[1]
x2 = startingData.points[2]
y2 = startingData.points[3]
square = startingData.square
if @obj?
[x1, x2] = [x2, x1] if x2 < x1
if y2 < y1
[y1, y2] = [y2, y1]
reversed = true
if square
if reversed #if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * @gw / @gh
else
y2 = y1 + (x2 - x1) * @gw / @gh
x = x1 * @gw + @xOffset
y = y1 * @gh + @yOffset
width = (x2 * @gw + @xOffset) - x
height = (y2 * @gh + @yOffset) - y
#if !square
@obj.attr
x: x
y: y
width: width
height: height
###else
@obj.attr
x: x
y: y
width: width
height: width###
# we need to update all these values, specially for when shapes are drawn backwards
@definition.data[0] = x1
@definition.data[1] = y1
@definition.data[2] = x2
@definition.data[3] = y2
# 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) ->
[x1, x2] = [x2, x1] if x2 < x1
[y1, y2] = [y2, y1] if y2 < y1
x = x1 * @gw
y = y1 * @gh
r = @paper.rect(x + @xOffset, y + @yOffset, (x2 * @gw) - x, (y2 * @gh) - y, 1)
r.attr Meteor.call("strokeAndThickness", colour, thickness)
r
# Creating a rectangle has started
# @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
# 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

View File

@ -0,0 +1,174 @@
const bind = function(fn, me) {
return function() {
return fn.apply(me, arguments);
};
};
// A rectangle in the whiteboard
this.WhiteboardRectModel = (function() {
class WhiteboardRectModel extends WhiteboardToolModel{
constructor(paper) {
super(paper);
this.paper = paper;
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"];
}
// 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) {
let color, thickness, x, y;
x = startingData.points[0];
y = startingData.points[1];
color = startingData.color;
thickness = startingData.thickness;
this.obj = this.paper.rect(x * this.gw + this.xOffset, y * this.gh + this.yOffset, 0, 0, 1);
this.obj.attr("stroke", formatColor(color));
this.obj.attr("stroke-width", zoomStroke(formatThickness(thickness)));
this.definition = {
shape: "rect",
data: [x, y, 0, 0, this.obj.attrs["stroke"], this.obj.attrs["stroke-width"]]
};
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) {
let height, ref, ref1, reversed, square, width, x, x1, x2, y, y1, y2;
x1 = startingData.points[0];
y1 = startingData.points[1];
x2 = startingData.points[2];
y2 = startingData.points[3];
square = startingData.square;
if(this.obj != null) {
if(x2 < x1) {
ref = [x2, x1], x1 = ref[0], x2 = ref[1];
}
if(y2 < y1) {
ref1 = [y2, y1], y1 = ref1[0], y2 = ref1[1];
reversed = true;
}
if(square) {
if(reversed) { //if reveresed, the y1 coordinate gets updated, not the y2 coordinate
y1 = y2 - (x2 - x1) * this.gw / this.gh;
} else {
y2 = y1 + (x2 - x1) * this.gw / this.gh;
}
}
x = x1 * this.gw + this.xOffset;
y = y1 * this.gh + this.yOffset;
width = (x2 * this.gw + this.xOffset) - x;
height = (y2 * this.gh + this.yOffset) - y;
//if !square
this.obj.attr({
x: x,
y: y,
width: width,
height: height
});
/*else
@obj.attr
x: x
y: y
width: 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[1] = y1;
this.definition.data[2] = x2;
return this.definition.data[3] = y2;
}
}
// 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) {
let r, ref, ref1, x, y;
if(x2 < x1) {
ref = [x2, x1], x1 = ref[0], x2 = ref[1];
}
if(y2 < y1) {
ref1 = [y2, y1], y1 = ref1[0], y2 = ref1[1];
}
x = x1 * this.gw;
y = y1 * this.gh;
r = this.paper.rect(x + this.xOffset, y + this.yOffset, (x2 * this.gw) - x, (y2 * this.gh) - y, 1);
r.attr(Meteor.call("strokeAndThickness", colour, thickness));
return r;
}
// Creating a rectangle has started
// @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
// 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;
})();

View File

@ -1,21 +0,0 @@
# A slide in the whiteboard
class @WhiteboardSlideModel
# TODO: check if we really need original and display width and heights separate or if they can be the same
constructor: (@id, @url, @img, @originalWidth, @originalHeight, @displayWidth, @displayHeight, @xOffset=0, @yOffset=0) ->
getWidth: -> @displayWidth
getHeight: -> @displayHeight
getOriginalWidth: -> @originalWidth
getOriginalHeight: -> @originalHeight
getId: -> @id
getDimensions: -> [@getWidth(), @getHeight()]
getOriginalDimensions: -> [@getOriginalWidth(), @getOriginalHeight()]
getOffsets: -> [@xOffset, @yOffset]

View File

@ -0,0 +1,61 @@
// A slide in the whiteboard
this.WhiteboardSlideModel = (function() {
class WhiteboardSlideModel {
// TODO: check if we really need original and display width and heights separate or if they can be the same
constructor(
id,
url,
img,
originalWidth,
originalHeight,
displayWidth,
displayHeight,
xOffset,
yOffset) {
this.id = id;
this.url = url;
this.img = img;
this.originalWidth = originalWidth;
this.originalHeight = originalHeight;
this.displayWidth = displayWidth;
this.displayHeight = displayHeight;
this.xOffset = xOffset != null ? xOffset : 0;
this.yOffset = yOffset != null ? yOffset : 0;
}
getWidth() {
return this.displayWidth;
}
getHeight() {
return this.displayHeight;
}
getOriginalWidth() {
return this.originalWidth;
}
getOriginalHeight() {
return this.originalHeight;
}
getId() {
return this.id;
}
getDimensions() {
return [this.getWidth(), this.getHeight()];
}
getOriginalDimensions() {
return [this.getOriginalWidth(), this.getOriginalHeight()];
}
getOffsets() {
return [this.xOffset, this.yOffset];
}
}
return WhiteboardSlideModel;
})();

View File

@ -1,207 +0,0 @@
# A text in the whiteboard
class @WhiteboardTextModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: x, y, width, height, colour, fontSize, calcFontSize, text
@definition = [0, 0, 0, 0, "#000", 0, 0, ""]
# Make a text on the whiteboard
make: (startingData) ->
#console.log "making text:" + JSON.stringify startingData
x = startingData.x
y = startingData.y
width = startingData.textBoxWidth
height = startingData.textBoxHeight
colour = formatColor(startingData.fontColor)
fontSize = startingData.fontSize
calcFontSize = startingData.calcedFontSize
text = startingData.text
@definition =
shape: "text"
data: [x, y, width, height, colour, fontSize, calcFontSize, text]
#calcFontSize = (calcFontSize/100 * @gh)
x = (x * @gw) + @xOffset
y = (y * @gh) + @yOffset + calcFontSize
width = width/100 * @gw
@obj = @paper.text(x/100, y/100, "")
@obj.attr
"fill": colour
"font-family": "Arial" # TODO: make dynamic
"font-size": calcFontSize
@obj.node.style["text-anchor"] = "start" # force left align
@obj.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different
@obj
# Update text shape drawn
# @param {object} the object containing the shape info
update: (startingData) ->
#console.log "updating text" + JSON.stringify startingData
x = startingData.x
y = startingData.y
maxWidth = startingData.textBoxWidth
height = startingData.textBoxHeight
colour = formatColor(startingData.fontColor)
fontSize = startingData.fontSize
calcFontSize = startingData.calcedFontSize
myText = startingData.text
if @obj?
@definition.data = [x, y, maxWidth, height, colour, fontSize, calcFontSize, myText]
calcFontSize = (calcFontSize/100 * @gh)
x = (x * @gw)/100 + @xOffset
maxWidth = maxWidth/100 * @gw
@obj.attr
"fill": colour
"font-family": "Arial" # TODO: make dynamic
"font-size": calcFontSize
cell = @obj.node
while cell? and cell.hasChildNodes()
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()
dashFound = true
indexPos = 0
cumulY = 0
svgNS = "http://www.w3.org/2000/svg"
while dashFound is true
result = myText.indexOf("-", indexPos)
if result is -1
#could not find a dash
dashFound = false
else
dashArray.push result
indexPos = result + 1
#split the text at all spaces and dashes
words = myText.split(/[\s-]/)
line = ""
dy = 0
curNumChars = 0
computedTextLength = 0
myTextNode = undefined
tspanEl = undefined
i = 0
#checking if any of the words exceed the width of a textBox
words = checkWidth(words, maxWidth, x, dy, cell)
while i < words.length
word = words[i]
curNumChars += word.length + 1
if computedTextLength > maxWidth or i is 0
if computedTextLength > maxWidth
tempText = tspanEl.firstChild.nodeValue
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
#setting up coordinates for the first line of text
if i is 0
dy = calcFontSize
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.setAttributeNS null, "x", x
tspanEl.setAttributeNS null, "dy", dy
myTextNode = document.createTextNode(line)
tspanEl.appendChild myTextNode
cell.appendChild tspanEl
if checkDashPosition(dashArray, curNumChars - 1)
line = word + "-"
else
line = word + " "
line = words[i - 1] + " " + line unless i is 0
dy = calcFontSize
cumulY += dy
else
if checkDashPosition(dashArray, curNumChars - 1)
line += word + "-"
else
line += word + " "
tspanEl.firstChild.nodeValue = line
computedTextLength = tspanEl.getComputedTextLength()+10
if i is words.length - 1
if computedTextLength > maxWidth
tempText = tspanEl.firstChild.nodeValue
tspanEl.firstChild.nodeValue = tempText.slice(0, (tempText.length - words[i].length - 1))
tspanEl = document.createElementNS(svgNS, "tspan")
tspanEl.setAttributeNS null, "x", x
tspanEl.setAttributeNS null, "dy", dy
myTextNode = document.createTextNode(words[i])
tspanEl.appendChild myTextNode
cell.appendChild tspanEl
i++
cumulY
#this function checks if there should be a dash at the given position, instead of a blank
checkDashPosition = (dashArray, pos) ->
result = false
i = 0
while i < dashArray.length
result = true if dashArray[i] is pos
i++
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 = (words, maxWidth, x, dy, cell) ->
count = 0
temp = words
temp3 = []
str = ""
svgNSi = "http://www.w3.org/2000/svg"
tempSpanEl = document.createElementNS(svgNSi, "tspan")
tempSpanEl.setAttributeNS null, "x", x
tempSpanEl.setAttributeNS null, "dy", dy
tempTextNode = document.createTextNode(str)
tempSpanEl.appendChild tempTextNode
num = 0
while num < temp.length
#creating a textNode and adding it to the cell to check the width
tempSpanEl.firstChild.nodeValue = temp[num]
cell.appendChild tempSpanEl
#if width is bigger than maxWidth + whitespace between textBox borders and a word
if tempSpanEl.getComputedTextLength()+10 > maxWidth
tempWord = temp[num]
cell.removeChild(cell.firstChild)
#initializing temp variables
count = 1
start = 0
partWord = "" + tempWord[0]
tempArray = []
#check the width by increasing the word character by character
while count < tempWord.length
partWord += tempWord[count]
tempSpanEl.firstChild.nodeValue = partWord
cell.appendChild tempSpanEl
if tempSpanEl.getComputedTextLength()+10 > maxWidth
temp3.push partWord.substring(0, partWord.length-1)
partWord = ""
partWord += tempWord[count]
if count is tempWord.length-1
temp3.push partWord
while cell? and cell.hasChildNodes()
cell.removeChild(cell.firstChild)
count++
else
temp3.push temp[num]
while cell? and cell.hasChildNodes()
cell.removeChild(cell.firstChild)
num++
while cell? and cell.hasChildNodes()
cell.removeChild(cell.firstChild)
temp3

View File

@ -0,0 +1,241 @@
// A text in the whiteboard
this.WhiteboardTextModel = (function() {
let checkDashPosition, checkWidth;
class WhiteboardTextModel extends WhiteboardToolModel {
constructor(paper) {
super(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, ""];
}
// Make a text on the whiteboard
make(startingData) {
//console.log "making text:" + JSON.stringify startingData
let calcFontSize, colour, fontSize, height, text, width, x, y;
x = startingData.x;
y = startingData.y;
width = startingData.textBoxWidth;
height = startingData.textBoxHeight;
colour = formatColor(startingData.fontColor);
fontSize = startingData.fontSize;
calcFontSize = startingData.calcedFontSize;
text = startingData.text;
this.definition = {
shape: "text",
data: [x, y, width, height, colour, fontSize, calcFontSize, text]
};
//calcFontSize = (calcFontSize/100 * @gh)
x = (x * this.gw) + this.xOffset;
y = (y * this.gh) + this.yOffset + calcFontSize;
width = width / 100 * this.gw;
this.obj = this.paper.text(x / 100, y / 100, "");
this.obj.attr({
"fill": colour,
"font-family": "Arial", // TODO: make dynamic
"font-size": calcFontSize
});
this.obj.node.style["text-anchor"] = "start"; // force left align
this.obj.node.style["textAnchor"] = "start"; // for firefox, 'cause they like to be different
return this.obj;
}
// Update text shape drawn
// @param {object} the object containing the shape info
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;
x = startingData.x;
y = startingData.y;
maxWidth = startingData.textBoxWidth;
height = startingData.textBoxHeight;
colour = formatColor(startingData.fontColor);
fontSize = startingData.fontSize;
calcFontSize = startingData.calcedFontSize;
myText = startingData.text;
if(this.obj != null) {
this.definition.data = [x, y, maxWidth, height, colour, fontSize, calcFontSize, myText];
calcFontSize = calcFontSize / 100 * this.gh;
x = (x * this.gw) / 100 + this.xOffset;
maxWidth = maxWidth / 100 * this.gw;
this.obj.attr({
"fill": colour,
"font-family": "Arial", // TODO: make dynamic
"font-size": calcFontSize
});
cell = this.obj.node;
while((cell != null) && cell.hasChildNodes()) {
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();
dashFound = true;
indexPos = 0;
cumulY = 0;
svgNS = "http://www.w3.org/2000/svg";
while(dashFound === true) {
result = myText.indexOf("-", indexPos);
if(result === -1) {
//could not find a dash
dashFound = false;
} else {
dashArray.push(result);
indexPos = result + 1;
}
}
//split the text at all spaces and dashes
words = myText.split(/[\s-]/);
line = "";
dy = 0;
curNumChars = 0;
computedTextLength = 0;
myTextNode = void 0;
tspanEl = void 0;
i = 0;
//checking if any of the words exceed the width of a textBox
words = checkWidth(words, maxWidth, x, dy, cell);
while(i < words.length) {
word = words[i];
curNumChars += word.length + 1;
if(computedTextLength > maxWidth || i === 0) {
if(computedTextLength > maxWidth) {
tempText = tspanEl.firstChild.nodeValue;
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;
}
//setting up coordinates for the first line of text
if(i === 0) {
dy = calcFontSize;
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.setAttributeNS(null, "x", x);
tspanEl.setAttributeNS(null, "dy", dy);
myTextNode = document.createTextNode(line);
tspanEl.appendChild(myTextNode);
cell.appendChild(tspanEl);
if(checkDashPosition(dashArray, curNumChars - 1)) {
line = `${word}-`;
} else {
line = `${word} `;
}
if(i !== 0) {
line = `${words[i - 1]} ${line}`;
}
dy = calcFontSize;
cumulY += dy;
} else {
if(checkDashPosition(dashArray, curNumChars - 1)) {
line += `${word}-`;
} else {
line += `${word} `;
}
}
tspanEl.firstChild.nodeValue = line;
computedTextLength = tspanEl.getComputedTextLength() + 10;
if(i === words.length - 1) {
if(computedTextLength > maxWidth) {
tempText = tspanEl.firstChild.nodeValue;
tspanEl.firstChild.nodeValue = tempText.slice(0, tempText.length - words[i].length - 1);
tspanEl = document.createElementNS(svgNS, "tspan");
tspanEl.setAttributeNS(null, "x", x);
tspanEl.setAttributeNS(null, "dy", dy);
myTextNode = document.createTextNode(words[i]);
tspanEl.appendChild(myTextNode);
cell.appendChild(tspanEl);
}
}
i++;
}
return cumulY;
}
}
}
//this function checks if there should be a dash at the given position, instead of a blank
checkDashPosition = function(dashArray, pos) {
let i, result;
result = false;
i = 0;
while(i < dashArray.length) {
if(dashArray[i] === pos) {
result = true;
}
i++;
}
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) {
let count, num, partWord, start, str, svgNSi, temp, temp3, tempArray, tempSpanEl, tempTextNode, tempWord;
count = 0;
temp = words;
temp3 = [];
str = "";
svgNSi = "http://www.w3.org/2000/svg";
tempSpanEl = document.createElementNS(svgNSi, "tspan");
tempSpanEl.setAttributeNS(null, "x", x);
tempSpanEl.setAttributeNS(null, "dy", dy);
tempTextNode = document.createTextNode(str);
tempSpanEl.appendChild(tempTextNode);
num = 0;
//creating a textNode and adding it to the cell to check the width
while(num < temp.length) {
tempSpanEl.firstChild.nodeValue = temp[num];
cell.appendChild(tempSpanEl);
//if width is bigger than maxWidth + whitespace between textBox borders and a word
if(tempSpanEl.getComputedTextLength() + 10 > maxWidth) {
tempWord = temp[num];
cell.removeChild(cell.firstChild);
//initializing temp variables
count = 1;
start = 0;
partWord = `${tempWord[0]}`;
tempArray = [];
//check the width by increasing the word character by character
while(count < tempWord.length) {
partWord += tempWord[count];
tempSpanEl.firstChild.nodeValue = partWord;
cell.appendChild(tempSpanEl);
if(tempSpanEl.getComputedTextLength() + 10 > maxWidth) {
temp3.push(partWord.substring(0, partWord.length - 1));
partWord = "";
partWord += tempWord[count];
}
if(count === tempWord.length - 1) {
temp3.push(partWord);
}
while((cell != null) && cell.hasChildNodes()) {
cell.removeChild(cell.firstChild);
}
count++;
}
} else {
temp3.push(temp[num]);
}
while((cell != null) && cell.hasChildNodes()) {
cell.removeChild(cell.firstChild);
}
num++;
}
while((cell != null) && cell.hasChildNodes()) {
cell.removeChild(cell.firstChild);
}
return temp3;
};
return WhiteboardTextModel;
})();

View File

@ -1,104 +0,0 @@
# A triangle in the whiteboard
class @WhiteboardTriangleModel extends WhiteboardToolModel
constructor: (@paper) ->
super @paper
# the defintion of this shape, kept so we can redraw the shape whenever needed
# format: x1, y1, x2, y2, stroke color, thickness
@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) ->
if info?.points?
x = info.points[0]
y = info.points[1]
color = info.color
thickness = info.thickness
path = @_buildPath(x, y, x, y, x, y)
@obj = @paper.path(path)
@obj.attr "stroke", formatColor(color)
@obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
@obj.attr({"stroke-linejoin": "round"})
@definition = [x, y, x, y, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
@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) ->
if info?.points?
x1 = info.points[0]
y1 = info.points[1]
x2 = info.points[2]
y2 = info.points[3]
if @obj?
[xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2)
path = @_buildPath(xTop * @gw + @xOffset, yTop * @gh + @yOffset,
xBottomLeft * @gw + @xOffset, yBottomLeft * @gh + @yOffset,
xBottomRight * @gw + @xOffset, yBottomRight * @gh + @yOffset)
@obj.attr path: path
@definition[0] = x1
@definition[1] = y1
@definition[2] = x2
@definition[3] = y2
# 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) ->
[xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2)
path = @_buildPath(xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight)
path = @_scaleTrianglePath(path, @gw, @gh, @xOffset, @yOffset)
triangle = @paper.path(path)
triangle.attr Utils.strokeAndThickness(colour, thickness)
triangle.attr({"stroke-linejoin": "round"})
triangle
_getCornersFromPoints: (x1, y1, x2, y2) ->
xTop = (((x2 - x1) / 2) + x1)
yTop = y1
xBottomLeft = x1
yBottomLeft = y2
xBottomRight = x2
yBottomRight = y2
[xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight]
_buildPath: (xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight) ->
"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=0, yOffset=0) ->
path = null
points = string.match(/(\d+[.]?\d*)/g)
len = points.length
j = 0
# go through each point and multiply it by the new height and width
path = "M"
while j < len
path += "," unless j is 0
path += "" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
j += 2
path + "z"
WhiteboardTriangleModel

View File

@ -0,0 +1,129 @@
// A triangle in the whiteboard
this.WhiteboardTriangleModel = (function() {
class WhiteboardTriangleModel extends WhiteboardToolModel {
constructor(paper) {
super(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"];
}
// 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) {
let color, path, thickness, x, y;
if((info != null ? info.points : void 0) != null) {
x = info.points[0];
y = info.points[1];
color = info.color;
thickness = info.thickness;
path = this._buildPath(x, y, x, y, x, y);
this.obj = this.paper.path(path);
this.obj.attr("stroke", formatColor(color));
this.obj.attr("stroke-width", zoomStroke(formatThickness(thickness)));
this.obj.attr({
"stroke-linejoin": "round"
});
this.definition = [x, y, x, y, this.obj.attrs["stroke"], this.obj.attrs["stroke-width"]];
}
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) {
let path, ref, x1, x2, xBottomLeft, xBottomRight, xTop, y1, y2, yBottomLeft, yBottomRight, yTop;
if((info != null ? info.points : void 0) != null) {
x1 = info.points[0];
y1 = info.points[1];
x2 = info.points[2];
y2 = info.points[3];
if (this.obj != null) {
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];
path = this._buildPath(xTop * this.gw + this.xOffset, yTop * this.gh + this.yOffset, xBottomLeft * this.gw + this.xOffset, yBottomLeft * this.gh + this.yOffset, xBottomRight * this.gw + this.xOffset, yBottomRight * this.gh + this.yOffset);
this.obj.attr({
path: path
});
this.definition[0] = x1;
this.definition[1] = y1;
this.definition[2] = x2;
return this.definition[3] = y2;
}
}
}
// 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) {
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];
path = this._buildPath(xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight);
path = this._scaleTrianglePath(path, this.gw, this.gh, this.xOffset, this.yOffset);
triangle = this.paper.path(path);
triangle.attr(Utils.strokeAndThickness(colour, thickness));
triangle.attr({
"stroke-linejoin": "round"
});
return triangle;
}
_getCornersFromPoints(x1, y1, x2, y2) {
let xBottomLeft, xBottomRight, xTop, yBottomLeft, yBottomRight, yTop;
xTop = ((x2 - x1) / 2) + x1;
yTop = y1;
xBottomLeft = x1;
yBottomLeft = y2;
xBottomRight = x2;
yBottomRight = y2;
return [xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight];
}
_buildPath(xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight) {
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) {
let j, len, path, points;
if(xOffset == null) {
xOffset = 0;
}
if(yOffset == null) {
yOffset = 0;
}
path = null;
points = string.match(/(\d+[.]?\d*)/g);
len = points.length;
j = 0;
// go through each point and multiply it by the new height and width
path = "M";
while(j < len) {
if(j !== 0) {
path += ",";
}
path += `${points[j + 1] * h}${yOffset}${points[j] * w + xOffset},${points[j + 1] * h + yOffset}`;
j += 2;
}
return `${path}z`;
}
}
return WhiteboardTriangleModel;
})();

View File

@ -1,10 +0,0 @@
Meteor.Users = new Meteor.Collection("bbb_users")
Meteor.Chat = new Meteor.Collection("bbb_chat")
Meteor.Meetings = new Meteor.Collection("meetings")
Meteor.Presentations = new Meteor.Collection("presentations")
Meteor.Cursor = new Meteor.Collection("bbb_cursor")
Meteor.Shapes = new Meteor.Collection("shapes")
Meteor.Slides = new Meteor.Collection("slides")
Meteor.Polls = new Meteor.Collection("bbb_poll")
Meteor.WhiteboardCleanStatus = new Meteor.Collection("whiteboard-clean-status")

View File

@ -0,0 +1,17 @@
Meteor.Users = new Meteor.Collection("bbb_users");
Meteor.Chat = new Meteor.Collection("bbb_chat");
Meteor.Meetings = new Meteor.Collection("meetings");
Meteor.Presentations = new Meteor.Collection("presentations");
Meteor.Cursor = new Meteor.Collection("bbb_cursor");
Meteor.Shapes = new Meteor.Collection("shapes");
Meteor.Slides = new Meteor.Collection("slides");
Meteor.Polls = new Meteor.Collection("bbb_poll");
Meteor.WhiteboardCleanStatus = new Meteor.Collection("whiteboard-clean-status");

View File

@ -1,69 +0,0 @@
# TODO: should be split on server and client side
# # Global configurations file
config = {}
# Default global variables
config.appName = 'BigBlueButton HTML5 Client'
config.bbbServerVersion = '1.0-beta'
config.copyrightYear = '2015'
config.html5ClientBuild = 'NNNN'
config.defaultWelcomeMessage = 'Welcome to %%CONFNAME%%!\r\rFor help on using BigBlueButton see these (short) <a href="event:http://www.bigbluebutton.org/content/videos"><u>tutorial videos</u></a>.\r\rTo join the audio bridge click the gear icon (upper-right hand corner). Use a headset to avoid causing background noise for others.\r\r\r'
config.defaultWelcomeMessageFooter = "This server is running a build of <a href='http://docs.bigbluebutton.org/1.0/10overview.html' target='_blank'><u>BigBlueButton #{config.bbbServerVersion}</u></a>."
config.maxUsernameLength = 30
config.maxChatLength = 140
config.lockOnJoin = true
## Application configurations
config.app = {}
#default font sizes for mobile / desktop
config.app.mobileFont=16
config.app.desktopFont=14
# Will offer the user to join the audio when entering the meeting
config.app.autoJoinAudio = false
config.app.listenOnly = 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
# Configs for redis
config.redis = {}
config.redis.host = "127.0.0.1"
config.redis.post = "6379"
config.redis.timeout = 5000
config.redis.channels = {}
config.redis.channels.fromBBBApps = "bigbluebutton:from-bbb-apps:*"
config.redis.channels.toBBBApps = {}
config.redis.channels.toBBBApps.pattern = "bigbluebutton:to-bbb-apps:*"
config.redis.channels.toBBBApps.chat = "bigbluebutton:to-bbb-apps:chat"
config.redis.channels.toBBBApps.meeting = "bigbluebutton:to-bbb-apps:meeting"
config.redis.channels.toBBBApps.presentation = "bigbluebutton:to-bbb-apps:presentation"
config.redis.channels.toBBBApps.users = "bigbluebutton:to-bbb-apps:users"
config.redis.channels.toBBBApps.voice = "bigbluebutton:to-bbb-apps:voice"
config.redis.channels.toBBBApps.whiteboard = "bigbluebutton:to-bbb-apps:whiteboard"
config.redis.channels.toBBBApps.polling = "bigbluebutton:to-bbb-apps:polling"
# Logging
config.log = {}
if Meteor.isServer
config.log.path = if process?.env?.NODE_ENV is "production"
"/var/log/bigbluebutton/bbbnode.log"
else
# logs in the directory immediatly before the meteor app
process.env.PWD + '/../log/development.log'
# Setting up a logger in Meteor.log
winston = Winston #Meteor.require 'winston'
file = config.log.path
transports = [ new winston.transports.Console(), new winston.transports.File { filename: file } ]
Meteor.log = new winston.Logger
transports: transports
Meteor.config = config

101
bigbluebutton-html5/app/config.js Executable file
View File

@ -0,0 +1,101 @@
// TODO: should be split on server and client side
// // Global configurations file
let config, file, ref, transports, winston;
config = {};
// Default global variables
config.appName = 'BigBlueButton HTML5 Client';
config.bbbServerVersion = '1.0-beta';
config.copyrightYear = '2015';
config.html5ClientBuild = 'NNNN';
config.defaultWelcomeMessage = 'Welcome to %%CONFNAME%%!\r\rFor help on using BigBlueButton see these (short) <a href="event:http://www.bigbluebutton.org/content/videos"><u>tutorial videos</u></a>.\r\rTo join the audio bridge click the gear icon (upper-right hand corner). Use a headset to avoid causing background noise for others.\r\r\r';
config.defaultWelcomeMessageFooter = `This server is running a build of <a href='http://docs.bigbluebutton.org/1.0/10overview.html' target='_blank'><u>BigBlueButton ${config.bbbServerVersion}</u></a>.`;
config.maxUsernameLength = 30;
config.maxChatLength = 140;
config.lockOnJoin = true;
//// Application configurations
config.app = {};
//default font sizes for mobile / desktop
config.app.mobileFont = 16;
config.app.desktopFont = 14;
// Will offer the user to join the audio when entering the meeting
config.app.autoJoinAudio = false;
config.app.listenOnly = 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;
// Configs for redis
config.redis = {};
config.redis.host = "127.0.0.1";
config.redis.post = "6379";
config.redis.timeout = 5000;
config.redis.channels = {};
config.redis.channels.fromBBBApps = "bigbluebutton:from-bbb-apps:*";
config.redis.channels.toBBBApps = {};
config.redis.channels.toBBBApps.pattern = "bigbluebutton:to-bbb-apps:*";
config.redis.channels.toBBBApps.chat = "bigbluebutton:to-bbb-apps:chat";
config.redis.channels.toBBBApps.meeting = "bigbluebutton:to-bbb-apps:meeting";
config.redis.channels.toBBBApps.presentation = "bigbluebutton:to-bbb-apps:presentation";
config.redis.channels.toBBBApps.users = "bigbluebutton:to-bbb-apps:users";
config.redis.channels.toBBBApps.voice = "bigbluebutton:to-bbb-apps:voice";
config.redis.channels.toBBBApps.whiteboard = "bigbluebutton:to-bbb-apps:whiteboard";
config.redis.channels.toBBBApps.polling = "bigbluebutton:to-bbb-apps:polling";
// Logging
config.log = {};
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`;
// Setting up a logger in Meteor.log
winston = Winston; //Meteor.require 'winston'
file = config.log.path;
transports = [
new winston.transports.Console(), new winston.transports.File({
filename: file
})
];
Meteor.log = new winston.Logger({
transports: transports
});
}
Meteor.config = config;

View File

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

View File

@ -0,0 +1,8 @@
// used in Flash and HTML to show a legitimate break in the line
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';
// handle this the same as carriage return, in case text copied has this
this.NEW_LINE = '\n';

View File

@ -1,103 +0,0 @@
@Router.configure layoutTemplate: 'layout'
@Router.map ->
# this is how we handle login attempts
@route "main",
path: "/html5client/:meeting_id/:user_id/:auth_token"
where: "client"
onBeforeAction: ->
meetingId = @params.meeting_id
userId = @params.user_id
authToken = @params.auth_token
setInSession("loginUrl", @originalUrl)
# catch if any of the user's meeting data is invalid
if not authToken? or not meetingId? or not userId?
# if their data is invalid, redirect the user to the logout page
document.location = getInSession 'logoutURL'
else
Meteor.call("validateAuthToken", meetingId, userId, authToken)
applyNewSessionVars = ->
setInSession("authToken", authToken)
setInSession("meetingId", meetingId)
setInSession("userId", userId)
Router.go('/html5client')
clearSessionVar(applyNewSessionVars)
@next()
# the user successfully logged in
@route "signedin",
path: "/html5client"
where: "client"
action: ->
meetingId = getInSession "meetingId"
userId = getInSession "userId"
authToken = getInSession "authToken"
onErrorFunction = (error, result) ->
console.log "ONERRORFUNCTION"
#make sure the user is not let through
Meteor.call("userLogout", meetingId, userId, authToken)
clearSessionVar()
# Attempt to log back in
unless error?
window.location.href = getInSession('loginUrl') or getInSession('logoutURL')
return
Meteor.subscribe 'chat', meetingId, userId, authToken, onError: onErrorFunction, onReady: =>
Meteor.subscribe 'shapes', meetingId, onReady: =>
Meteor.subscribe 'slides', meetingId, onReady: =>
Meteor.subscribe 'meetings', meetingId, onReady: =>
Meteor.subscribe 'presentations', meetingId, onReady: =>
Meteor.subscribe 'users', meetingId, userId, authToken, onError: onErrorFunction, onReady: =>
Meteor.subscribe 'whiteboard-clean-status', meetingId, onReady: =>
Meteor.subscribe 'bbb_poll', meetingId, userId, authToken, onReady: =>
Meteor.subscribe 'bbb_cursor', meetingId, onReady: =>
# done subscribing, start rendering the client and set default settings
@render('main')
onLoadComplete()
handleLogourUrlError = () ->
alert "Error: could not find the logoutURL"
setInSession("logoutURL", document.location.hostname)
return
# obtain the logoutURL
a = $.ajax({dataType: 'json', url: '/bigbluebutton/api/enter'})
a.done (data) ->
if data.response.logoutURL? # for a meeting with 0 users
setInSession("logoutURL", data.response.logoutURL)
return
else
if data.response.logoutUrl? # for a running meeting
setInSession("logoutURL", data.response.logoutUrl)
return
else
handleLogourUrlError()
a.fail (data, textStatus, errorThrown) ->
handleLogourUrlError()
@render('loading')
# endpoint - is the html5client running (ready to handle a user)
@route 'meteorEndpoint',
path: '/check'
where: 'server'
action: ->
@response.writeHead 200, 'Content-Type': 'application/json'
# reply that the html5client is running
@response.end JSON.stringify {"html5clientStatus":"running"}
return
return

View File

@ -0,0 +1,144 @@
this.Router.configure({
layoutTemplate: 'layout'
});
this.Router.map(function() {
// this is how we handle login attempts
this.route("main", {
path: "/html5client/:meeting_id/:user_id/:auth_token",
where: "client",
onBeforeAction() {
let applyNewSessionVars, authToken, meetingId, userId;
meetingId = this.params.meeting_id;
userId = this.params.user_id;
authToken = this.params.auth_token;
setInSession("loginUrl", this.originalUrl);
// catch if any of the user's meeting data is invalid
if ((authToken == null) || (meetingId == null) || (userId == null)) {
// if their data is invalid, redirect the user to the logout page
document.location = getInSession('logoutURL');
} else {
Meteor.call("validateAuthToken", meetingId, userId, authToken);
applyNewSessionVars = function() {
setInSession("authToken", authToken);
setInSession("meetingId", meetingId);
setInSession("userId", userId);
return Router.go('/html5client');
};
clearSessionVar(applyNewSessionVars);
}
return this.next();
}
});
// the user successfully logged in
this.route("signedin", {
path: "/html5client",
where: "client",
action() {
let authToken, meetingId, onErrorFunction, userId;
meetingId = getInSession("meetingId");
userId = getInSession("userId");
authToken = getInSession("authToken");
onErrorFunction = function(error, result) {
console.log("ONERRORFUNCTION");
//make sure the user is not let through
Meteor.call("userLogout", meetingId, userId, authToken);
clearSessionVar();
// Attempt to log back in
if (error == null) {
window.location.href = getInSession('loginUrl') || getInSession('logoutURL');
}
};
Meteor.subscribe('chat', meetingId, userId, authToken, {
onError: onErrorFunction,
onReady: (_this => {
return function() {
return Meteor.subscribe('shapes', meetingId, {
onReady: function() {
return Meteor.subscribe('slides', meetingId, {
onReady: function() {
return Meteor.subscribe('meetings', meetingId, {
onReady: function() {
return Meteor.subscribe('presentations', meetingId, {
onReady: function() {
return Meteor.subscribe('users', meetingId, userId, authToken, {
onError: onErrorFunction,
onReady: function() {
return Meteor.subscribe('whiteboard-clean-status', meetingId, {
onReady: function() {
return Meteor.subscribe('bbb_poll', meetingId, userId, authToken, {
onReady: function() {
return Meteor.subscribe('bbb_cursor', meetingId, {
onReady: function() {
let a, handleLogourUrlError;
// done subscribing, start rendering the client and set default settings
_this.render('main');
onLoadComplete();
handleLogourUrlError = function() {
alert("Error: could not find the logoutURL");
setInSession("logoutURL", document.location.hostname);
};
// obtain the logoutURL
a = $.ajax({
dataType: 'json',
url: '/bigbluebutton/api/enter'
});
a.done(data => {
if (data.response.logoutURL != null) { // for a meeting with 0 users
setInSession("logoutURL", data.response.logoutURL);
} else {
if (data.response.logoutUrl != null) { // for a running meeting
setInSession("logoutURL", data.response.logoutUrl);
} else {
return handleLogourUrlError();
}
}
});
return a.fail((data, textStatus, errorThrown) => {
return handleLogourUrlError();
});
}
});
}
});
}
});
}
});
}
});
}
});
}
});
}
});
};
})(this)
});
return this.render('loading');
}
});
// endpoint - is the html5client running (ready to handle a user)
this.route('meteorEndpoint', {
path: '/check',
where: 'server',
action() {
this.response.writeHead(200, {
'Content-Type': 'application/json'
});
// reply that the html5client is running
this.response.end(JSON.stringify({
"html5clientStatus": "running"
}));
}
});
});

View File

@ -1,17 +1,19 @@
###
bunyan = Meteor.require 'bunyan'
logger = bunyan.createLogger({
name: 'bbbnode',
streams: [
{
level: 'debug',
stream: process.stdout,
},
{
level: 'info',
path: Meteor.config.log.path
}
]
})
###
// Generated by CoffeeScript 1.10.0
/*
bunyan = Meteor.require 'bunyan'
logger = bunyan.createLogger({
name: 'bbbnode',
streams: [
{
level: 'debug',
stream: process.stdout,
},
{
level: 'info',
path: Meteor.config.log.path
}
]
})
*/

View File

@ -1,99 +0,0 @@
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) ->
chatType = chatObject.chat_type
recipient = chatObject.to_userid
eventName = null
action = ->
if chatType is "PUBLIC_CHAT"
eventName = "send_public_chat_message"
return 'chatPublic'
else
eventName = "send_private_chat_message"
if recipient is requesterUserId
return 'chatSelf' #not allowed
else
return 'chatPrivate'
if isAllowedTo(action(), meetingId, requesterUserId, requesterToken) and chatObject.from_userid is requesterUserId
chatObject.message = translateHTML5ToFlash(chatObject.message)
message =
header :
timestamp: new Date().getTime()
name: eventName
payload:
message: chatObject
meeting_id: meetingId
requester_id: chatObject.from_userid
Meteor.log.info "publishing chat to redis"
publish Meteor.config.redis.channels.toBBBApps.chat, message
return
deletePrivateChatMessages: (userId, contact_id) ->
# if authorized pass through
requester = Meteor.Users.findOne({userId: userId})
contact = Meteor.Users.findOne({_id: contact_id})
deletePrivateChatMessages(requester.userId, contact.userId)
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@addChatToCollection = (meetingId, messageObject) ->
# 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]
if messageObject.from_userid? and messageObject.to_userid?
messageObject.message = translateFlashToHTML5(messageObject.message)
id = Meteor.Chat.upsert({
meetingId:meetingId
'message.message': messageObject.message
'message.from_time': messageObject.from_time
'message.from_userid': messageObject.from_userid
}, {
meetingId: meetingId
message:
chat_type: messageObject.chat_type
message: messageObject.message
to_username: messageObject.to_username
from_tz_offset: messageObject.from_tz_offset
from_color: messageObject.from_color
to_userid: messageObject.to_userid
from_userid: messageObject.from_userid
from_time: messageObject.from_time
from_username: messageObject.from_username
from_lang: messageObject.from_lang
}, (err, numChanged) ->
if err?
Meteor.log.error "_error #{err} when adding chat to collection"
if numChanged.insertedId?
Meteor.log.info "_added chat id=[#{numChanged.insertedId}]
#{messageObject.from_username} to #{'PUBLIC' if messageObject.to_username?}:#{messageObject.message}")
# called on server start and meeting end
@clearChatCollection = (meetingId) ->
if meetingId?
Meteor.Chat.remove({meetingId: meetingId}, Meteor.log.info "cleared Chat Collection (meetingId: #{meetingId}!")
else
Meteor.Chat.remove({}, Meteor.log.info "cleared Chat Collection (all meetings)!")
# --------------------------------------------------------------------------------------------
# end Private methods on server
# --------------------------------------------------------------------------------------------
# translate '\n' newline character and '\r' carriage returns to '<br/>' breakline character for Flash
@translateHTML5ToFlash = (message) ->
result = message
result = result.replace(new RegExp(CARRIAGE_RETURN, 'g'), BREAK_LINE)
result = result.replace(new RegExp(NEW_LINE, 'g'), BREAK_LINE)
result
# translate '<br/>' breakline character to '\r' carriage return character for HTML5
@translateFlashToHTML5 = (message) ->
result = message
result = result.replace(new RegExp(BREAK_LINE, 'g'), CARRIAGE_RETURN)
result

View File

@ -0,0 +1,124 @@
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) {
let action, chatType, eventName, message, recipient;
chatType = chatObject.chat_type;
recipient = chatObject.to_userid;
eventName = null;
action = function() {
if(chatType === "PUBLIC_CHAT") {
eventName = "send_public_chat_message";
return 'chatPublic';
} else {
eventName = "send_private_chat_message";
if(recipient === requesterUserId) {
return 'chatSelf'; //not allowed
} else {
return 'chatPrivate';
}
}
};
if(isAllowedTo(action(), meetingId, requesterUserId, requesterToken) && chatObject.from_userid === requesterUserId) {
chatObject.message = translateHTML5ToFlash(chatObject.message);
message = {
header: {
timestamp: new Date().getTime(),
name: eventName
},
payload: {
message: chatObject,
meeting_id: meetingId,
requester_id: chatObject.from_userid
}
};
Meteor.log.info("publishing chat to redis");
publish(Meteor.config.redis.channels.toBBBApps.chat, message);
}
},
deletePrivateChatMessages(userId, contact_id) {
// if authorized pass through
let contact, requester;
requester = Meteor.Users.findOne({
userId: userId
});
contact = Meteor.Users.findOne({
_id: contact_id
});
return deletePrivateChatMessages(requester.userId, contact.userId);
}
});
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addChatToCollection = function(meetingId, messageObject) {
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];
if((messageObject.from_userid != null) && (messageObject.to_userid != null)) {
messageObject.message = translateFlashToHTML5(messageObject.message);
return id = Meteor.Chat.upsert({
meetingId: meetingId,
'message.message': messageObject.message,
'message.from_time': messageObject.from_time,
'message.from_userid': messageObject.from_userid
}, {
meetingId: meetingId,
message: {
chat_type: messageObject.chat_type,
message: messageObject.message,
to_username: messageObject.to_username,
from_tz_offset: messageObject.from_tz_offset,
from_color: messageObject.from_color,
to_userid: messageObject.to_userid,
from_userid: messageObject.from_userid,
from_time: messageObject.from_time,
from_username: messageObject.from_username,
from_lang: messageObject.from_lang
}
}, (err, numChanged) => {
if(err != null) {
Meteor.log.error(`_error ${err} when adding chat to collection`);
}
if(numChanged.insertedId != null) {
return Meteor.log.info(`${messageObject.to_username != null}_added chat id=[${numChanged.insertedId}] ${messageObject.from_username} to ${messageObject.to_username != null ? 'PUBLIC' : void 0}:${messageObject.message}`);
}
});
}
};
// called on server start and meeting end
this.clearChatCollection = function(meetingId) {
if(meetingId != null) {
return Meteor.Chat.remove({
meetingId: meetingId
}, Meteor.log.info(`cleared Chat Collection (meetingId: ${meetingId}!`));
} else {
return Meteor.Chat.remove({}, Meteor.log.info("cleared Chat Collection (all meetings)!"));
}
};
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------
// translate '\n' newline character and '\r' carriage returns to '<br/>' breakline character for Flash
this.translateHTML5ToFlash = function(message) {
let result;
result = message;
result = result.replace(new RegExp(CARRIAGE_RETURN, 'g'), BREAK_LINE);
result = result.replace(new RegExp(NEW_LINE, 'g'), BREAK_LINE);
return result;
};
// translate '<br/>' breakline character to '\r' carriage return character for HTML5
this.translateFlashToHTML5 = function(message) {
let result;
result = message;
result = result.replace(new RegExp(BREAK_LINE, 'g'), CARRIAGE_RETURN);
return result;
};

View File

@ -1,39 +0,0 @@
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@initializeCursor = (meetingId) ->
Meteor.Cursor.upsert({meetingId:meetingId}, {
meetingId:meetingId
x:0
y:0
}, (err, numChanged) ->
if err
Meteor.log.error "err upserting cursor for #{meetingId}"
else
# Meteor.log.info "ok upserting cursor for #{meetingId}"
)
@updateCursorLocation = (meetingId, cursorObject) ->
Meteor.Cursor.update({meetingId:meetingId}, {$set:{
x:cursorObject.x
y:cursorObject.y
}}, (err, numChanged) ->
if err?
Meteor.log.error "_unsucc update of cursor for #{meetingId} #{JSON.stringify cursorObject}
err=#{JSON.stringify err}"
else
# Meteor.log.info "updated cursor for #{meetingId} #{JSON.stringify cursorObject}"
)
# called on server start and meeting end
@clearCursorCollection = (meetingId) ->
if meetingId?
Meteor.Cursor.remove {meetingId: meetingId}, ->
Meteor.log.info "cleared Cursor Collection (meetingId: #{meetingId})!"
else
Meteor.Cursor.remove {}, ->
Meteor.log.info "cleared Cursor Collection (all meetings)!"
# --------------------------------------------------------------------------------------------
# end Private methods on server
# --------------------------------------------------------------------------------------------

View File

@ -0,0 +1,54 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.initializeCursor = function(meetingId) {
return Meteor.Cursor.upsert({
meetingId: meetingId
}, {
meetingId: meetingId,
x: 0,
y: 0
}, (err, numChanged) => {
if(err) {
return Meteor.log.error(`err upserting cursor for ${meetingId}`);
} else {
// Meteor.log.info "ok upserting cursor for #{meetingId}"
}
});
};
this.updateCursorLocation = function(meetingId, cursorObject) {
return Meteor.Cursor.update({
meetingId: meetingId
}, {
$set: {
x: cursorObject.x,
y: cursorObject.y
}
}, (err, numChanged) => {
if(err != null) {
return Meteor.log.error(`_unsucc update of cursor for ${meetingId} ${JSON.stringify(cursorObject)} err=${JSON.stringify(err)}`);
} else {
// Meteor.log.info "updated cursor for #{meetingId} #{JSON.stringify cursorObject}"
}
});
};
// called on server start and meeting end
this.clearCursorCollection = function(meetingId) {
if(meetingId != null) {
return Meteor.Cursor.remove({
meetingId: meetingId
}, () => {
return Meteor.log.info(`cleared Cursor Collection (meetingId: ${meetingId})!`);
});
} else {
return Meteor.Cursor.remove({}, () => {
return Meteor.log.info("cleared Cursor Collection (all meetings)!");
});
}
};
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,83 +0,0 @@
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@addMeetingToCollection = (meetingId, name, intendedForRecording, voiceConf, duration, callback) ->
#check if the meeting is already in the collection
Meteor.Meetings.upsert({meetingId:meetingId}, {$set: {
meetingName:name
intendedForRecording: intendedForRecording
currentlyBeingRecorded: false # default value
voiceConf: voiceConf
duration: duration
roomLockSettings:
# by default the lock settings will be disabled on meeting create
disablePrivateChat: false
disableCam: false
disableMic: false
lockOnJoin: Meteor.config.lockOnJoin
lockedLayout: false
disablePublicChat: false
lockOnJoinConfigurable: false # TODO
}}, (err, numChanged) =>
if numChanged.insertedId?
funct = (cbk) ->
Meteor.log.info "__added MEETING #{meetingId}"
cbk()
funct(callback)
else
Meteor.log.error "nothing happened"
callback()
)
# initialize the cursor in the meeting
initializeCursor(meetingId)
@clearMeetingsCollection = (meetingId) ->
if meetingId?
Meteor.Meetings.remove({meetingId: meetingId},
Meteor.log.info "cleared Meetings Collection (meetingId: #{meetingId}!")
else
Meteor.Meetings.remove({}, Meteor.log.info "cleared Meetings Collection (all meetings)!")
#clean up upon a meeting's end
@removeMeetingFromCollection = (meetingId, callback) ->
if Meteor.Meetings.findOne({meetingId: meetingId})?
Meteor.log.info "end of meeting #{meetingId}. Clear the meeting data from all collections"
# delete all users in the meeting
clearUsersCollection(meetingId)
# delete all slides in the meeting
clearSlidesCollection(meetingId)
# delete all shapes in the meeting
clearShapesCollection(meetingId)
# delete all presentations in the meeting
clearPresentationsCollection(meetingId)
# delete all chat messages in the meeting
clearChatCollection(meetingId)
# delete the meeting
clearMeetingsCollection(meetingId)
# delete the cursor for the meeting
clearCursorCollection(meetingId)
callback()
else
funct = (localCallback) ->
Meteor.log.error ("Error! There was no such meeting #{meetingId}")
localCallback()
funct(callback)
# --------------------------------------------------------------------------------------------
# end Private methods on server
# --------------------------------------------------------------------------------------------

View File

@ -0,0 +1,100 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addMeetingToCollection = function(meetingId, name, intendedForRecording, voiceConf, duration, callback) {
//check if the meeting is already in the collection
Meteor.Meetings.upsert({
meetingId: meetingId
}, {
$set: {
meetingName: name,
intendedForRecording: intendedForRecording,
currentlyBeingRecorded: false,
voiceConf: voiceConf,
duration: duration,
roomLockSettings: {
// by default the lock settings will be disabled on meeting create
disablePrivateChat: false,
disableCam: false,
disableMic: false,
lockOnJoin: Meteor.config.lockOnJoin,
lockedLayout: false,
disablePublicChat: false,
lockOnJoinConfigurable: false // TODO
}
}
}, (_this => {
return function(err, numChanged) {
let funct;
if(numChanged.insertedId != null) {
funct = function(cbk) {
Meteor.log.info(`__added MEETING ${meetingId}`);
return cbk();
};
return funct(callback);
} else {
Meteor.log.error("nothing happened");
return callback();
}
};
})(this));
// initialize the cursor in the meeting
return initializeCursor(meetingId);
};
this.clearMeetingsCollection = function(meetingId) {
if(meetingId != null) {
return Meteor.Meetings.remove({
meetingId: meetingId
}, Meteor.log.info(`cleared Meetings Collection (meetingId: ${meetingId}!`));
} else {
return Meteor.Meetings.remove({}, Meteor.log.info("cleared Meetings Collection (all meetings)!"));
}
};
//clean up upon a meeting's end
this.removeMeetingFromCollection = function(meetingId, callback) {
let funct;
if(Meteor.Meetings.findOne({
meetingId: meetingId
}) != null) {
Meteor.log.info(`end of meeting ${meetingId}. Clear the meeting data from all collections`);
// delete all users in the meeting
clearUsersCollection(meetingId);
// delete all slides in the meeting
clearSlidesCollection(meetingId);
// delete all shapes in the meeting
clearShapesCollection(meetingId);
// delete all presentations in the meeting
clearPresentationsCollection(meetingId);
// delete all chat messages in the meeting
clearChatCollection(meetingId);
// delete the meeting
clearMeetingsCollection(meetingId);
// delete the cursor for the meeting
clearCursorCollection(meetingId);
return callback();
} else {
funct = function(localCallback) {
Meteor.log.error(`Error! There was no such meeting ${meetingId}`);
return localCallback();
};
return funct(callback);
}
};
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,72 +0,0 @@
# --------------------------------------------------------------------------------------------
# Public methods on server
# --------------------------------------------------------------------------------------------
Meteor.methods
publishVoteMessage: (meetingId, pollAnswerId, requesterUserId, requesterToken) ->
if isAllowedTo("subscribePoll", meetingId, requesterUserId, requesterToken)
eventName = "vote_poll_user_request_message"
result = Meteor.Polls.findOne({"poll_info.users": requesterUserId, "poll_info.meetingId": meetingId, "poll_info.poll.answers.id": pollAnswerId},
{fields: {"poll_info.poll.id": 1, _id: 0}})
_poll_id = result.poll_info.poll.id
if eventName? and meetingId? and requesterUserId? and _poll_id? and pollAnswerId?
message =
header:
timestamp: new Date().getTime()
name: eventName
payload:
meeting_id: meetingId
user_id: requesterUserId
poll_id: _poll_id
question_id: 0
answer_id: pollAnswerId
Meteor.Polls.update({"poll_info.users": requesterUserId, "poll_info.meetingId": meetingId, "poll_info.poll.answers.id": pollAnswerId},
{ $pull: {"poll_info.users": requesterUserId}});
Meteor.log.info "publishing Poll response to redis"
publish Meteor.config.redis.channels.toBBBApps.polling, message
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@addPollToCollection = (poll, requester_id, users, meetingId) ->
#copying all the userids into an array
_users = []
for user in users
_users.push user.user.userid
#adding the initial number of votes for each answer
for answer in poll.answers
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_respondents = -1
#adding all together and inserting into the Polls collection
entry =
poll_info:
"meetingId": meetingId
"poll": poll
"requester": requester_id
"users": _users
Meteor.log.info "added poll _id=[#{poll.id}]:meetingId=[#{meetingId}]."
Meteor.Polls.insert(entry)
@clearPollCollection = (meetingId, poll_id) ->
if meetingId? and poll_id? and Meteor.Polls.findOne({"poll_info.meetingId": meetingId, "poll_info.poll.id": poll_id})?
Meteor.Polls.remove({
"poll_info.meetingId": meetingId,
"poll_info.poll.id": poll_id},
Meteor.log.info "cleared Polls Collection (meetingId: #{meetingId}, pollId: #{poll_id}!)")
else
Meteor.Polls.remove({}, Meteor.log.info "cleared Polls Collection (all meetings)!")
@updatePollCollection = (poll, meetingId, requesterId) ->
if poll.answers? and poll.num_responders? and poll.num_respondents? and poll.id? and meetingId? and requesterId?
Meteor.Polls.update({"poll_info.meetingId": meetingId, "poll_info.requester": requesterId, "poll_info.poll.id": poll.id},
{$set:
{"poll_info.poll.answers": poll.answers, "poll_info.poll.num_responders": poll.num_responders, "poll_info.poll.num_respondents": poll.num_respondents}
}, Meteor.log.info "updating Polls Collection (meetingId: #{meetingId}, pollId: #{poll.id}!)")

View File

@ -0,0 +1,113 @@
// --------------------------------------------------------------------------------------------
// Public methods on server
// --------------------------------------------------------------------------------------------
Meteor.methods({
publishVoteMessage(meetingId, pollAnswerId, requesterUserId, requesterToken) {
let _poll_id, eventName, message, result;
if (isAllowedTo("subscribePoll", meetingId, requesterUserId, requesterToken)) {
eventName = "vote_poll_user_request_message";
result = Meteor.Polls.findOne({
"poll_info.users": requesterUserId,
"poll_info.meetingId": meetingId,
"poll_info.poll.answers.id": pollAnswerId
}, {
fields: {
"poll_info.poll.id": 1,
_id: 0
}
});
_poll_id = result.poll_info.poll.id;
if ((eventName != null) && (meetingId != null) && (requesterUserId != null) && (_poll_id != null) && (pollAnswerId != null)) {
message = {
header: {
timestamp: new Date().getTime(),
name: eventName
},
payload: {
meeting_id: meetingId,
user_id: requesterUserId,
poll_id: _poll_id,
question_id: 0,
answer_id: pollAnswerId
}
};
Meteor.Polls.update({
"poll_info.users": requesterUserId,
"poll_info.meetingId": meetingId,
"poll_info.poll.answers.id": pollAnswerId
}, {
$pull: {
"poll_info.users": requesterUserId
}
});
Meteor.log.info("publishing Poll response to redis");
return publish(Meteor.config.redis.channels.toBBBApps.polling, message);
}
}
}
});
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addPollToCollection = function(poll, requester_id, users, meetingId) {
let _users, answer, entry, i, j, len, len1, ref, user;
//copying all the userids into an array
_users = [];
for (i = 0, len = users.length; i < len; i++) {
user = users[i];
_users.push(user.user.userid);
}
//adding the initial number of votes for each answer
ref = poll.answers;
for (j = 0, len1 = ref.length; j < len1; j++) {
answer = ref[j];
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_respondents = -1;
//adding all together and inserting into the Polls collection
entry = {
poll_info: {
"meetingId": meetingId,
"poll": poll,
"requester": requester_id,
"users": _users
}
};
Meteor.log.info(`added poll _id=[${poll.id}]:meetingId=[${meetingId}].`);
return Meteor.Polls.insert(entry);
};
this.clearPollCollection = function(meetingId, poll_id) {
if ((meetingId != null) && (poll_id != null) && (Meteor.Polls.findOne({
"poll_info.meetingId": meetingId,
"poll_info.poll.id": poll_id
}) != null)) {
return Meteor.Polls.remove({
"poll_info.meetingId": meetingId,
"poll_info.poll.id": poll_id
}, Meteor.log.info(`cleared Polls Collection (meetingId: ${meetingId}, pollId: ${poll_id}!)`));
} else {
return Meteor.Polls.remove({}, Meteor.log.info("cleared Polls Collection (all meetings)!"));
}
};
this.updatePollCollection = function(poll, meetingId, requesterId) {
if ((poll.answers != null) && (poll.num_responders != null) && (poll.num_respondents != null) && (poll.id != null) && (meetingId != null) && (requesterId != null)) {
return Meteor.Polls.update({
"poll_info.meetingId": meetingId,
"poll_info.requester": requesterId,
"poll_info.poll.id": poll.id
}, {
$set: {
"poll_info.poll.answers": poll.answers,
"poll_info.poll.num_responders": poll.num_responders,
"poll_info.poll.num_respondents": poll.num_respondents
}
}, Meteor.log.info(`updating Polls Collection (meetingId: ${meetingId}, pollId: ${poll.id}!)`));
}
};

View File

@ -1,85 +0,0 @@
Meteor.methods
publishSwitchToPreviousSlideMessage: (meetingId, userId, authToken) ->
currentPresentationDoc = Meteor.Presentations.findOne({
"meetingId": meetingId
"presentation.current" : true})
currentSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId
"presentationId": currentPresentationDoc?.presentation.id
"slide.current" : true})
previousSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId
"presentationId": currentPresentationDoc?.presentation.id
"slide.num" : currentSlideDoc?.slide.num-1})
if previousSlideDoc? and isAllowedTo('switchSlide', meetingId, userId, authToken)
message =
"payload":
"page": previousSlideDoc.slide.id
"meeting_id": meetingId
"header":
"name": "go_to_slide"
publish Meteor.config.redis.channels.toBBBApps.presentation, message
publishSwitchToNextSlideMessage: (meetingId, userId, authToken) ->
currentPresentationDoc = Meteor.Presentations.findOne({
"meetingId": meetingId
"presentation.current" : true})
currentSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId
"presentationId": currentPresentationDoc?.presentation.id
"slide.current" : true})
nextSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId
"presentationId": currentPresentationDoc?.presentation.id
"slide.num" : currentSlideDoc?.slide.num+1})
if nextSlideDoc? and isAllowedTo('switchSlide', meetingId, userId, authToken)
message =
"payload":
"page": nextSlideDoc.slide.id
"meeting_id": meetingId
"header":
"name": "go_to_slide"
publish Meteor.config.redis.channels.toBBBApps.presentation, message
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@addPresentationToCollection = (meetingId, presentationObject) ->
#check if the presentation is already in the collection
unless Meteor.Presentations.findOne({meetingId: meetingId, 'presentation.id': presentationObject.id})?
entry =
meetingId: meetingId
presentation:
id: presentationObject.id
name: presentationObject.name
current: presentationObject.current
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()}"
@removePresentationFromCollection = (meetingId, presentationId) ->
if meetingId? and presentationId? and Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId})?
id = Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId})
if id?
Meteor.Slides.remove({presentationId: presentationId}, Meteor.log.info "cleared Slides Collection (presentationId: #{presentationId}!")
Meteor.Presentations.remove(id._id)
Meteor.log.info "----removed presentation[" + presentationId + "] from " + meetingId
# called on server start and meeting end
@clearPresentationsCollection = (meetingId) ->
if meetingId?
Meteor.Presentations.remove({meetingId: meetingId}, Meteor.log.info "cleared Presentations Collection (meetingId: #{meetingId}!")
else
Meteor.Presentations.remove({}, Meteor.log.info "cleared Presentations Collection (all meetings)!")
# --------------------------------------------------------------------------------------------
# end Private methods on server
# --------------------------------------------------------------------------------------------

View File

@ -0,0 +1,118 @@
Meteor.methods({
publishSwitchToPreviousSlideMessage(meetingId, userId, authToken) {
let currentPresentationDoc, currentSlideDoc, message, previousSlideDoc;
currentPresentationDoc = Meteor.Presentations.findOne({
"meetingId": meetingId,
"presentation.current": true
});
currentSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId,
"presentationId": currentPresentationDoc != null ? currentPresentationDoc.presentation.id : void 0,
"slide.current": true
});
previousSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId,
"presentationId": currentPresentationDoc != null ? currentPresentationDoc.presentation.id : void 0,
"slide.num": (currentSlideDoc != null ? currentSlideDoc.slide.num : void 0) - 1
});
if((previousSlideDoc != null) && isAllowedTo('switchSlide', meetingId, userId, authToken)) {
message = {
"payload": {
"page": previousSlideDoc.slide.id,
"meeting_id": meetingId
},
"header": {
"name": "go_to_slide"
}
};
return publish(Meteor.config.redis.channels.toBBBApps.presentation, message);
}
},
publishSwitchToNextSlideMessage(meetingId, userId, authToken) {
let currentPresentationDoc, currentSlideDoc, message, nextSlideDoc;
currentPresentationDoc = Meteor.Presentations.findOne({
"meetingId": meetingId,
"presentation.current": true
});
currentSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId,
"presentationId": currentPresentationDoc != null ? currentPresentationDoc.presentation.id : void 0,
"slide.current": true
});
nextSlideDoc = Meteor.Slides.findOne({
"meetingId": meetingId,
"presentationId": currentPresentationDoc != null ? currentPresentationDoc.presentation.id : void 0,
"slide.num": (currentSlideDoc != null ? currentSlideDoc.slide.num : void 0) + 1
});
if((nextSlideDoc != null) && isAllowedTo('switchSlide', meetingId, userId, authToken)) {
message = {
"payload": {
"page": nextSlideDoc.slide.id,
"meeting_id": meetingId
},
"header": {
"name": "go_to_slide"
}
};
return publish(Meteor.config.redis.channels.toBBBApps.presentation, message);
}
}
});
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addPresentationToCollection = function(meetingId, presentationObject) {
let entry, id;
//check if the presentation is already in the collection
if(Meteor.Presentations.findOne({
meetingId: meetingId,
'presentation.id': presentationObject.id
}) == null) {
entry = {
meetingId: meetingId,
presentation: {
id: presentationObject.id,
name: presentationObject.name,
current: presentationObject.current
}
};
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()}"
}
};
this.removePresentationFromCollection = function(meetingId, presentationId) {
let id;
if((meetingId != null) && (presentationId != null) && (Meteor.Presentations.findOne({
meetingId: meetingId,
"presentation.id": presentationId
}) != null)) {
id = Meteor.Presentations.findOne({
meetingId: meetingId,
"presentation.id": presentationId
});
if(id != null) {
Meteor.Slides.remove({
presentationId: presentationId
}, Meteor.log.info(`cleared Slides Collection (presentationId: ${presentationId}!`));
Meteor.Presentations.remove(id._id);
return Meteor.log.info(`----removed presentation[${presentationId}] from ${meetingId}`);
}
}
};
// called on server start and meeting end
this.clearPresentationsCollection = function(meetingId) {
if(meetingId != null) {
return Meteor.Presentations.remove({
meetingId: meetingId
}, Meteor.log.info(`cleared Presentations Collection (meetingId: ${meetingId}!`));
} else {
return Meteor.Presentations.remove({}, Meteor.log.info("cleared Presentations Collection (all meetings)!"));
}
};
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,102 +0,0 @@
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@addShapeToCollection = (meetingId, whiteboardId, shapeObject) ->
if shapeObject?.shape_type is "text"
Meteor.log.info "we are dealing with a text shape and the event is:#{shapeObject.status}"
entry =
meetingId: meetingId
whiteboardId: whiteboardId
shape:
type: shapeObject.shape.type
textBoxHeight: shapeObject.shape.textBoxHeight
backgroundColor: shapeObject.shape.backgroundColor
fontColor: shapeObject.shape.fontColor
status: shapeObject.shape.status
dataPoints: shapeObject.shape.dataPoints
x: shapeObject.shape.x
textBoxWidth: shapeObject.shape.textBoxWidth
whiteboardId: shapeObject.shape.whiteboardId
fontSize: shapeObject.shape.fontSize
id: shapeObject.shape.id
y: shapeObject.shape.y
calcedFontSize: shapeObject.shape.calcedFontSize
text: shapeObject.shape.text
background: shapeObject.shape.background
if shapeObject.status is "textEdited" or shapeObject.status is "textPublished"
# only keep the final version of the text shape
removeTempTextShape = (callback) ->
Meteor.Shapes.remove({'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}"
callback()
removeTempTextShape( ->
# display as the prestenter is typing
id = Meteor.Shapes.insert(entry)
Meteor.log.info "#{shapeObject.status} substituting the temp shapes with the newer one"
)
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?.status is "DRAW_END" or (shapeObject?.status is "DRAW_START" and shapeObject?.shape_type is "pencil")
entry =
meetingId: meetingId
whiteboardId: whiteboardId
shape:
wb_id: shapeObject.wb_id
shape_type: shapeObject.shape_type
status: shapeObject.status
id: shapeObject.id
shape:
type: shapeObject.shape.type
status: shapeObject.shape.status
points: shapeObject.shape.points
whiteboardId: shapeObject.shape.whiteboardId
id: shapeObject.shape.id
square: shapeObject.shape.square
transparency: shapeObject.shape.transparency
thickness: shapeObject.shape.thickness
color: shapeObject.shape.color
result: shapeObject.shape.result
num_respondents: shapeObject.shape.num_respondents
num_responders: shapeObject.shape.num_responders
id = Meteor.Shapes.insert(entry)
@removeAllShapesFromSlide = (meetingId, whiteboardId) ->
Meteor.log.info "removeAllShapesFromSlide__" + whiteboardId
if meetingId? and whiteboardId? and Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId})?
Meteor.Shapes.remove {meetingId: meetingId, whiteboardId: whiteboardId}, ->
Meteor.log.info "clearing all shapes from slide"
# After shapes are cleared, wait 1 second and set cleaning off
Meteor.setTimeout ->
Meteor.WhiteboardCleanStatus.update({meetingId: meetingId}, {$set: {in_progress: false}})
, 1000
@removeShapeFromSlide = (meetingId, whiteboardId, shapeId) ->
shapeToRemove = Meteor.Shapes.findOne({meetingId: meetingId, whiteboardId: whiteboardId, "shape.id": shapeId})
if meetingId? and whiteboardId? and shapeId? and shapeToRemove?
Meteor.Shapes.remove(shapeToRemove._id)
Meteor.log.info "----removed shape[" + shapeId + "] from " + whiteboardId
Meteor.log.info "remaining shapes on the slide:" + Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).count()
# called on server start and meeting end
@clearShapesCollection = (meetingId) ->
if meetingId?
Meteor.Shapes.remove {meetingId: meetingId}, ->
Meteor.log.info "cleared Shapes Collection (meetingId: #{meetingId}!"
Meteor.WhiteboardCleanStatus.update({meetingId: meetingId}, {$set: {in_progress: false}})
else
Meteor.Shapes.remove {}, ->
Meteor.log.info "cleared Shapes Collection (all meetings)!"
Meteor.WhiteboardCleanStatus.update({meetingId: meetingId}, {$set: {in_progress: false}})
# --------------------------------------------------------------------------------------------
# end Private methods on server
# --------------------------------------------------------------------------------------------

View File

@ -0,0 +1,153 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.addShapeToCollection = function(meetingId, whiteboardId, shapeObject) {
let entry, id, removeTempTextShape;
if((shapeObject != null ? shapeObject.shape_type : void 0) === "text") {
Meteor.log.info(`we are dealing with a text shape and the event is:${shapeObject.status}`);
entry = {
meetingId: meetingId,
whiteboardId: whiteboardId,
shape: {
type: shapeObject.shape.type,
textBoxHeight: shapeObject.shape.textBoxHeight,
backgroundColor: shapeObject.shape.backgroundColor,
fontColor: shapeObject.shape.fontColor,
status: shapeObject.shape.status,
dataPoints: shapeObject.shape.dataPoints,
x: shapeObject.shape.x,
textBoxWidth: shapeObject.shape.textBoxWidth,
whiteboardId: shapeObject.shape.whiteboardId,
fontSize: shapeObject.shape.fontSize,
id: shapeObject.shape.id,
y: shapeObject.shape.y,
calcedFontSize: shapeObject.shape.calcedFontSize,
text: shapeObject.shape.text,
background: shapeObject.shape.background
}
};
if(shapeObject.status === "textEdited" || shapeObject.status === "textPublished") {
// only keep the final version of the text shape
removeTempTextShape = function(callback) {
Meteor.Shapes.remove({
'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 removeTempTextShape(() => {
// display as the prestenter is typing
let id;
id = Meteor.Shapes.insert(entry);
return Meteor.log.info(`${shapeObject.status} substituting the temp shapes with the newer one`);
});
}
} 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")) {
entry = {
meetingId: meetingId,
whiteboardId: whiteboardId,
shape: {
wb_id: shapeObject.wb_id,
shape_type: shapeObject.shape_type,
status: shapeObject.status,
id: shapeObject.id,
shape: {
type: shapeObject.shape.type,
status: shapeObject.shape.status,
points: shapeObject.shape.points,
whiteboardId: shapeObject.shape.whiteboardId,
id: shapeObject.shape.id,
square: shapeObject.shape.square,
transparency: shapeObject.shape.transparency,
thickness: shapeObject.shape.thickness,
color: shapeObject.shape.color,
result: shapeObject.shape.result,
num_respondents: shapeObject.shape.num_respondents,
num_responders: shapeObject.shape.num_responders
}
}
};
return id = Meteor.Shapes.insert(entry);
}
}
};
this.removeAllShapesFromSlide = function(meetingId, whiteboardId) {
Meteor.log.info(`removeAllShapesFromSlide__${whiteboardId}`);
if((meetingId != null) && (whiteboardId != null) && (Meteor.Shapes.find({
meetingId: meetingId,
whiteboardId: whiteboardId
}) != null)) {
return Meteor.Shapes.remove({
meetingId: meetingId,
whiteboardId: whiteboardId
}, () => {
Meteor.log.info("clearing all shapes from slide");
// After shapes are cleared, wait 1 second and set cleaning off
return Meteor.setTimeout(() => {
return Meteor.WhiteboardCleanStatus.update({
meetingId: meetingId
}, {
$set: {
in_progress: false
}
});
}, 1000);
});
}
};
this.removeShapeFromSlide = function(meetingId, whiteboardId, shapeId) {
let shapeToRemove;
shapeToRemove = Meteor.Shapes.findOne({
meetingId: meetingId,
whiteboardId: whiteboardId,
"shape.id": shapeId
});
if((meetingId != null) && (whiteboardId != null) && (shapeId != null) && (shapeToRemove != null)) {
Meteor.Shapes.remove(shapeToRemove._id);
Meteor.log.info(`----removed shape[${shapeId}] from ${whiteboardId}`);
return Meteor.log.info(`remaining shapes on the slide:${Meteor.Shapes.find({
meetingId: meetingId,
whiteboardId: whiteboardId
}).count()}`);
}
};
// called on server start and meeting end
this.clearShapesCollection = function(meetingId) {
if(meetingId != null) {
return Meteor.Shapes.remove({
meetingId: meetingId
}, () => {
Meteor.log.info(`cleared Shapes Collection (meetingId: ${meetingId}!`);
return Meteor.WhiteboardCleanStatus.update({
meetingId: meetingId
}, {
$set: {
in_progress: false
}
});
});
} else {
return Meteor.Shapes.remove({}, () => {
Meteor.log.info("cleared Shapes Collection (all meetings)!");
return Meteor.WhiteboardCleanStatus.update({
meetingId: meetingId
}, {
$set: {
in_progress: false
}
});
});
}
};
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,52 +0,0 @@
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@displayThisSlide = (meetingId, newSlideId, slideObject) ->
presentationId = newSlideId.split("/")[0] # grab the presentationId part of the slideId
# change current to false for the old slide
Meteor.Slides.update({presentationId: presentationId, "slide.current": true}, {$set: {"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)
# add the new slide to the collection
addSlideToCollection(meetingId, presentationId, slideObject)
@addSlideToCollection = (meetingId, presentationId, slideObject) ->
unless Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideObject.id})?
entry =
meetingId: meetingId
presentationId: presentationId
slide:
height_ratio: slideObject.height_ratio
y_offset: slideObject.y_offset
num: slideObject.num
x_offset: slideObject.x_offset
current: slideObject.current
img_uri: if slideObject.svg_uri isnt undefined then slideObject.svg_uri else slideObject.png_uri
txt_uri: slideObject.txt_uri
id: slideObject.id
width_ratio: slideObject.width_ratio
swf_uri: slideObject.swf_uri
thumb_uri: slideObject.thumb_uri
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"
@removeSlideFromCollection = (meetingId, slideId) ->
if meetingId? and slideId? and Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId})?
id = Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId})
if id?
Meteor.Slides.remove(id._id)
Meteor.log.info "----removed slide[" + slideId + "] from " + meetingId
# called on server start and meeting end
@clearSlidesCollection = (meetingId) ->
if meetingId?
Meteor.Slides.remove({meetingId: meetingId}, Meteor.log.info "cleared Slides Collection (meetingId: #{meetingId}!")
else
Meteor.Slides.remove({}, Meteor.log.info "cleared Slides Collection (all meetings)!")
# --------------------------------------------------------------------------------------------
# end Private methods on server
# --------------------------------------------------------------------------------------------

View File

@ -0,0 +1,81 @@
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
this.displayThisSlide = function(meetingId, newSlideId, slideObject) {
let presentationId;
presentationId = newSlideId.split("/")[0]; // grab the presentationId part of the slideId
// change current to false for the old slide
Meteor.Slides.update({
presentationId: presentationId,
"slide.current": true
}, {
$set: {
"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);
// add the new slide to the collection
return addSlideToCollection(meetingId, presentationId, slideObject);
};
this.addSlideToCollection = function(meetingId, presentationId, slideObject) {
let entry, id;
if(Meteor.Slides.findOne({
meetingId: meetingId,
"slide.id": slideObject.id
}) == null) {
entry = {
meetingId: meetingId,
presentationId: presentationId,
slide: {
height_ratio: slideObject.height_ratio,
y_offset: slideObject.y_offset,
num: slideObject.num,
x_offset: slideObject.x_offset,
current: slideObject.current,
img_uri: slideObject.svg_uri !== void 0 ? slideObject.svg_uri : slideObject.png_uri,
txt_uri: slideObject.txt_uri,
id: slideObject.id,
width_ratio: slideObject.width_ratio,
swf_uri: slideObject.swf_uri,
thumb_uri: slideObject.thumb_uri
}
};
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"
}
};
this.removeSlideFromCollection = function(meetingId, slideId) {
let id;
if((meetingId != null) && (slideId != null) && (Meteor.Slides.findOne({
meetingId: meetingId,
"slide.id": slideId
}) != null)) {
id = Meteor.Slides.findOne({
meetingId: meetingId,
"slide.id": slideId
});
if(id != null) {
Meteor.Slides.remove(id._id);
return Meteor.log.info(`----removed slide[${slideId}] from ${meetingId}`);
}
}
};
// called on server start and meeting end
this.clearSlidesCollection = function(meetingId) {
if(meetingId != null) {
return Meteor.Slides.remove({
meetingId: meetingId
}, Meteor.log.info(`cleared Slides Collection (meetingId: ${meetingId}!`));
} else {
return Meteor.Slides.remove({}, Meteor.log.info("cleared Slides Collection (all meetings)!"));
}
};
// --------------------------------------------------------------------------------------------
// end Private methods on server
// --------------------------------------------------------------------------------------------

View File

@ -1,496 +0,0 @@
# --------------------------------------------------------------------------------------------
# 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
# 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) ->
voiceConf = Meteor.Meetings.findOne({meetingId:meetingId})?.voiceConf
username = Meteor.Users.findOne({meetingId:meetingId, userId:userId})?.user.name
if isJoining
if isAllowedTo('joinListenOnly', meetingId, userId, authToken)
message =
payload:
userid: userId
meeting_id: meetingId
voice_conf: voiceConf
name: username
header:
timestamp: new Date().getTime()
name: "user_connected_to_global_audio"
version: "0.0.1"
Meteor.log.info "publishing a user listenOnly toggleRequest #{isJoining} request for #{userId}"
publish Meteor.config.redis.channels.toBBBApps.meeting, message
else
if isAllowedTo('leaveListenOnly', meetingId, userId, authToken)
message =
payload:
userid: userId
meeting_id: meetingId
voice_conf: voiceConf
name: username
header:
timestamp: new Date().getTime()
name: "user_disconnected_from_global_audio"
version: "0.0.1"
Meteor.log.info "publishing a user listenOnly toggleRequest #{isJoining} request for #{userId}"
publish Meteor.config.redis.channels.toBBBApps.meeting, message
return
# 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) ->
action = ->
if toMuteUserId is requesterUserId
return 'muteSelf'
else
return 'muteOther'
if isAllowedTo(action(), meetingId, requesterUserId, requesterToken)
message =
payload:
user_id: toMuteUserId
meeting_id: meetingId
mute: true
requester_id: requesterUserId
header:
timestamp: new Date().getTime()
name: "mute_user_request_message"
version: "0.0.1"
Meteor.log.info "publishing a user mute request for #{toMuteUserId}"
publish Meteor.config.redis.channels.toBBBApps.users, message
updateVoiceUser meetingId, {'web_userid': toMuteUserId, talking:false, muted:true}
return
# 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) ->
action = ->
if toMuteUserId is requesterUserId
return 'unmuteSelf'
else
return 'unmuteOther'
if isAllowedTo(action(), meetingId, requesterUserId, requesterToken)
message =
payload:
user_id: toMuteUserId
meeting_id: meetingId
mute: false
requester_id: requesterUserId
header:
timestamp: new Date().getTime()
name: "mute_user_request_message"
version: "0.0.1"
Meteor.log.info "publishing a user unmute request for #{toMuteUserId}"
publish Meteor.config.redis.channels.toBBBApps.users, message
updateVoiceUser meetingId, {'web_userid': toMuteUserId, talking:false, muted:false}
return
userSetEmoji: (meetingId, toRaiseUserId, raisedByUserId, raisedByToken, status) ->
if isAllowedTo('setEmojiStatus', meetingId, raisedByUserId, raisedByToken)
message =
payload:
emoji_status: status
userid: toRaiseUserId
meeting_id: meetingId
header:
timestamp: new Date().getTime()
name: "user_emoji_status_message"
version: "0.0.1"
# publish to pubsub
publish Meteor.config.redis.channels.toBBBApps.users, message
return
# 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) ->
if isAllowedTo('logoutSelf', meetingId, userId, authToken)
Meteor.log.info "a user is logging out from #{meetingId}:" + userId
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) ->
if isAllowedTo('kickUser', meetingId, requesterUserId, authToken)
message =
"payload":
"userid": toKickUserId
"ejected_by": requesterUserId
"meeting_id": meetingId
"header":
"name": "eject_user_from_meeting_request_message"
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: (meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken) ->
if isAllowedTo('setPresenter', meetingId, requesterSetPresenter, authToken)
message =
"payload":
"new_presenter_id": newPresenterId
"new_presenter_name": newPresenterName
"meeting_id": meetingId
"assigned_by": requesterSetPresenter
"header":
"name": "assign_presenter_request_message"
publish Meteor.config.redis.channels.toBBBApps.users, message
# --------------------------------------------------------------------------------------------
# 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
@markUserOffline = (meetingId, userId, callback) ->
# mark the user as offline. remove from the collection on meeting_end #TODO
user = Meteor.Users.findOne({meetingId: meetingId, userId: userId})
if user?.clientType is "HTML5"
Meteor.log.info "marking html5 user [#{userId}] as offline in meeting[#{meetingId}]"
Meteor.Users.update({meetingId: meetingId, userId: userId}, {$set:{
'user.connection_status':'offline'
'voiceUser.talking': false
'voiceUser.joined': false
'voiceUser.muted': false
'user.time_of_joining': 0
'user.listenOnly': false
}}, (err, numChanged) ->
if err?
Meteor.log.error "_unsucc update (mark as offline) of user #{user?.user.name} #{userId}
err=#{JSON.stringify err}"
callback()
else
funct = (cbk) ->
Meteor.log.info "_marking as offline html5 user #{user?.user.name}
#{userId} numChanged=#{numChanged}"
cbk()
funct(callback)
)
else
Meteor.Users.remove({meetingId: meetingId, userId: userId}, (err, numDeletions) ->
if err?
Meteor.log.error "_unsucc deletion of user #{user?.user.name} #{userId}
err=#{JSON.stringify err}"
callback()
else
funct = (cbk) ->
Meteor.log.info "_deleting info for user #{user?.user.name} #{userId}
numDeletions=#{numDeletions}"
cbk()
funct(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
@requestUserLeaving = (meetingId, userId) ->
userObject = Meteor.Users.findOne({'meetingId': meetingId, 'userId': userId})
voiceConf = Meteor.Meetings.findOne({meetingId:meetingId})?.voiceConf
if userObject? and voiceConf? and userId? and meetingId?
# end listenOnly audio for the departing user
if userObject.user.listenOnly
listenOnlyMessage =
payload:
userid: userId
meeting_id: meetingId
voice_conf: voiceConf
name: userObject.user.name
header:
timestamp: new Date().getTime()
name: "user_disconnected_from_global_audio"
publish Meteor.config.redis.channels.toBBBApps.meeting, listenOnlyMessage
# remove user from meeting
message =
payload:
meeting_id: meetingId
userid: userId
header:
timestamp: new Date().getTime()
name: "user_leaving_request"
Meteor.log.info "sending a user_leaving_request for #{meetingId}:#{userId}"
publish Meteor.config.redis.channels.toBBBApps.users, message
else
Meteor.log.info "did not have enough information to send a user_leaving_request"
#update a voiceUser - a helper method
@updateVoiceUser = (meetingId, voiceUserObject, callback) ->
u = Meteor.Users.findOne userId: voiceUserObject.web_userid
if u?
if voiceUserObject.talking?
Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid},
{$set: {'user.voiceUser.talking':voiceUserObject.talking}},
(err, numChanged) ->
if err?
Meteor.log.error "_unsucc update of voiceUser #{voiceUserObject.web_userid}
[talking] err=#{JSON.stringify err}"
callback()
) # talking
if voiceUserObject.joined?
Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid},
{$set: {'user.voiceUser.joined':voiceUserObject.joined}},
(err, numChanged) ->
if err?
Meteor.log.error "_unsucc update of voiceUser #{voiceUserObject.web_userid}
[joined] err=#{JSON.stringify err}"
callback()
) # joined
if voiceUserObject.locked?
Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid},
{$set: {'user.voiceUser.locked':voiceUserObject.locked}},
(err, numChanged) ->
if err?
Meteor.log.error "_unsucc update of voiceUser #{voiceUserObject.web_userid}
[locked] err=#{JSON.stringify err}"
callback()
) # locked
if voiceUserObject.muted?
Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid},
{$set: {'user.voiceUser.muted':voiceUserObject.muted}},
(err, numChanged) ->
if err?
Meteor.log.error "_unsucc update of voiceUser #{voiceUserObject.web_userid}
[muted] err=#{JSON.stringify err}"
callback()
) # muted
if voiceUserObject.listen_only?
Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid},
{$set: {'user.listenOnly':voiceUserObject.listen_only}},
(err, numChanged) ->
if err?
Meteor.log.error "_unsucc update of voiceUser #{voiceUserObject.web_userid}
[listenOnly] err=#{JSON.stringify err}"
callback()
) # listenOnly
else
Meteor.log.error "ERROR! did not find such voiceUser!"
callback()
@userJoined = (meetingId, user, callback) ->
userId = user.userid
u = Meteor.Users.findOne({userId:user.userid, 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? and u.authToken?
Meteor.Users.update({userId:user.userid, meetingId: meetingId}, {$set:{
user:
userid: user.userid
presenter: user.presenter
name: user.name
_sort_name: user.name.toLowerCase()
phone_user: user.phone_user
set_emoji_time: user.set_emoji_time
emoji_status: user.emoji_status
has_stream: user.has_stream
role: user.role
listenOnly: user.listenOnly
extern_userid: user.extern_userid
locked: user.locked
time_of_joining: user.timeOfJoining
connection_status: "online" # TODO consider other default value
voiceUser:
web_userid: user.voiceUser.web_userid
callernum: user.voiceUser.callernum
userid: user.voiceUser.userid
talking: user.voiceUser.talking
joined: user.voiceUser.joined
callername: user.voiceUser.callername
locked: user.voiceUser.locked
muted: user.voiceUser.muted
webcam_stream: user.webcam_stream
}}, (err) ->
if err?
Meteor.log.error "_error #{err} when trying to insert user #{userId}"
callback()
else
funct = (cbk) ->
Meteor.log.info "_(case1) UPDATING USER #{user.userid}, authToken=
#{u.authToken}, locked=#{user.locked}, username=#{user.name}"
cbk()
funct(callback)
)
welcomeMessage = Meteor.config.defaultWelcomeMessage
.replace /%%CONFNAME%%/, Meteor.Meetings.findOne({meetingId: meetingId})?.meetingName
welcomeMessage = welcomeMessage + Meteor.config.defaultWelcomeMessageFooter
# add the welcome message if it's not there already OR update time_of_joining
Meteor.Chat.upsert({
meetingId: meetingId
userId: userId
'message.chat_type': 'SYSTEM_MESSAGE'
'message.to_userid': userId
}, {
meetingId: meetingId
userId: userId
message:
chat_type: 'SYSTEM_MESSAGE'
message: welcomeMessage
from_color: '0x3399FF'
to_userid: userId
from_userid: 'SYSTEM_MESSAGE'
from_username: ''
from_time: user.timeOfJoining?.toString()
}, (err) ->
if err?
Meteor.log.error "_error #{err} when trying to insert welcome message for #{userId}"
else
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
# Meteor.log.info "NOTE: got user_joined_message #{user.name} #{user.userid}"
Meteor.Users.upsert({meetingId: meetingId, userId: userId}, {
meetingId: meetingId
userId: userId
user:
userid: user.userid
presenter: user.presenter
name: user.name
_sort_name: user.name.toLowerCase()
phone_user: user.phone_user
emoji_status: user.emoji_status
set_emoji_time: user.set_emoji_time
has_stream: user.has_stream
role: user.role
listenOnly: user.listenOnly
extern_userid: user.extern_userid
locked: user.locked
time_of_joining: user.timeOfJoining
connection_status: "" # TODO consider other default value
voiceUser:
web_userid: user.voiceUser.web_userid
callernum: user.voiceUser.callernum
userid: user.voiceUser.userid
talking: user.voiceUser.talking
joined: user.voiceUser.joined
callername: user.voiceUser.callername
locked: user.voiceUser.locked
muted: user.voiceUser.muted
webcam_stream: user.webcam_stream
}, (err, numChanged) ->
if numChanged.insertedId?
funct = (cbk) ->
Meteor.log.info "_joining user (case2) userid=[#{userId}]:#{user.name}.
Users.size is now #{Meteor.Users.find({meetingId: meetingId}).count()}"
cbk()
funct(callback)
else
callback()
)
@createDummyUser = (meetingId, userId, authToken) ->
if Meteor.Users.findOne({userId:userId, meetingId: meetingId, authToken:authToken})?
Meteor.log.info "html5 user userId:[#{userId}] from [#{meetingId}] tried to revalidate token"
else
Meteor.Users.insert({
meetingId: meetingId
userId: userId
authToken: authToken
clientType: "HTML5"
validated: false #will be validated on validate_auth_token_reply
}, (err, id) ->
Meteor.log.info "_added a dummy html5 user with: userId=[#{userId}]
Users.size is now #{Meteor.Users.find({meetingId: meetingId}).count()}"
)
# when new lock settings including disableMic are set,
# all viewers that are in the audio bridge with a mic should be muted and locked
@handleLockingMic = (meetingId, newSettings) ->
# send mute requests for the viewer users joined with mic
for u in Meteor.Users.find({
meetingId:meetingId
'user.role':'VIEWER'
'user.listenOnly':false
'user.locked':true
'user.voiceUser.joined':true
'user.voiceUser.muted':false})?.fetch()
# Meteor.log.info u.user.name #
Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true) #true for muted
# change the locked status of a user (lock settings)
@setUserLockedStatus = (meetingId, userId, isLocked) ->
u = Meteor.Users.findOne({meetingId:meetingId, userId:userId})
if u?
Meteor.Users.update({userId:userId, meetingId: meetingId},
{$set:{'user.locked': isLocked}},
(err, numChanged) ->
if err?
Meteor.log.error "_error #{err} while updating user #{userId} with lock settings"
else
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 is 'VIEWER' and !u.user.listenOnly and u.user.voiceUser.joined and !u.user.voiceUser.muted and isLocked
Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true) #true for muted
else
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
@clearUsersCollection = (meetingId) ->
if meetingId?
Meteor.Users.remove({meetingId: meetingId}, (err) ->
if err?
Meteor.log.error "_error #{JSON.stringify err} while removing users from meeting #{meetingId}"
else
Meteor.log.info "_cleared Users Collection (meetingId: #{meetingId})!"
)
else
Meteor.Users.remove({}, (err) ->
if err?
Meteor.log.error "_error #{JSON.stringify err} while removing users from all meetings!"
else
Meteor.log.info "_cleared Users Collection (all meetings)!"
)

View File

@ -0,0 +1,659 @@
// --------------------------------------------------------------------------------------------
// 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({
// 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) {
let message, ref, ref1, username, voiceConf;
voiceConf = (ref = Meteor.Meetings.findOne({
meetingId: meetingId
})) != null ? ref.voiceConf : void 0;
username = (ref1 = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
})) != null ? ref1.user.name : void 0;
if(isJoining) {
if(isAllowedTo('joinListenOnly', meetingId, userId, authToken)) {
message = {
payload: {
userid: userId,
meeting_id: meetingId,
voice_conf: voiceConf,
name: username
},
header: {
timestamp: new Date().getTime(),
name: "user_connected_to_global_audio",
version: "0.0.1"
}
};
Meteor.log.info(`publishing a user listenOnly toggleRequest ${isJoining} request for ${userId}`);
publish(Meteor.config.redis.channels.toBBBApps.meeting, message);
}
} else {
if(isAllowedTo('leaveListenOnly', meetingId, userId, authToken)) {
message = {
payload: {
userid: userId,
meeting_id: meetingId,
voice_conf: voiceConf,
name: username
},
header: {
timestamp: new Date().getTime(),
name: "user_disconnected_from_global_audio",
version: "0.0.1"
}
};
Meteor.log.info(`publishing a user listenOnly toggleRequest ${isJoining} request for ${userId}`);
publish(Meteor.config.redis.channels.toBBBApps.meeting, message);
}
}
},
// 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) {
let action, message;
action = function() {
if(toMuteUserId === requesterUserId) {
return 'muteSelf';
} else {
return 'muteOther';
}
};
if(isAllowedTo(action(), meetingId, requesterUserId, requesterToken)) {
message = {
payload: {
user_id: toMuteUserId,
meeting_id: meetingId,
mute: true,
requester_id: requesterUserId
},
header: {
timestamp: new Date().getTime(),
name: "mute_user_request_message",
version: "0.0.1"
}
};
Meteor.log.info(`publishing a user mute request for ${toMuteUserId}`);
publish(Meteor.config.redis.channels.toBBBApps.users, message);
updateVoiceUser(meetingId, {
'web_userid': toMuteUserId,
talking: false,
muted: true
});
}
},
// 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) {
let action, message;
action = function() {
if(toMuteUserId === requesterUserId) {
return 'unmuteSelf';
} else {
return 'unmuteOther';
}
};
if(isAllowedTo(action(), meetingId, requesterUserId, requesterToken)) {
message = {
payload: {
user_id: toMuteUserId,
meeting_id: meetingId,
mute: false,
requester_id: requesterUserId
},
header: {
timestamp: new Date().getTime(),
name: "mute_user_request_message",
version: "0.0.1"
}
};
Meteor.log.info(`publishing a user unmute request for ${toMuteUserId}`);
publish(Meteor.config.redis.channels.toBBBApps.users, message);
updateVoiceUser(meetingId, {
'web_userid': toMuteUserId,
talking: false,
muted: false
});
}
},
userSetEmoji(meetingId, toRaiseUserId, raisedByUserId, raisedByToken, status) {
let message;
if(isAllowedTo('setEmojiStatus', meetingId, raisedByUserId, raisedByToken)) {
message = {
payload: {
emoji_status: status,
userid: toRaiseUserId,
meeting_id: meetingId
},
header: {
timestamp: new Date().getTime(),
name: "user_emoji_status_message",
version: "0.0.1"
}
};
// publish to pubsub
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) {
if(isAllowedTo('logoutSelf', meetingId, userId, authToken)) {
Meteor.log.info(`a user is logging out from ${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) {
let message;
if(isAllowedTo('kickUser', meetingId, requesterUserId, authToken)) {
message = {
"payload": {
"userid": toKickUserId,
"ejected_by": requesterUserId,
"meeting_id": meetingId
},
"header": {
"name": "eject_user_from_meeting_request_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(
meetingId,
newPresenterId,
requesterSetPresenter,
newPresenterName,
authToken) {
let message;
if(isAllowedTo('setPresenter', meetingId, requesterSetPresenter, authToken)) {
message = {
"payload": {
"new_presenter_id": newPresenterId,
"new_presenter_name": newPresenterName,
"meeting_id": meetingId,
"assigned_by": requesterSetPresenter
},
"header": {
"name": "assign_presenter_request_message"
}
};
}
return publish(Meteor.config.redis.channels.toBBBApps.users, message);
}
});
// --------------------------------------------------------------------------------------------
// 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) {
// mark the user as offline. remove from the collection on meeting_end #TODO
let user;
user = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
});
if((user != null ? user.clientType : void 0) === "HTML5") {
Meteor.log.info(`marking html5 user [${userId}] as offline in meeting[${meetingId}]`);
return Meteor.Users.update({
meetingId: meetingId,
userId: userId
}, {
$set: {
'user.connection_status': 'offline',
'voiceUser.talking': false,
'voiceUser.joined': false,
'voiceUser.muted': false,
'user.time_of_joining': 0,
'user.listenOnly': false
}
}, (err, numChanged) => {
let funct;
if(err != null) {
Meteor.log.error(`_unsucc update (mark as offline) of user ${user != null ? user.user.name : void 0} ${userId} err=${JSON.stringify(err)}`);
return callback();
} else {
funct = function(cbk) {
Meteor.log.info(`_marking as offline html5 user ${user != null ? user.user.name : void 0} ${userId} numChanged=${numChanged}`);
return cbk();
};
return funct(callback);
}
});
} else {
return Meteor.Users.remove({
meetingId: meetingId,
userId: userId
}, (err, numDeletions) => {
let funct;
if(err != null) {
Meteor.log.error(`_unsucc deletion of user ${user != null ? user.user.name : void 0} ${userId} err=${JSON.stringify(err)}`);
return callback();
} else {
funct = function(cbk) {
Meteor.log.info(`_deleting info for user ${user != null ? user.user.name : void 0} ${userId} numDeletions=${numDeletions}`);
return cbk();
};
return funct(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) {
let listenOnlyMessage, message, ref, userObject, voiceConf;
userObject = Meteor.Users.findOne({
'meetingId': meetingId,
'userId': userId
});
voiceConf = (ref = Meteor.Meetings.findOne({
meetingId: meetingId
})) != null ? ref.voiceConf : void 0;
if((userObject != null) && (voiceConf != null) && (userId != null) && (meetingId != null)) {
// end listenOnly audio for the departing user
if(userObject.user.listenOnly) {
listenOnlyMessage = {
payload: {
userid: userId,
meeting_id: meetingId,
voice_conf: voiceConf,
name: userObject.user.name
},
header: {
timestamp: new Date().getTime(),
name: "user_disconnected_from_global_audio"
}
};
publish(Meteor.config.redis.channels.toBBBApps.meeting, listenOnlyMessage);
}
// remove user from meeting
message = {
payload: {
meeting_id: meetingId,
userid: userId
},
header: {
timestamp: new Date().getTime(),
name: "user_leaving_request"
}
};
Meteor.log.info(`sending a user_leaving_request for ${meetingId}:${userId}`);
return publish(Meteor.config.redis.channels.toBBBApps.users, message);
} else {
return Meteor.log.info("did not have enough information to send a user_leaving_request");
}
};
//update a voiceUser - a helper method
this.updateVoiceUser = function(meetingId, voiceUserObject, callback) {
let u;
u = Meteor.Users.findOne({
userId: voiceUserObject.web_userid
});
if(u != null) {
if(voiceUserObject.talking != null) {
Meteor.Users.update({
meetingId: meetingId,
userId: voiceUserObject.web_userid
}, {
$set: {
'user.voiceUser.talking': voiceUserObject.talking
}
}, (err, numChanged) => {
if(err != null) {
Meteor.log.error(`_unsucc update of voiceUser ${voiceUserObject.web_userid} [talking] err=${JSON.stringify(err)}`);
}
return callback();
});
} // talking
if(voiceUserObject.joined != null) {
Meteor.Users.update({
meetingId: meetingId,
userId: voiceUserObject.web_userid
}, {
$set: {
'user.voiceUser.joined': voiceUserObject.joined
}
}, (err, numChanged) => {
if (err != null) {
Meteor.log.error(
`_unsucc update of voiceUser ${voiceUserObject.web_userid} [joined] err=${JSON.stringify(err)}`
);
}
return callback();
});
} // joined
if(voiceUserObject.locked != null) {
Meteor.Users.update({
meetingId: meetingId,
userId: voiceUserObject.web_userid
}, {
$set: {
'user.voiceUser.locked': voiceUserObject.locked
}
}, (err, numChanged) => {
if(err != null) {
Meteor.log.error(`_unsucc update of voiceUser ${voiceUserObject.web_userid} [locked] err=${JSON.stringify(err)}`);
}
return callback();
});
} // locked
if(voiceUserObject.muted != null) {
Meteor.Users.update({
meetingId: meetingId,
userId: voiceUserObject.web_userid
}, {
$set: {
'user.voiceUser.muted': voiceUserObject.muted
}
}, (err, numChanged) => {
if(err != null) {
Meteor.log.error(`_unsucc update of voiceUser ${voiceUserObject.web_userid} [muted] err=${JSON.stringify(err)}`);
}
return callback();
});
} // muted
if(voiceUserObject.listen_only != null) {
return Meteor.Users.update({
meetingId: meetingId,
userId: voiceUserObject.web_userid
}, {
$set: {
'user.listenOnly': voiceUserObject.listen_only
}
}, (err, numChanged) => {
if(err != null) {
Meteor.log.error(`_unsucc update of voiceUser ${voiceUserObject.web_userid} [listenOnly] err=${JSON.stringify(err)}`);
}
return callback();
});
} // listenOnly
} else {
Meteor.log.error("ERROR! did not find such voiceUser!");
return callback();
}
};
this.userJoined = function(meetingId, user, callback) {
let ref, ref1, u, userId, welcomeMessage;
userId = user.userid;
u = Meteor.Users.findOne({
userId: user.userid,
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)) {
Meteor.Users.update({
userId: user.userid,
meetingId: meetingId
}, {
$set: {
user: {
userid: user.userid,
presenter: user.presenter,
name: user.name,
_sort_name: user.name.toLowerCase(),
phone_user: user.phone_user,
set_emoji_time: user.set_emoji_time,
emoji_status: user.emoji_status,
has_stream: user.has_stream,
role: user.role,
listenOnly: user.listenOnly,
extern_userid: user.extern_userid,
locked: user.locked,
time_of_joining: user.timeOfJoining,
connection_status: "online", // TODO consider other default value
voiceUser: {
web_userid: user.voiceUser.web_userid,
callernum: user.voiceUser.callernum,
userid: user.voiceUser.userid,
talking: user.voiceUser.talking,
joined: user.voiceUser.joined,
callername: user.voiceUser.callername,
locked: user.voiceUser.locked,
muted: user.voiceUser.muted
},
webcam_stream: user.webcam_stream
}
}
}, err => {
let funct;
if(err != null) {
Meteor.log.error(`_error ${err} when trying to insert user ${userId}`);
return callback();
} else {
funct = function(cbk) {
Meteor.log.info(`_(case1) UPDATING USER ${user.userid}, authToken= ${u.authToken}, locked=${user.locked}, username=${user.name}`);
return cbk();
};
return funct(callback);
}
});
welcomeMessage = Meteor.config.defaultWelcomeMessage.replace(/%%CONFNAME%%/, (ref = Meteor.Meetings.findOne({
meetingId: meetingId
})) != null ? ref.meetingName : void 0);
welcomeMessage = welcomeMessage + Meteor.config.defaultWelcomeMessageFooter;
// add the welcome message if it's not there already OR update time_of_joining
return Meteor.Chat.upsert({
meetingId: meetingId,
userId: userId,
'message.chat_type': 'SYSTEM_MESSAGE',
'message.to_userid': userId
}, {
meetingId: meetingId,
userId: userId,
message: {
chat_type: 'SYSTEM_MESSAGE',
message: welcomeMessage,
from_color: '0x3399FF',
to_userid: userId,
from_userid: 'SYSTEM_MESSAGE',
from_username: '',
from_time: (ref1 = user.timeOfJoining) != null ? ref1.toString() : void 0
}
}, err => {
if(err != null) {
return Meteor.log.error(`_error ${err} when trying to insert welcome message for ${userId}`);
} else {
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 {
// Meteor.log.info "NOTE: got user_joined_message #{user.name} #{user.userid}"
return Meteor.Users.upsert({
meetingId: meetingId,
userId: userId
}, {
meetingId: meetingId,
userId: userId,
user: {
userid: user.userid,
presenter: user.presenter,
name: user.name,
_sort_name: user.name.toLowerCase(),
phone_user: user.phone_user,
emoji_status: user.emoji_status,
set_emoji_time: user.set_emoji_time,
has_stream: user.has_stream,
role: user.role,
listenOnly: user.listenOnly,
extern_userid: user.extern_userid,
locked: user.locked,
time_of_joining: user.timeOfJoining,
connection_status: "",
voiceUser: {
web_userid: user.voiceUser.web_userid,
callernum: user.voiceUser.callernum,
userid: user.voiceUser.userid,
talking: user.voiceUser.talking,
joined: user.voiceUser.joined,
callername: user.voiceUser.callername,
locked: user.voiceUser.locked,
muted: user.voiceUser.muted
},
webcam_stream: user.webcam_stream
}
}, (err, numChanged) => {
let funct;
if(numChanged.insertedId != null) {
funct = function(cbk) {
Meteor.log.info(
`_joining user (case2) userid=[${userId}]:${user.name}. Users.size is now ${Meteor.Users.find({
meetingId: meetingId
}).count()}`
);
return cbk();
};
return funct(callback);
} else {
return callback();
}
});
}
};
this.createDummyUser = function(meetingId, userId, authToken) {
if(Meteor.Users.findOne({
userId: userId,
meetingId: meetingId,
authToken: authToken
}) != null) {
return Meteor.log.info(`html5 user userId:[${userId}] from [${meetingId}] tried to revalidate token`);
} else {
return Meteor.Users.insert({
meetingId: meetingId,
userId: userId,
authToken: authToken,
clientType: "HTML5",
validated: false //will be validated on validate_auth_token_reply
}, (err, id) => {
return Meteor.log.info(`_added a dummy html5 user with: userId=[${userId}] Users.size is now ${Meteor.Users.find({
meetingId: meetingId
}).count()}`);
});
}
};
// 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) {
// send mute requests for the viewer users joined with mic
let i, len, ref, ref1, results, u;
ref1 = (ref = Meteor.Users.find({
meetingId: meetingId,
'user.role': 'VIEWER',
'user.listenOnly': false,
'user.locked': true,
'user.voiceUser.joined': true,
'user.voiceUser.muted': false
})) != null ? ref.fetch() : void 0;
results = [];
for(i = 0, len = ref1.length; i < len; i++) {
u = ref1[i];
// Meteor.log.info u.user.name #
results.push(Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true)); //true for muted
}
return results;
};
// change the locked status of a user (lock settings)
this.setUserLockedStatus = function(meetingId, userId, isLocked) {
let u;
u = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
});
if(u != null) {
Meteor.Users.update({
userId: userId,
meetingId: meetingId
}, {
$set: {
'user.locked': isLocked
}
}, (err, numChanged) => {
if(err != null) {
return Meteor.log.error(`_error ${err} while updating user ${userId} with lock settings`);
} else {
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) {
return Meteor.call('muteUser', meetingId, u.userId, u.userId, u.authToken, true); //true for muted
}
} else {
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) {
if(meetingId != null) {
return Meteor.Users.remove({
meetingId: meetingId
}, err => {
if(err != null) {
return Meteor.log.error(`_error ${JSON.stringify(err)} while removing users from meeting ${meetingId}`);
} else {
return Meteor.log.info(`_cleared Users Collection (meetingId: ${meetingId})!`);
}
});
} else {
return Meteor.Users.remove({}, err => {
if(err != null) {
return Meteor.log.error(`_error ${JSON.stringify(err)} while removing users from all meetings!`);
} else {
return Meteor.log.info("_cleared Users Collection (all meetings)!");
}
});
}
};

View File

@ -1,95 +0,0 @@
# Publish only the online users that are in the particular meetingId
# On the client side we pass the meetingId parameter
Meteor.publish 'users', (meetingId, userid, authToken) ->
Meteor.log.info "attempt publishing users for #{meetingId}, #{userid}, #{authToken}"
u = Meteor.Users.findOne({'userId': userid, 'meetingId': meetingId})
if u?
Meteor.log.info "found it from the first time #{userid}"
if isAllowedTo('subscribeUsers', meetingId, userid, authToken)
Meteor.log.info "#{userid} was allowed to subscribe to 'users'"
username = u?.user?.name or "UNKNOWN"
# offline -> online
if u.user?.connection_status isnt 'online'
Meteor.call "validateAuthToken", meetingId, userid, authToken
Meteor.Users.update({'meetingId':meetingId, 'userId': userid}, {$set:{'user.connection_status': "online"}})
Meteor.log.info "username of the subscriber: " + username + ", connection_status becomes online"
@_session.socket.on("close", Meteor.bindEnvironment(=>
Meteor.log.info "\na user lost connection: session.id=#{@_session.id} userId = #{userid}, username=#{username}, meeting=#{meetingId}"
Meteor.Users.update({'meetingId':meetingId, 'userId': userid}, {$set:{'user.connection_status': "offline"}})
Meteor.log.info "username of the user losing connection: " + username + ", connection_status: becomes offline"
requestUserLeaving meetingId, userid
)
)
#publish the users which are not offline
Meteor.Users.find(
{meetingId: meetingId, 'user.connection_status':{$in: ["online", ""]}},
{fields:{'authToken': false}
})
else
Meteor.log.warn "was not authorized to subscribe to 'users'"
@error(new Meteor.Error(402, "The user was not authorized to subscribe to 'users'"))
else #subscribing before the user was added to the collection
Meteor.call "validateAuthToken", meetingId, userid, authToken
Meteor.log.error "there was no such user #{userid} in #{meetingId}"
Meteor.Users.find(
{meetingId: meetingId, 'user.connection_status':{$in: ["online", ""]}},
{fields:{'authToken': false}
})
Meteor.publish 'chat', (meetingId, userid, authToken) ->
if isAllowedTo('subscribeChat', meetingId, userid, authToken)
Meteor.log.info "publishing chat for #{meetingId} #{userid} #{authToken}"
return Meteor.Chat.find({$or: [
{'message.chat_type': 'PUBLIC_CHAT', 'meetingId': meetingId},
{'message.from_userid': userid, 'meetingId': meetingId},
{'message.to_userid': userid, 'meetingId': meetingId}
]})
else
@error new Meteor.Error(402, "The user was not authorized to subscribe for 'chats'")
return
Meteor.publish 'bbb_poll', (meetingId, userid, authToken) ->
#checking if it is allowed to see Poll Collection in general
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)
Meteor.log.info "publishing Poll for presenter: #{meetingId} #{userid} #{authToken}"
return Meteor.Polls.find({"poll_info.meetingId": meetingId, "poll_info.users": userid})
else
Meteor.log.info "publishing Poll for viewer: #{meetingId} #{userid} #{authToken}"
return Meteor.Polls.find({"poll_info.meetingId": meetingId, "poll_info.users": userid},
{fields: {"poll_info.poll.answers.num_votes": 0}})
else
@error new Meteor.Error(402, "The user was not authorized to subscribe for 'bbb_poll'")
return
Meteor.publish 'shapes', (meetingId) ->
Meteor.Shapes.find({meetingId: meetingId})
Meteor.publish 'slides', (meetingId) ->
Meteor.log.info "publishing slides for #{meetingId}"
Meteor.Slides.find({meetingId: meetingId})
Meteor.publish 'meetings', (meetingId) ->
Meteor.log.info "publishing meetings for #{meetingId}"
Meteor.Meetings.find({meetingId: meetingId})
Meteor.publish 'presentations', (meetingId) ->
Meteor.log.info "publishing presentations for #{meetingId}"
Meteor.Presentations.find({meetingId: meetingId})
Meteor.publish 'bbb_cursor', (meetingId) ->
Meteor.log.info "publishing cursor for #{meetingId}"
Meteor.Cursor.find({meetingId: meetingId})
Meteor.publish 'whiteboard-clean-status', (meetingId) ->
Meteor.log.info "whiteboard clean status #{meetingId}"
Meteor.WhiteboardCleanStatus.find({meetingId: meetingId})

View File

@ -0,0 +1,163 @@
// 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) {
let ref, ref1, u, username;
Meteor.log.info(`attempt publishing users for ${meetingId}, ${userid}, ${authToken}`);
u = Meteor.Users.findOne({
'userId': userid,
'meetingId': meetingId
});
if(u != null) {
Meteor.log.info("found it from the first time " + userid);
if(isAllowedTo('subscribeUsers', meetingId, userid, authToken)) {
Meteor.log.info(`${userid} was allowed to subscribe to 'users'`);
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') {
Meteor.call("validateAuthToken", meetingId, userid, authToken);
}
Meteor.Users.update({
'meetingId': meetingId,
'userId': userid
}, {
$set: {
'user.connection_status': "online"
}
});
Meteor.log.info(`username of the subscriber: ${username}, connection_status becomes online`);
this._session.socket.on("close", Meteor.bindEnvironment((function(_this) {
return function() {
Meteor.log.info(`\na user lost connection: session.id=${_this._session.id} userId = ${userid}, username=${username}, meeting=${meetingId}`);
Meteor.Users.update({
'meetingId': meetingId,
'userId': userid
}, {
$set: {
'user.connection_status': "offline"
}
});
Meteor.log.info(`username of the user losing connection: ${username}, connection_status: becomes offline`);
return requestUserLeaving(meetingId, userid);
};
})(this)));
//publish the users which are not offline
return Meteor.Users.find({
meetingId: meetingId,
'user.connection_status': {
$in: ["online", ""]
}
}, {
fields: {
'authToken': false
}
});
} else {
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'"));
}
} else { //subscribing before the user was added to the collection
Meteor.call("validateAuthToken", meetingId, userid, authToken);
Meteor.log.error(`there was no such user ${userid} in ${meetingId}`);
return Meteor.Users.find({
meetingId: meetingId,
'user.connection_status': {
$in: ["online", ""]
}
}, {
fields: {
'authToken': false
}
});
}
});
Meteor.publish('chat', function(meetingId, userid, authToken) {
if(isAllowedTo('subscribeChat', meetingId, userid, authToken)) {
Meteor.log.info(`publishing chat for ${meetingId} ${userid} ${authToken}`);
return Meteor.Chat.find({
$or: [
{
'message.chat_type': 'PUBLIC_CHAT',
'meetingId': meetingId
}, {
'message.from_userid': userid,
'meetingId': meetingId
}, {
'message.to_userid': userid,
'meetingId': meetingId
}
]
});
} else {
this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'chats'"));
}
});
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)) {
//checking if it is allowed to see a number of votes (presenter only)
if(isAllowedTo('subscribeAnswers', meetingId, userid, authToken)) {
Meteor.log.info("publishing Poll for presenter: " + meetingId + " " + userid + " " + authToken);
return Meteor.Polls.find({
"poll_info.meetingId": meetingId,
"poll_info.users": userid
});
} else {
Meteor.log.info("publishing Poll for viewer: " + meetingId + " " + userid + " " + authToken);
return Meteor.Polls.find({
"poll_info.meetingId": meetingId,
"poll_info.users": userid
}, {
fields: {
"poll_info.poll.answers.num_votes": 0
}
});
}
} else {
this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'bbb_poll'"));
}
});
Meteor.publish('shapes', function(meetingId) {
return Meteor.Shapes.find({
meetingId: meetingId
});
});
Meteor.publish('slides', function(meetingId) {
Meteor.log.info(`publishing slides for ${meetingId}`);
return Meteor.Slides.find({
meetingId: meetingId
});
});
Meteor.publish('meetings', function(meetingId) {
Meteor.log.info(`publishing meetings for ${meetingId}`);
return Meteor.Meetings.find({
meetingId: meetingId
});
});
Meteor.publish('presentations', function(meetingId) {
Meteor.log.info(`publishing presentations for ${meetingId}`);
return Meteor.Presentations.find({
meetingId: meetingId
});
});
Meteor.publish('bbb_cursor', function(meetingId) {
Meteor.log.info(`publishing cursor for ${meetingId}`);
return Meteor.Cursor.find({
meetingId: meetingId
});
});
Meteor.publish('whiteboard-clean-status', function(meetingId) {
Meteor.log.info(`whiteboard clean status ${meetingId}`);
return Meteor.WhiteboardCleanStatus.find({
meetingId: meetingId
});
});

View File

@ -1,88 +0,0 @@
Meteor.methods
# Construct and send a message to bbb-web to validate the user
validateAuthToken: (meetingId, userId, authToken) ->
Meteor.log.info "sending a validate_auth_token with",
userid: userId
authToken: authToken
meetingid: meetingId
message =
"payload":
"auth_token": authToken
"userid": userId
"meeting_id": meetingId
"header":
"timestamp": new Date().getTime()
"reply_to": meetingId + "/" + userId
"name": "validate_auth_token"
if authToken? and userId? and meetingId?
createDummyUser meetingId, userId, authToken
publish Meteor.config.redis.channels.toBBBApps.meeting, message
else
Meteor.log.info "did not have enough information to send a validate_auth_token message"
class Meteor.RedisPubSub
constructor: (callback) ->
Meteor.log.info "constructor RedisPubSub"
@pubClient = redis.createClient()
@subClient = redis.createClient()
Meteor.log.info("Subscribing message on channel: #{Meteor.config.redis.channels.fromBBBApps}")
@subClient.on "psubscribe", Meteor.bindEnvironment(@_onSubscribe)
@subClient.on "pmessage", Meteor.bindEnvironment(@_addToQueue)
@subClient.psubscribe(Meteor.config.redis.channels.fromBBBApps)
callback @
_onSubscribe: (channel, count) =>
Meteor.log.info "Subscribed to #{channel}"
#grab data about all active meetings on the server
message =
"header":
"name": "get_all_meetings_request"
"payload": {} # I need this, otherwise bbb-apps won't recognize the message
publish Meteor.config.redis.channels.toBBBApps.meeting, message
_addToQueue: (pattern, channel, jsonMsg) =>
message = JSON.parse(jsonMsg)
eventName = message.header.name
messagesWeIgnore = [
"BbbPubSubPongMessage"
"bbb_apps_is_alive_message"
"broadcast_layout_message"
]
unless eventName in messagesWeIgnore
console.log "Q #{eventName} #{Meteor.myQueue.total()}"
Meteor.myQueue.add({
pattern: pattern
channel: channel
jsonMsg: jsonMsg
})
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
# message should be an object
@publish = (channel, message) ->
Meteor.log.info "redis outgoing message #{message.header.name}",
channel: channel
message: message
if Meteor.redisPubSub?
Meteor.redisPubSub.pubClient.publish channel, JSON.stringify(message), (err, res) ->
if err
Meteor.log.info "error",
error: err
else
Meteor.log.info "ERROR!! Meteor.redisPubSub was undefined"

View File

@ -0,0 +1,102 @@
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({
// Construct and send a message to bbb-web to validate the user
validateAuthToken(meetingId, userId, authToken) {
let message;
Meteor.log.info("sending a validate_auth_token with", {
userid: userId,
authToken: authToken,
meetingid: meetingId
});
message = {
"payload": {
"auth_token": authToken,
"userid": userId,
"meeting_id": meetingId
},
"header": {
"timestamp": new Date().getTime(),
"reply_to": `${meetingId}/${userId}`,
"name": "validate_auth_token"
}
};
if((authToken != null) && (userId != null) && (meetingId != null)) {
createDummyUser(meetingId, userId, authToken);
return publish(Meteor.config.redis.channels.toBBBApps.meeting, message);
} else {
return Meteor.log.info("did not have enough information to send a validate_auth_token message");
}
}
});
Meteor.RedisPubSub = (function() {
class RedisPubSub {
constructor(callback) {
this._addToQueue = bind(this._addToQueue, this);
this._onSubscribe = bind(this._onSubscribe, this);
Meteor.log.info("constructor RedisPubSub");
this.pubClient = redis.createClient();
this.subClient = redis.createClient();
Meteor.log.info(`Subscribing message on channel: ${Meteor.config.redis.channels.fromBBBApps}`);
this.subClient.on("psubscribe", Meteor.bindEnvironment(this._onSubscribe));
this.subClient.on("pmessage", Meteor.bindEnvironment(this._addToQueue));
this.subClient.psubscribe(Meteor.config.redis.channels.fromBBBApps);
callback(this);
}
_onSubscribe(channel, count) {
let message;
Meteor.log.info(`Subscribed to ${channel}`);
//grab data about all active meetings on the server
message = {
"header": {
"name": "get_all_meetings_request"
},
"payload": {} // I need this, otherwise bbb-apps won't recognize the message
};
return publish(Meteor.config.redis.channels.toBBBApps.meeting, message);
}
_addToQueue(pattern, channel, jsonMsg) {
let eventName, message, messagesWeIgnore;
message = JSON.parse(jsonMsg);
eventName = message.header.name;
messagesWeIgnore = ["BbbPubSubPongMessage", "bbb_apps_is_alive_message", "broadcast_layout_message"];
if(indexOf.call(messagesWeIgnore, eventName) < 0) {
console.log(`Q ${eventName} ${Meteor.myQueue.total()}`);
return Meteor.myQueue.add({
pattern: pattern,
channel: channel,
jsonMsg: jsonMsg
});
}
}
}
return RedisPubSub;
})();
// --------------------------------------------------------------------------------------------
// Private methods on server
// --------------------------------------------------------------------------------------------
// message should be an object
this.publish = function(channel, message) {
Meteor.log.info(`redis outgoing message ${message.header.name}`, {
channel: channel,
message: message
});
if(Meteor.redisPubSub != null) {
return Meteor.redisPubSub.pubClient.publish(channel, JSON.stringify(message), (err, res) => {
if(err) {
return Meteor.log.info("error", {
error: err
});
}
});
} else {
return Meteor.log.info("ERROR!! Meteor.redisPubSub was undefined");
}
};

View File

@ -1,503 +0,0 @@
Meteor.startup ->
Meteor.log.info "server start"
#remove all data
Meteor.WhiteboardCleanStatus.remove({})
clearUsersCollection()
clearChatCollection()
clearMeetingsCollection()
clearShapesCollection()
clearSlidesCollection()
clearPresentationsCollection()
clearPollCollection()
clearCursorCollection()
# create create a PubSub connection, start listening
Meteor.redisPubSub = new Meteor.RedisPubSub(->
Meteor.log.info "created pubsub")
Meteor.myQueue = new PowerQueue({
# autoStart:true
# isPaused: true
})
Meteor.myQueue.taskHandler = (data, next, failures) ->
eventName = JSON.parse(data.jsonMsg)?.header.name
if failures > 0
Meteor.log.error "got a failure on taskHandler #{eventName} #{failures}"
else
handleRedisMessage(data, ()->
length = Meteor.myQueue.length()
lengthString = ->
if length>0
"In the queue we have #{length} event(s) to process."
else ""
Meteor.log.info "in callback after handleRedisMessage #{eventName}.
#{lengthString()}"
next()
)
# 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.
@handleRedisMessage = (data, callback) ->
message = JSON.parse(data.jsonMsg)
# correlationId = message.payload?.reply_to or message.header?.reply_to
meetingId = message.payload?.meeting_id
# 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
meetingId = message.payload?.meeting_id
unless message?.header? and message.payload?
Meteor.log.error "ERROR!! No header or payload"
callback()
unless message.header.name in notLoggedEventTypes
Meteor.log.info "redis incoming message #{eventName} ",
message: data.jsonMsg
# we currently disregard the pattern and channel
if message?.header? and message.payload?
if eventName is 'meeting_created_message'
# Meteor.log.error JSON.stringify message
meetingName = message.payload.name
intendedForRecording = message.payload.recorded
voiceConf = message.payload.voice_conf
duration = message.payload.duration
addMeetingToCollection meetingId, meetingName, intendedForRecording,
voiceConf, duration, callback
# handle voice events
else if message.payload.user? and eventName in [
'user_left_voice_message'
'user_joined_voice_message'
'user_voice_talking_message'
'user_voice_muted_message']
voiceUserObj = {
'web_userid': message.payload.user.voiceUser.web_userid
'listen_only': message.payload.listen_only
'talking': message.payload.user.voiceUser.talking
'joined': message.payload.user.voiceUser.joined
'locked': message.payload.user.voiceUser.locked
'muted': message.payload.user.voiceUser.muted
}
updateVoiceUser meetingId, voiceUserObj, callback
else if eventName is 'user_listening_only'
voiceUserObj = {
'web_userid': message.payload.userid
'listen_only': message.payload.listen_only
}
updateVoiceUser meetingId, voiceUserObj, callback
else if eventName is 'get_all_meetings_reply'
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)
listOfMeetings = message.payload.meetings
# Processing the meetings recursively with a callback to notify us,
# ensuring that we update the meeting collection serially
processMeeting = () ->
meeting = listOfMeetings.pop()
if meeting?
addMeetingToCollection meeting.meetingID, meeting.meetingName,
meeting.recorded, meeting.voiceBridge, meeting.duration, processMeeting
else
callback() # all meeting arrays (if any) have been processed
processMeeting()
else if eventName is 'user_joined_message'
userObj = message.payload.user
dbUser = Meteor.Users.findOne({userId: userObj.userid, 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?.user?.connection_status is "offline" and message.payload.user?.phone_user
Meteor.log.error "offline AND phone user"
callback() #return without joining the user
else
if dbUser?.clientType is "HTML5" # typically html5 users will be in
# the db [as a dummy user] before the joining message
status = dbUser?.validated
Meteor.log.info "in user_joined_message the validStatus
of the user was #{status}"
userObj.timeOfJoining = message.header.current_time
userJoined meetingId, userObj, callback
else
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 is 'get_users_reply' and message.payload.requester_id is 'nodeJSapp'
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 = () ->
user = users.pop()
if user?
user.timeOfJoining = message.header.current_time
if user.emoji_status isnt 'none' and typeof user.emoji_status is 'string'
console.log "3"
user.set_emoji_time = new Date()
userJoined meetingId, user, processUser
else
# console.error("this is not supposed to happen")
userJoined meetingId, user, processUser
else
callback() # all meeting arrays (if any) have been processed
processUser()
else if eventName is 'validate_auth_token_reply'
userId = message.payload.userid
user = Meteor.Users.findOne({userId:userId, meetingId: meetingId})
validStatus = message.payload.valid
# if the user already exists in the db
if user?.clientType is "HTML5"
#if the html5 client user was validated successfully, add a flag
Meteor.Users.update({userId:userId, meetingId:message.payload.meeting_id},
{$set:{validated: validStatus}},
(err, numChanged) ->
if numChanged.insertedId?
funct = (cbk) ->
val=Meteor.Users.findOne({userId:userId, meetingId: message.payload.meeting_id})?.validated
Meteor.log.info "user.validated for user #{userId} in meeting #{user.meetingId} just became #{val}"
cbk()
funct(callback)
else
callback()
)
else
Meteor.log.info "a non-html5 user got validate_auth_token_reply."
callback()
else if eventName is 'user_left_message'
userId = message.payload.user?.userid
if userId? and meetingId?
markUserOffline meetingId, userId, callback
else
callback() #TODO check how to get these cases out and reuse code
# for now not handling this serially #TODO
else if eventName is 'presenter_assigned_message'
newPresenterId = message.payload.new_presenter_id
if newPresenterId?
# reset the previous presenter
Meteor.Users.update({"user.presenter": true, meetingId: meetingId},
{$set: {"user.presenter": false}},
(err, numUpdated) ->
Meteor.log.info(" Updating old presenter numUpdated=#{numUpdated},
err=#{err}")
)
# set the new presenter
Meteor.Users.update({"user.userid": newPresenterId, meetingId: meetingId},
{$set: {"user.presenter": true}},
(err, numUpdated) ->
Meteor.log.info(" Updating new presenter numUpdated=#{numUpdated},
err=#{err}")
)
callback()
# for now not handling this serially #TODO
else if eventName is 'user_emoji_status_message'
userId = message.payload.userid
meetingId = message.payload.meeting_id
emojiStatus = message.payload.emoji_status
if userId? and meetingId?
set_emoji_time = new Date()
Meteor.Users.update({"user.userid": userId},
{$set:{"user.set_emoji_time":set_emoji_time,"user.emoji_status":emojiStatus}},
(err, numUpdated) ->
Meteor.log.info(" Updating emoji numUpdated=#{numUpdated}, err=#{err}")
)
callback()
# for now not handling this serially #TODO
else if eventName in ['user_locked_message', 'user_unlocked_message']
userId = message.payload.userid
isLocked = message.payload.locked
setUserLockedStatus(meetingId, userId, isLocked)
callback()
# for now not handling this serially #TODO
else if eventName in ["meeting_ended_message", "meeting_destroyed_event",
"end_and_kick_all_message", "disconnect_all_users_message"]
Meteor.log.info("DESTROYING MEETING #{meetingId}")
removeMeetingFromCollection meetingId, callback
###
if Meteor.Meetings.findOne({meetingId: meetingId})?
count=Meteor.Users.find({meetingId: meetingId}).count()
Meteor.log.info "there are #{count} users in the meeting"
for user in Meteor.Users.find({meetingId: meetingId}).fetch()
markUserOffline meetingId, user.userId
#TODO should we clear the chat messages for that meeting?!
unless eventName is "disconnect_all_users_message"
removeMeetingFromCollection meetingId
###
# for now not handling this serially #TODO
else if eventName is "get_chat_history_reply" and message.payload.requester_id is "nodeJSapp"
unless Meteor.Meetings.findOne({MeetingId: message.payload.meeting_id})?
for chatMessage in message.payload.chat_history
addChatToCollection meetingId, chatMessage
callback()
# for now not handling this serially #TODO
else if eventName is "send_public_chat_message" or eventName is "send_private_chat_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
addChatToCollection meetingId, messageObject
callback()
# for now not handling this serially #TODO
else if eventName is "presentation_shared_message"
presentationId = message.payload.presentation?.id
# change the currently displayed presentation to presentation.current = false
Meteor.Presentations.update({"presentation.current": true, meetingId: meetingId},
{$set: {"presentation.current": false}})
#update(if already present) entirely the presentation with the fresh data
removePresentationFromCollection meetingId, presentationId
addPresentationToCollection meetingId, message.payload.presentation
for slide in message.payload.presentation?.pages
addSlideToCollection meetingId, message.payload.presentation?.id, slide
if slide.current
displayThisSlide meetingId, slide.id, slide
callback()
# for now not handling this serially #TODO
else if eventName is "get_presentation_info_reply" and message.payload.requester_id is "nodeJSapp"
for presentation in message.payload.presentations
addPresentationToCollection meetingId, presentation
for page in presentation.pages
#add the slide to the collection
addSlideToCollection meetingId, presentation.id, page
#request for shapes
whiteboardId = "#{presentation.id}/#{page.num}" # d2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1404411622872/1
#Meteor.log.info "the whiteboard_id here is:" + whiteboardId
replyTo = "#{meetingId}/nodeJSapp"
message =
"payload":
"meeting_id": meetingId
"requester_id": "nodeJSapp"
"whiteboard_id": whiteboardId
"reply_to": replyTo
"header":
"timestamp": new Date().getTime()
"name": "request_whiteboard_annotation_history_request"
if whiteboardId? and meetingId?
publish Meteor.config.redis.channels.toBBBApps.whiteboard, message #TODO
else
Meteor.log.info "did not have enough information to send a user_leaving_request" #TODO
callback()
# for now not handling this serially #TODO
else if eventName is "presentation_page_changed_message"
newSlide = message.payload.page
displayThisSlide meetingId, newSlide?.id, newSlide
callback()
# for now not handling this serially #TODO
else if eventName is "presentation_removed_message"
presentationId = message.payload.presentation_id
meetingId = message.payload.meeting_id
removePresentationFromCollection meetingId, presentationId
callback()
# for now not handling this serially #TODO
else if eventName is "get_whiteboard_shapes_reply" and message.payload.requester_id is "nodeJSapp"
# Create a whiteboard clean status or find one for the current meeting
if not Meteor.WhiteboardCleanStatus.findOne({meetingId: meetingId})?
Meteor.WhiteboardCleanStatus.insert({meetingId: meetingId, in_progress: false})
for shape in message.payload.shapes
whiteboardId = shape.wb_id
addShapeToCollection meetingId, whiteboardId, shape
callback()
# for now not handling this serially #TODO
else if eventName is "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 is "poll_result" and typeof message.payload.shape.shape.result is 'string'
message.payload.shape.shape.result = JSON.parse message.payload.shape.shape.result
shape = message.payload.shape
whiteboardId = shape?.wb_id
addShapeToCollection meetingId, whiteboardId, shape
callback()
# for now not handling this serially #TODO
else if eventName is "presentation_cursor_updated_message"
cursor =
x: message.payload.x_percent
y: message.payload.y_percent
# update the location of the cursor on the whiteboard
updateCursorLocation(meetingId, cursor)
callback()
# for now not handling this serially #TODO
else if eventName is "whiteboard_cleared_message"
whiteboardId = message.payload.whiteboard_id
Meteor.WhiteboardCleanStatus.update({meetingId: meetingId}, {$set: {'in_progress': true}})
removeAllShapesFromSlide meetingId, whiteboardId
callback()
# for now not handling this serially #TODO
else if eventName is "undo_whiteboard_request"
whiteboardId = message.payload.whiteboard_id
shapeId = message.payload.shape_id
removeShapeFromSlide meetingId, whiteboardId, shapeId
callback()
# for now not handling this serially #TODO
else if eventName is "presentation_page_resized_message"
slideId = message.payload.page?.id
heightRatio = message.payload.page?.height_ratio
widthRatio = message.payload.page?.width_ratio
xOffset = message.payload.page?.x_offset
yOffset = message.payload.page?.y_offset
presentationId = slideId.split("/")[0]
Meteor.Slides.update({presentationId: presentationId, "slide.current": true},
{$set:{"slide.height_ratio": heightRatio,"slide.width_ratio": widthRatio,"slide.x_offset":xOffset,"slide.y_offset":yOffset}}
)
callback()
# for now not handling this serially #TODO
else if eventName is "recording_status_changed_message"
intendedForRecording = message.payload.recorded
currentlyBeingRecorded = message.payload.recording
Meteor.Meetings.update({meetingId: meetingId, intendedForRecording: intendedForRecording},
{$set: {currentlyBeingRecorded: currentlyBeingRecorded}}
)
callback()
# --------------------------------------------------
# lock settings ------------------------------------
# for now not handling this serially #TODO
else if eventName is "eject_voice_user_message"
callback()
# for now not handling this serially #TODO
else if eventName is "new_permission_settings"
oldSettings = Meteor.Meetings.findOne({meetingId:meetingId})?.roomLockSettings
newSettings = message.payload?.permissions
# if the disableMic setting was turned on
if !oldSettings?.disableMic and newSettings.disableMic
handleLockingMic(meetingId, newSettings)
# substitute with the new lock settings
Meteor.Meetings.update({meetingId: meetingId}, {$set: {
'roomLockSettings.disablePrivateChat': newSettings.disablePrivateChat
'roomLockSettings.disableCam': newSettings.disableCam
'roomLockSettings.disableMic': newSettings.disableMic
'roomLockSettings.lockOnJoin': newSettings.lockOnJoin
'roomLockSettings.lockedLayout': newSettings.lockedLayout
'roomLockSettings.disablePublicChat': newSettings.disablePublicChat
'roomLockSettings.lockOnJoinConfigurable': newSettings.lockOnJoinConfigurable #TODO
}})
callback()
# for now not handling this serially #TODO
else if eventName is "poll_started_message"
if message.payload.meeting_id? and message.payload.requester_id? and message.payload.poll?
if Meteor.Meetings.findOne({meetingId: message.payload.meeting_id})?
#initializing the list of current users
users = Meteor.Users.find({meetingId: message.payload.meeting_id},
{fields:{"user.userid": 1, _id: 0}} ).fetch()
addPollToCollection message.payload.poll, message.payload.requester_id,
users, message.payload.meeting_id
callback()
# for now not handling this serially #TODO
else if eventName is "poll_stopped_message"
meetingId = message.payload.meeting_id
poll_id = message.payload.poll_id
clearPollCollection meetingId, poll_id
callback()
# for now not handling this serially #TODO
else if eventName is "user_voted_poll_message"
if message.payload?.poll? and message.payload.meeting_id? and message.payload.presenter_id?
pollObj = message.payload.poll
meetingId = message.payload.meeting_id
requesterId = message.payload.presenter_id
updatePollCollection pollObj, meetingId, requesterId
callback()
# for now not handling this serially #TODO
else if eventName is "poll_show_result_message"
if message.payload.poll.id? and message.payload.meeting_id?
poll_id = message.payload.poll.id
meetingId = message.payload.meeting_id
clearPollCollection meetingId, poll_id
callback()
else # keep moving in the queue
unless eventName in notLoggedEventTypes
Meteor.log.info "WARNING!!!
THE JSON MESSAGE WAS NOT OF TYPE SUPPORTED BY THIS APPLICATION\n
#{eventName} {JSON.stringify message}"
callback()
else
callback()

View File

@ -0,0 +1,607 @@
const 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.startup(() => {
Meteor.log.info("server start");
//remove all data
Meteor.WhiteboardCleanStatus.remove({});
clearUsersCollection();
clearChatCollection();
clearMeetingsCollection();
clearShapesCollection();
clearSlidesCollection();
clearPresentationsCollection();
clearPollCollection();
clearCursorCollection();
// create create a PubSub connection, start listening
Meteor.redisPubSub = new Meteor.RedisPubSub(function() {
return Meteor.log.info("created pubsub");
});
Meteor.myQueue = new PowerQueue({
// autoStart:true
// isPaused: true
});
Meteor.myQueue.taskHandler = function(data, next, failures) {
let eventName, ref;
eventName = (ref = JSON.parse(data.jsonMsg)) != null ? ref.header.name : void 0;
if(failures > 0) {
return Meteor.log.error(`got a failure on taskHandler ${eventName} ${failures}`);
} else {
return handleRedisMessage(data, () => {
let length, lengthString;
length = Meteor.myQueue.length();
lengthString = function() {
if(length > 0) {
return `In the queue we have ${length} event(s) to process.`;
} else {
return "";
}
};
Meteor.log.info(`in callback after handleRedisMessage ${eventName}. ${lengthString()}`);
return next();
});
}
};
// 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) {
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);
// correlationId = message.payload?.reply_to or message.header?.reply_to
meetingId = (ref = message.payload) != null ? ref.meeting_id : void 0;
// 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;
meetingId = (ref1 = message.payload) != null ? ref1.meeting_id : void 0;
if(!(((message != null ? message.header : void 0) != null) && (message.payload != null))) {
Meteor.log.error("ERROR!! No header or payload");
callback();
}
if(ref2 = message.header.name, indexOf.call(notLoggedEventTypes, ref2) < 0) {
Meteor.log.info(`redis incoming message ${eventName} `, {
message: data.jsonMsg
});
}
// we currently disregard the pattern and channel
if(((message != null ? message.header : void 0) != null) && (message.payload != null)) {
if(eventName === 'meeting_created_message') {
// Meteor.log.error JSON.stringify message
meetingName = message.payload.name;
intendedForRecording = message.payload.recorded;
voiceConf = message.payload.voice_conf;
duration = message.payload.duration;
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')) {
voiceUserObj = {
'web_userid': message.payload.user.voiceUser.web_userid,
'listen_only': message.payload.listen_only,
'talking': message.payload.user.voiceUser.talking,
'joined': message.payload.user.voiceUser.joined,
'locked': message.payload.user.voiceUser.locked,
'muted': message.payload.user.voiceUser.muted
};
return updateVoiceUser(meetingId, voiceUserObj, callback);
} else if(eventName === 'user_listening_only') {
voiceUserObj = {
'web_userid': message.payload.userid,
'listen_only': message.payload.listen_only
};
return updateVoiceUser(meetingId, voiceUserObj, callback);
} else if (eventName === 'get_all_meetings_reply') {
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));
listOfMeetings = message.payload.meetings;
// Processing the meetings recursively with a callback to notify us,
// ensuring that we update the meeting collection serially
processMeeting = function() {
let meeting;
meeting = listOfMeetings.pop();
if(meeting != null) {
return addMeetingToCollection(meeting.meetingID, meeting.meetingName, meeting.recorded, meeting.voiceBridge, meeting.duration, processMeeting);
} else {
return callback(); // all meeting arrays (if any) have been processed
}
};
return processMeeting();
} else if(eventName === 'user_joined_message') {
userObj = message.payload.user;
dbUser = Meteor.Users.findOne({
userId: userObj.userid,
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)) {
Meteor.log.error("offline AND phone user");
return callback(); //return without joining the user
} else {
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;
Meteor.log.info(`in user_joined_message the validStatus of the user was ${status}`);
userObj.timeOfJoining = message.header.current_time;
return userJoined(meetingId, userObj, callback);
} else {
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') {
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() {
let user;
user = users.pop();
if(user != null) {
user.timeOfJoining = message.header.current_time;
if(user.emoji_status !== 'none' && typeof user.emoji_status === 'string') {
console.log("3");
user.set_emoji_time = new Date();
return userJoined(meetingId, user, processUser);
} else {
// console.error("this is not supposed to happen")
return userJoined(meetingId, user, processUser);
}
} else {
return callback(); // all meeting arrays (if any) have been processed
}
};
return processUser();
} else if(eventName === 'validate_auth_token_reply') {
userId = message.payload.userid;
user = Meteor.Users.findOne({
userId: userId,
meetingId: meetingId
});
validStatus = message.payload.valid;
// if the user already exists in the db
if((user != null ? user.clientType : void 0) === "HTML5") {
//if the html5 client user was validated successfully, add a flag
return Meteor.Users.update({
userId: userId,
meetingId: message.payload.meeting_id
}, {
$set: {
validated: validStatus
}
}, (err, numChanged) => {
let funct;
if(numChanged.insertedId != null) {
funct = function(cbk) {
let ref5, val;
val = (ref5 = Meteor.Users.findOne({
userId: userId,
meetingId: message.payload.meeting_id
})) != null ? ref5.validated : void 0;
Meteor.log.info(`user.validated for user ${userId} in meeting ${user.meetingId} just became ${val}`);
return cbk();
};
return funct(callback);
} else {
return callback();
}
});
} else {
Meteor.log.info("a non-html5 user got validate_auth_token_reply.");
return callback();
}
} else if(eventName === 'user_left_message') {
userId = (ref5 = message.payload.user) != null ? ref5.userid : void 0;
if((userId != null) && (meetingId != null)) {
return markUserOffline(meetingId, userId, callback);
} else {
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') {
newPresenterId = message.payload.new_presenter_id;
if(newPresenterId != null) {
// reset the previous presenter
Meteor.Users.update({
"user.presenter": true,
meetingId: meetingId
}, {
$set: {
"user.presenter": false
}
}, (err, numUpdated) => {
return Meteor.log.info(` Updating old presenter numUpdated=${numUpdated}, err=${err}`);
});
// set the new presenter
Meteor.Users.update({
"user.userid": newPresenterId,
meetingId: meetingId
}, {
$set: {
"user.presenter": true
}
}, (err, numUpdated) => {
return Meteor.log.info(` Updating new presenter numUpdated=${numUpdated}, err=${err}`);
});
}
return callback();
// for now not handling this serially #TODO
} else if(eventName === 'user_emoji_status_message') {
userId = message.payload.userid;
meetingId = message.payload.meeting_id;
emojiStatus = message.payload.emoji_status;
if((userId != null) && (meetingId != null)) {
set_emoji_time = new Date();
Meteor.Users.update({
"user.userid": userId
}, {
$set: {
"user.set_emoji_time": set_emoji_time,
"user.emoji_status": emojiStatus
}
}, (err, numUpdated) => {
return Meteor.log.info(` Updating emoji numUpdated=${numUpdated}, err=${err}`);
});
}
return callback();
// for now not handling this serially #TODO
} else if(eventName === 'user_locked_message' || eventName === 'user_unlocked_message') {
userId = message.payload.userid;
isLocked = message.payload.locked;
setUserLockedStatus(meetingId, userId, isLocked);
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") {
Meteor.log.info(`DESTROYING MEETING ${meetingId}`);
return removeMeetingFromCollection(meetingId, callback);
/*
if Meteor.Meetings.findOne({meetingId: meetingId})?
count=Meteor.Users.find({meetingId: meetingId}).count()
Meteor.log.info "there are #{count} users in the meeting"
for user in Meteor.Users.find({meetingId: meetingId}).fetch()
markUserOffline meetingId, user.userId
#TODO should we clear the chat messages for that meeting?!
unless eventName is "disconnect_all_users_message"
removeMeetingFromCollection meetingId
*/
// for now not handling this serially #TODO
} else if(eventName === "get_chat_history_reply" && message.payload.requester_id === "nodeJSapp") {
if(Meteor.Meetings.findOne({
MeetingId: message.payload.meeting_id
}) == null) {
ref6 = message.payload.chat_history;
for(i = 0, len = ref6.length; i < len; i++) {
chatMessage = ref6[i];
addChatToCollection(meetingId, chatMessage);
}
}
return callback();
// for now not handling this serially #TODO
} else if(eventName === "send_public_chat_message" || eventName === "send_private_chat_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;
addChatToCollection(meetingId, messageObject);
return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_shared_message") {
presentationId = (ref7 = message.payload.presentation) != null ? ref7.id : void 0;
// change the currently displayed presentation to presentation.current = false
Meteor.Presentations.update({
"presentation.current": true,
meetingId: meetingId
}, {
$set: {
"presentation.current": false
}
});
//update(if already present) entirely the presentation with the fresh data
removePresentationFromCollection(meetingId, presentationId);
addPresentationToCollection(meetingId, message.payload.presentation);
ref9 = (ref8 = message.payload.presentation) != null ? ref8.pages : void 0;
for(j = 0, len1 = ref9.length; j < len1; j++) {
slide = ref9[j];
addSlideToCollection(
meetingId,
(ref10 = message.payload.presentation) != null ? ref10.id : void 0,
slide
);
if(slide.current) {
displayThisSlide(meetingId, slide.id, slide);
}
}
return callback();
// for now not handling this serially #TODO
} else if(eventName === "get_presentation_info_reply" && message.payload.requester_id === "nodeJSapp") {
ref11 = message.payload.presentations;
for(k = 0, len2 = ref11.length; k < len2; k++) {
presentation = ref11[k];
addPresentationToCollection(meetingId, presentation);
ref12 = presentation.pages;
for(l = 0, len3 = ref12.length; l < len3; l++) {
page = ref12[l];
//add the slide to the collection
addSlideToCollection(meetingId, presentation.id, page);
//request for shapes
whiteboardId = `${presentation.id}/${page.num}`; // d2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1404411622872/1
//Meteor.log.info "the whiteboard_id here is:" + whiteboardId
replyTo = `${meetingId}/nodeJSapp`;
message = {
"payload": {
"meeting_id": meetingId,
"requester_id": "nodeJSapp",
"whiteboard_id": whiteboardId,
"reply_to": replyTo
},
"header": {
"timestamp": new Date().getTime(),
"name": "request_whiteboard_annotation_history_request"
}
};
if((whiteboardId != null) && (meetingId != null)) {
publish(Meteor.config.redis.channels.toBBBApps.whiteboard, message);
} else {
Meteor.log.info("did not have enough information to send a user_leaving_request");
}
}
}
return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_page_changed_message") {
newSlide = message.payload.page;
displayThisSlide(meetingId, newSlide != null ? newSlide.id : void 0, newSlide);
return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_removed_message") {
presentationId = message.payload.presentation_id;
meetingId = message.payload.meeting_id;
removePresentationFromCollection(meetingId, presentationId);
return callback();
// for now not handling this serially #TODO
} 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({
meetingId: meetingId
}) == null) {
Meteor.WhiteboardCleanStatus.insert({
meetingId: meetingId,
in_progress: false
});
}
ref13 = message.payload.shapes;
for(m = 0, len4 = ref13.length; m < len4; m++) {
shape = ref13[m];
whiteboardId = shape.wb_id;
addShapeToCollection(meetingId, whiteboardId, shape);
}
return callback();
// for now not handling this serially #TODO
} 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') {
message.payload.shape.shape.result = JSON.parse(message.payload.shape.shape.result);
}
shape = message.payload.shape;
whiteboardId = shape != null ? shape.wb_id : void 0;
addShapeToCollection(meetingId, whiteboardId, shape);
return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_cursor_updated_message") {
cursor = {
x: message.payload.x_percent,
y: message.payload.y_percent
};
// update the location of the cursor on the whiteboard
updateCursorLocation(meetingId, cursor);
return callback();
// for now not handling this serially #TODO
} else if(eventName === "whiteboard_cleared_message") {
whiteboardId = message.payload.whiteboard_id;
Meteor.WhiteboardCleanStatus.update({
meetingId: meetingId
}, {
$set: {
'in_progress': true
}
});
removeAllShapesFromSlide(meetingId, whiteboardId);
return callback();
// for now not handling this serially #TODO
} else if(eventName === "undo_whiteboard_request") {
whiteboardId = message.payload.whiteboard_id;
shapeId = message.payload.shape_id;
removeShapeFromSlide(meetingId, whiteboardId, shapeId);
return callback();
// for now not handling this serially #TODO
} else if(eventName === "presentation_page_resized_message") {
slideId = (ref14 = message.payload.page) != null ? ref14.id : void 0;
heightRatio = (ref15 = message.payload.page) != null ? ref15.height_ratio : void 0;
widthRatio = (ref16 = message.payload.page) != null ? ref16.width_ratio : void 0;
xOffset = (ref17 = message.payload.page) != null ? ref17.x_offset : void 0;
yOffset = (ref18 = message.payload.page) != null ? ref18.y_offset : void 0;
presentationId = slideId.split("/")[0];
Meteor.Slides.update({
presentationId: presentationId,
"slide.current": true
}, {
$set: {
"slide.height_ratio": heightRatio,
"slide.width_ratio": widthRatio,
"slide.x_offset": xOffset,
"slide.y_offset": yOffset
}
});
return callback();
// for now not handling this serially #TODO
} else if(eventName === "recording_status_changed_message") {
intendedForRecording = message.payload.recorded;
currentlyBeingRecorded = message.payload.recording;
Meteor.Meetings.update({
meetingId: meetingId,
intendedForRecording: intendedForRecording
}, {
$set: {
currentlyBeingRecorded: currentlyBeingRecorded
}
});
return callback();
// --------------------------------------------------
// lock settings ------------------------------------
// for now not handling this serially #TODO
} else if(eventName === "eject_voice_user_message") {
return callback();
// for now not handling this serially #TODO
} else if(eventName === "new_permission_settings") {
oldSettings = (ref19 = Meteor.Meetings.findOne({
meetingId: meetingId
})) != null ? ref19.roomLockSettings : 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) {
handleLockingMic(meetingId, newSettings);
}
// substitute with the new lock settings
Meteor.Meetings.update({
meetingId: meetingId
}, {
$set: {
'roomLockSettings.disablePrivateChat': newSettings.disablePrivateChat,
'roomLockSettings.disableCam': newSettings.disableCam,
'roomLockSettings.disableMic': newSettings.disableMic,
'roomLockSettings.lockOnJoin': newSettings.lockOnJoin,
'roomLockSettings.lockedLayout': newSettings.lockedLayout,
'roomLockSettings.disablePublicChat': newSettings.disablePublicChat,
'roomLockSettings.lockOnJoinConfigurable': newSettings.lockOnJoinConfigurable
}
});
return callback();
// for now not handling this serially #TODO
} else if(eventName === "poll_started_message") {
if((message.payload.meeting_id != null) && (message.payload.requester_id != null) && (message.payload.poll != null)) {
if(Meteor.Meetings.findOne({
meetingId: message.payload.meeting_id
}) != null) {
users = Meteor.Users.find({
meetingId: message.payload.meeting_id
}, {
fields: {
"user.userid": 1,
_id: 0
}
}).fetch();
addPollToCollection(
message.payload.poll,
message.payload.requester_id,
users,
message.payload.meeting_id
);
}
}
return callback();
// for now not handling this serially #TODO
} else if(eventName === "poll_stopped_message") {
meetingId = message.payload.meeting_id;
poll_id = message.payload.poll_id;
clearPollCollection(meetingId, poll_id);
return callback();
// for now not handling this serially #TODO
} 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)) {
pollObj = message.payload.poll;
meetingId = message.payload.meeting_id;
requesterId = message.payload.presenter_id;
updatePollCollection(pollObj, meetingId, requesterId);
return callback();
}
// for now not handling this serially #TODO
} else if(eventName === "poll_show_result_message") {
if((message.payload.poll.id != null) && (message.payload.meeting_id != null)) {
poll_id = message.payload.poll.id;
meetingId = message.payload.meeting_id;
clearPollCollection(meetingId, poll_id);
}
return callback();
} else { // keep moving in the queue
if(indexOf.call(notLoggedEventTypes, eventName) < 0) {
Meteor.log.info(`WARNING!!! THE JSON MESSAGE WAS NOT OF TYPE SUPPORTED BY THIS APPLICATION
${eventName} {JSON.stringify message}`);
}
return callback();
}
} else {
return callback();
}
};
});

View File

@ -1,133 +0,0 @@
presenter =
switchSlide: true
#poll
subscribePoll: 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 =
# audio listen only
joinListenOnly: 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
lowerOwnHand : true
# muting
muteSelf : true
unmuteSelf : true
logoutSelf : true
#subscribing
subscribeUsers: true
subscribeChat: true
#chat
chatPublic: true
chatPrivate: true
#poll
subscribePoll: true
subscribeAnswers: false
#emojis
setEmojiStatus: true
clearEmojiStatus: true
#user control
kickUser: 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 = (meetingId, userId) ->
# listen only
joinListenOnly: 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
lowerOwnHand : true
# muting
muteSelf : true
unmuteSelf : !(Meteor.Meetings.findOne({meetingId:meetingId})?.roomLockSettings.disableMic) or
!(Meteor.Users.findOne({meetingId:meetingId, userId:userId})?.user.locked)
logoutSelf : true
#subscribing
subscribeUsers: true
subscribeChat: true
#chat
chatPublic: !(Meteor.Meetings.findOne({meetingId:meetingId})?.roomLockSettings.disablePublicChat) or
!(Meteor.Users.findOne({meetingId:meetingId, userId:userId})?.user.locked) or
Meteor.Users.findOne({meetingId:meetingId, userId:userId})?.user.presenter
chatPrivate: !(Meteor.Meetings.findOne({meetingId:meetingId})?.roomLockSettings.disablePrivateChat) or
!(Meteor.Users.findOne({meetingId:meetingId, userId:userId})?.user.locked) or
Meteor.Users.findOne({meetingId:meetingId, userId:userId})?.user.presenter
#poll
subscribePoll: true
subscribeAnswers: false
#emojis
setEmojiStatus: 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
@isAllowedTo = (action, meetingId, userId, authToken) ->
validated = Meteor.Users.findOne({meetingId:meetingId, userId: userId})?.validated
Meteor.log.info "in isAllowedTo: action-#{action}, userId=#{userId}, authToken=#{authToken} validated:#{validated}"
user = Meteor.Users.findOne({meetingId:meetingId, userId: userId})
# Meteor.log.info "user=" + JSON.stringify user
if user? and authToken is user.authToken # check if the user is who he claims to be
if user.validated and user.clientType is "HTML5"
# PRESENTER
# check presenter specific actions or fallback to regular viewer actions
if user.user?.presenter
Meteor.log.info "user permissions presenter case"
return presenter[action] or viewer(meetingId, userId)[action] or false
# VIEWER
else if user.user?.role is 'VIEWER'
Meteor.log.info "user permissions viewer case"
return viewer(meetingId, userId)[action] or false
# MODERATOR
else if user.user?.role is 'MODERATOR'
Meteor.log.info "user permissions moderator case"
return moderator[action] or false
else
Meteor.log.warn "UNSUCCESSFULL ATTEMPT FROM userid=#{userId} to perform:#{action}"
return false
else
# user was not validated
if action is "logoutSelf"
# on unsuccessful sign-in
Meteor.log.warn "a user was successfully removed from the meeting following an unsuccessful login"
return true
return false
else
Meteor.log.error "in meetingId=#{meetingId} userId=#{userId} tried to perform #{action} without permission" +
"\n..while the authToken was #{user?.authToken} and the user's object is #{JSON.stringify user}"
return false

View File

@ -0,0 +1,163 @@
let moderator, presenter, viewer;
presenter = {
switchSlide: true,
//poll
subscribePoll: 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 = {
// audio listen only
joinListenOnly: 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,
lowerOwnHand: true,
// muting
muteSelf: true,
unmuteSelf: true,
logoutSelf: true,
//subscribing
subscribeUsers: true,
subscribeChat: true,
//chat
chatPublic: true,
chatPrivate: true,
//poll
subscribePoll: true,
subscribeAnswers: false,
//emojis
setEmojiStatus: true,
clearEmojiStatus: true,
//user control
kickUser: 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) {
let ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7;
return {
// listen only
joinListenOnly: 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,
lowerOwnHand: true,
// muting
muteSelf: true,
unmuteSelf: !((ref = Meteor.Meetings.findOne({
meetingId: meetingId
})) != null ? ref.roomLockSettings.disableMic : void 0) || !((ref1 = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
})) != null ? ref1.user.locked : void 0),
logoutSelf: true,
//subscribing
subscribeUsers: true,
subscribeChat: true,
//chat
chatPublic: !((ref2 = Meteor.Meetings.findOne({
meetingId: meetingId
})) != null ? ref2.roomLockSettings.disablePublicChat : void 0) || !((ref3 = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
})) != null ? ref3.user.locked : void 0) || ((ref4 = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
})) != null ? ref4.user.presenter : void 0),
chatPrivate: !((ref5 = Meteor.Meetings.findOne({
meetingId: meetingId
})) != null ? ref5.roomLockSettings.disablePrivateChat : void 0) || !((ref6 = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
})) != null ? ref6.user.locked : void 0) || ((ref7 = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
})) != null ? ref7.user.presenter : void 0),
//poll
subscribePoll: true,
subscribeAnswers: false,
//emojis
setEmojiStatus: 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) {
let ref, ref1, ref2, ref3, user, validated;
validated = (ref = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
})) != null ? ref.validated : void 0;
Meteor.log.info(`in isAllowedTo: action-${action}, userId=${userId}, authToken=${authToken} validated:${validated}`);
user = Meteor.Users.findOne({
meetingId: meetingId,
userId: userId
});
// 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") {
// PRESENTER
// check presenter specific actions or fallback to regular viewer actions
if((ref1 = user.user) != null ? ref1.presenter : void 0) {
Meteor.log.info("user permissions presenter case");
return presenter[action] || viewer(meetingId, userId)[action] || false;
// VIEWER
} else if(((ref2 = user.user) != null ? ref2.role : void 0) === 'VIEWER') {
Meteor.log.info("user permissions viewer case");
return viewer(meetingId, userId)[action] || false;
// MODERATOR
} else if(((ref3 = user.user) != null ? ref3.role : void 0) === 'MODERATOR') {
Meteor.log.info("user permissions moderator case");
return moderator[action] || false;
} else {
Meteor.log.warn(`UNSUCCESSFULL ATTEMPT FROM userid=${userId} to perform:${action}`);
return false;
}
} else {
// user was not validated
if(action === "logoutSelf") {
// on unsuccessful sign-in
Meteor.log.warn("a user was successfully removed from the meeting following an unsuccessful login");
return true;
}
return false;
}
} else {
Meteor.log.error(`..while the authToken was ${user != null ? user.authToken : void 0} and the user's object is ${JSON.stringify(user)}in meetingId=${meetingId} userId=${userId} tried to perform ${action} without permission${"\n..while the authToken was " + (user != null ? user.authToken : void 0) + " and the user's object is " + (JSON.stringify(user))}`);
return false;
}
};