Merge branch 'master' of github.com:bigbluebutton/bigbluebutton
This commit is contained in:
commit
1cb3fe6601
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
105
bigbluebutton-config/bin/bbb-conf
Normal file → Executable 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"
|
||||
|
@ -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
|
||||
|
@ -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">×</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")
|
127
bigbluebutton-html5/app/client/NotificationControl.js
Executable file
127
bigbluebutton-html5/app/client/NotificationControl.js
Executable 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">×</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");
|
||||
};
|
@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
|
||||
@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')
|
910
bigbluebutton-html5/app/client/globals.js
Executable file
910
bigbluebutton-html5/app/client/globals.js
Executable 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
@ -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
|
||||
)()
|
532
bigbluebutton-html5/app/client/lib/bbb_api_bridge.js
Executable file
532
bigbluebutton-html5/app/client/lib/bbb_api_bridge.js
Executable 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;
|
||||
})();
|
@ -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()
|
336
bigbluebutton-html5/app/client/main.js
Executable file
336
bigbluebutton-html5/app/client/main.js
Executable 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();
|
||||
};
|
@ -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, '&').replace(/<(?![au\/])/g, '<').replace(/\/([^au])>/g, '$1>').replace(/([^=])"(?!>)/g, '$1"');
|
||||
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="'
|
309
bigbluebutton-html5/app/client/views/chat/chat_bar.js
Executable file
309
bigbluebutton-html5/app/client/views/chat/chat_bar.js
Executable 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, '&').replace(/<(?![au\/])/g, '<').replace(/\/([^au])>/g, '$1>').replace(/([^=])"(?!>)/g, '$1"');
|
||||
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="');
|
||||
}
|
||||
};
|
@ -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')
|
77
bigbluebutton-html5/app/client/views/modals/modals.js
Executable file
77
bigbluebutton-html5/app/client/views/modals/modals.js
Executable 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -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"
|
52
bigbluebutton-html5/app/client/views/sharedTemplates.js
Executable file
52
bigbluebutton-html5/app/client/views/sharedTemplates.js
Executable 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";
|
||||
}
|
||||
}
|
||||
});
|
@ -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
|
106
bigbluebutton-html5/app/client/views/users/user_item.js
Executable file
106
bigbluebutton-html5/app/client/views/users/user_item.js
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -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')
|
30
bigbluebutton-html5/app/client/views/users/user_list.js
Executable file
30
bigbluebutton-html5/app/client/views/users/user_list.js
Executable 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');
|
||||
}
|
||||
};
|
@ -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()
|
171
bigbluebutton-html5/app/client/views/whiteboard/slide.js
Executable file
171
bigbluebutton-html5/app/client/views/whiteboard/slide.js
Executable 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();
|
||||
}
|
||||
};
|
@ -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 + '`');
|
334
bigbluebutton-html5/app/client/views/whiteboard/whiteboard.js
Executable file
334
bigbluebutton-html5/app/client/views/whiteboard/whiteboard.js
Executable 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 + '`');
|
||||
}
|
||||
});
|
@ -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?
|
47
bigbluebutton-html5/app/client/whiteboard_models/_whiteboard_tool.js
Executable file
47
bigbluebutton-html5/app/client/whiteboard_models/_whiteboard_tool.js
Executable 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;
|
||||
})();
|
@ -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
|
49
bigbluebutton-html5/app/client/whiteboard_models/utils.js
Executable file
49
bigbluebutton-html5/app/client/whiteboard_models/utils.js
Executable 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;
|
||||
};
|
@ -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()
|
83
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_cursor.js
Executable file
83
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_cursor.js
Executable 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;
|
||||
})();
|
@ -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
|
171
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_ellipse.js
Executable file
171
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_ellipse.js
Executable 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;
|
||||
})();
|
@ -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
|
216
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_line.js
Executable file
216
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_line.js
Executable 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;
|
||||
})();
|
@ -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)
|
459
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_paper.js
Executable file
459
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_paper.js
Executable 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;
|
||||
})();
|
@ -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
|
342
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_poll.js
Executable file
342
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_poll.js
Executable 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;
|
||||
};
|
@ -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
|
174
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_rect.js
Executable file
174
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_rect.js
Executable 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;
|
||||
})();
|
@ -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]
|
61
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_slide.js
Executable file
61
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_slide.js
Executable 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;
|
||||
})();
|
@ -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
|
241
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_text.js
Executable file
241
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_text.js
Executable 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;
|
||||
})();
|
@ -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
|
129
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_triangle.js
Executable file
129
bigbluebutton-html5/app/client/whiteboard_models/whiteboard_triangle.js
Executable 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;
|
||||
})();
|
@ -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")
|
17
bigbluebutton-html5/app/collections/collections.js
Executable file
17
bigbluebutton-html5/app/collections/collections.js
Executable 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");
|
@ -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
101
bigbluebutton-html5/app/config.js
Executable 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;
|
@ -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'
|
8
bigbluebutton-html5/app/lib/environment.js
Executable file
8
bigbluebutton-html5/app/lib/environment.js
Executable 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';
|
@ -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
|
144
bigbluebutton-html5/app/lib/router.js
Executable file
144
bigbluebutton-html5/app/lib/router.js
Executable 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"
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
*/
|
@ -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
|
124
bigbluebutton-html5/app/server/collection_methods/chat.js
Executable file
124
bigbluebutton-html5/app/server/collection_methods/chat.js
Executable 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;
|
||||
};
|
@ -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
|
||||
# --------------------------------------------------------------------------------------------
|
54
bigbluebutton-html5/app/server/collection_methods/cursor.js
Executable file
54
bigbluebutton-html5/app/server/collection_methods/cursor.js
Executable 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
|
||||
// --------------------------------------------------------------------------------------------
|
@ -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
|
||||
# --------------------------------------------------------------------------------------------
|
100
bigbluebutton-html5/app/server/collection_methods/meetings.js
Executable file
100
bigbluebutton-html5/app/server/collection_methods/meetings.js
Executable 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
|
||||
// --------------------------------------------------------------------------------------------
|
@ -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}!)")
|
113
bigbluebutton-html5/app/server/collection_methods/poll.js
Executable file
113
bigbluebutton-html5/app/server/collection_methods/poll.js
Executable 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}!)`));
|
||||
}
|
||||
};
|
@ -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
|
||||
# --------------------------------------------------------------------------------------------
|
118
bigbluebutton-html5/app/server/collection_methods/presentations.js
Executable file
118
bigbluebutton-html5/app/server/collection_methods/presentations.js
Executable 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
|
||||
// --------------------------------------------------------------------------------------------
|
@ -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
|
||||
# --------------------------------------------------------------------------------------------
|
153
bigbluebutton-html5/app/server/collection_methods/shapes.js
Executable file
153
bigbluebutton-html5/app/server/collection_methods/shapes.js
Executable 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
|
||||
// --------------------------------------------------------------------------------------------
|
@ -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
|
||||
# --------------------------------------------------------------------------------------------
|
81
bigbluebutton-html5/app/server/collection_methods/slides.js
Executable file
81
bigbluebutton-html5/app/server/collection_methods/slides.js
Executable 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
|
||||
// --------------------------------------------------------------------------------------------
|
@ -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)!"
|
||||
)
|
659
bigbluebutton-html5/app/server/collection_methods/users.js
Executable file
659
bigbluebutton-html5/app/server/collection_methods/users.js
Executable 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)!");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -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})
|
163
bigbluebutton-html5/app/server/publish.js
Executable file
163
bigbluebutton-html5/app/server/publish.js
Executable 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
|
||||
});
|
||||
});
|
@ -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"
|
102
bigbluebutton-html5/app/server/redispubsub.js
Executable file
102
bigbluebutton-html5/app/server/redispubsub.js
Executable 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");
|
||||
}
|
||||
};
|
@ -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()
|
607
bigbluebutton-html5/app/server/server.js
Executable file
607
bigbluebutton-html5/app/server/server.js
Executable 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();
|
||||
}
|
||||
};
|
||||
});
|
@ -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
|
163
bigbluebutton-html5/app/server/user_permissions.js
Executable file
163
bigbluebutton-html5/app/server/user_permissions.js
Executable 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;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user