Merge branch 'master' into fix-chat-auto-reconnect

This commit is contained in:
Richard Alam 2017-12-19 13:19:55 -08:00
commit e2a1cc786f
73 changed files with 1466 additions and 1260 deletions

View File

@ -1,54 +0,0 @@
package org.bigbluebutton.core.apps.presentationpod
import org.bigbluebutton.common2.domain.PresentationVO
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.LiveMeeting
trait GetPresentationInfoReqMsgHdlr extends RightsManagementTrait {
this: PresentationPodHdlrs =>
def handle(msg: GetPresentationInfoReqMsg, state: MeetingState2x,
liveMeeting: LiveMeeting, bus: MessageBus): MeetingState2x = {
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission get presentation info from meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
state
} else {
def buildGetPresentationInfoRespMsg(presentations: Vector[PresentationVO], podId: String,
requesterId: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, requesterId)
val envelope = BbbCoreEnvelope(GetPresentationInfoRespMsg.NAME, routing)
val header = BbbClientMsgHeader(GetPresentationInfoRespMsg.NAME, liveMeeting.props.meetingProp.intId, requesterId)
val body = GetPresentationInfoRespMsgBody(podId, presentations)
val event = GetPresentationInfoRespMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
val requesterId = msg.header.userId
val podId = msg.body.podId
for {
pod <- PresentationPodsApp.getPresentationPod(state, podId)
} yield {
val presInPod = pod.presentations
val presVOs = presInPod.values.map { p =>
PresentationVO(p.id, p.name, p.current, p.pages.values.toVector, p.downloadable)
}.toVector
val event = buildGetPresentationInfoRespMsg(presVOs, podId, requesterId)
bus.outGW.send(event)
}
state
}
}
}

View File

@ -5,7 +5,6 @@ import akka.event.Logging
class PresentationPodHdlrs(implicit val context: ActorContext)
extends CreateNewPresentationPodPubMsgHdlr
with GetPresentationInfoReqMsgHdlr
with GetAllPresentationPodsReqMsgHdlr
with SetCurrentPresentationPubMsgHdlr
with PresentationConversionCompletedSysPubMsgHdlr

View File

@ -186,8 +186,6 @@ class ReceivedJsonMsgHandlerActor(
// Presentation
case SetCurrentPresentationPubMsg.NAME =>
routeGenericMsg[SetCurrentPresentationPubMsg](envelope, jsonNode)
case GetPresentationInfoReqMsg.NAME =>
routeGenericMsg[GetPresentationInfoReqMsg](envelope, jsonNode)
case SetCurrentPagePubMsg.NAME =>
routeGenericMsg[SetCurrentPagePubMsg](envelope, jsonNode)
case ResizeAndMovePagePubMsg.NAME =>

View File

@ -279,7 +279,6 @@ class MeetingActor(
// Presentation Pods
case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: RemovePresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: GetPresentationInfoReqMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: GetAllPresentationPodsReqMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: SetCurrentPresentationPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationConversionCompletedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)

View File

@ -12,10 +12,6 @@ object RemovePresentationPodPubMsg { val NAME = "RemovePresentationPodPubMsg"}
case class RemovePresentationPodPubMsg(header: BbbClientMsgHeader, body: RemovePresentationPodPubMsgBody) extends StandardMsg
case class RemovePresentationPodPubMsgBody(podId: String)
object GetPresentationInfoReqMsg { val NAME = "GetPresentationInfoReqMsg"}
case class GetPresentationInfoReqMsg(header: BbbClientMsgHeader, body: GetPresentationInfoReqMsgBody) extends StandardMsg
case class GetPresentationInfoReqMsgBody(podId: String)
object PresentationUploadTokenReqMsg { val NAME = "PresentationUploadTokenReqMsg"}
case class PresentationUploadTokenReqMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenReqMsgBody) extends StandardMsg
case class PresentationUploadTokenReqMsgBody(podId: String, filename: String)
@ -91,10 +87,6 @@ object RemovePresentationPodEvtMsg { val NAME = "RemovePresentationPodEvtMsg"}
case class RemovePresentationPodEvtMsg(header: BbbClientMsgHeader, body: RemovePresentationPodEvtMsgBody) extends StandardMsg
case class RemovePresentationPodEvtMsgBody(podId: String)
object GetPresentationInfoRespMsg { val NAME = "GetPresentationInfoRespMsg"}
case class GetPresentationInfoRespMsg(header: BbbClientMsgHeader, body: GetPresentationInfoRespMsgBody) extends BbbCoreMsg
case class GetPresentationInfoRespMsgBody(podId: String, presentations: Vector[PresentationVO])
object PresentationUploadTokenPassRespMsg { val NAME = "PresentationUploadTokenPassRespMsg"}
case class PresentationUploadTokenPassRespMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenPassRespMsgBody) extends StandardMsg
case class PresentationUploadTokenPassRespMsgBody(podId: String, authzToken: String, filename: String)

Binary file not shown.

1
bbb-lti/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.asscache

View File

@ -2,4 +2,5 @@
#Fri Aug 19 19:12:11 UTC 2016
app.grails.version=2.5.2
app.name=lti
app.version=0.3
app.servlet.version=3.0
app.version=0.4

View File

@ -15,26 +15,33 @@
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*/
$(document).ready( function () {
$(document).ready(function() {
if (typeof jQuery !== 'undefined') {
(function($) {
$('[data-toggle="confirmation"]').confirmation({popout: true});
$('#recordings').dataTable({
columnDefs: [ {
targets: 3,
render: $.fn.dataTable.render.moment('X', 'LLL', locale)
targets: 4,
render: $.fn.dataTable.render.moment('X', 'lll', locale)
} ],
sPaginationType : "full_numbers",
"columns": [
null,
null,
null,
{ "width": "90px" },
null,
null,
{ "width": "160px" }
{ "width": "40px" },
{ "width": "120px" }
],
"order": [[ 3, "desc" ]]
"order": [[ 4, "desc" ]]
});
$(".glyphicon-eye-open").hover(function() {
$(this).toggleClass('glyphicon-eye-open glyphicon-eye-close');
});
$(".glyphicon-eye-close").hover(function() {
$(this).toggleClass('glyphicon-eye-close glyphicon-eye-open');
});
})(jQuery);
}
};
});

View File

@ -0,0 +1,14 @@
.thumbnail {
height: 51px;
padding: 4px;
width: 66px;
float: left;
}
.thumbnail:hover {
display: inline-block;
height: auto;
position: absolute;
width: auto;
z-index: 99999;
}

View File

@ -1,4 +1,4 @@
/*
/*
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@ -14,7 +14,7 @@
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*/
*/
grails.servlet.version = "3.0" // Change depending on target container compliance (2.5 or 3.0)
grails.project.class.dir = "target/classes"
@ -65,6 +65,7 @@ grails.project.dependency.resolution = {
}
dependencies {
compile 'org.json:json:20171018'
// specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes e.g.
// runtime 'mysql:mysql-connector-java:5.1.29'
// runtime 'org.postgresql:postgresql:9.3-1101-jdbc41'

View File

@ -1,4 +1,4 @@
/*
/*
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@ -14,7 +14,7 @@
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*/
*/
// locations to search for config files that get merged into the main config;
// config files can be ConfigSlurper scripts, Java properties files, or classes
@ -103,6 +103,9 @@ grails.hibernate.pass.readonly = false
// configure passing read-only to OSIV session by default, requires "singleSession = false" OSIV mode
grails.hibernate.osiv.readonly = false
// Enable hot reloading for production environments
grails.gsp.enable.reload=true
environments {
development {
grails.logging.jul.usebridge = true

View File

@ -48,125 +48,103 @@ class ToolController {
def index = {
log.debug CONTROLLER_NAME + "#index"
if( ltiService.consumerMap == null) ltiService.initConsumerMap()
if (ltiService.consumerMap == null) {
ltiService.initConsumerMap()
}
setLocalization(params)
params.put(REQUEST_METHOD, request.getMethod().toUpperCase())
ltiService.logParameters(params)
if( request.post ){
def scheme = request.isSecure()? "https": "http"
def endPoint = scheme + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null? "." + params.get("format"): "")
log.info "endPoint: " + endPoint
Map<String, String> result = new HashMap<String, String>()
ArrayList<String> missingParams = new ArrayList<String>()
if (hasAllRequiredParams(params, missingParams)) {
def sanitizedParams = sanitizePrametersForBaseString(params)
def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
if ( !ltiService.hasRestrictedAccess() || consumer != null) {
if (ltiService.hasRestrictedAccess() ) {
log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
}
if (!ltiService.hasRestrictedAccess() || checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))) {
if (!ltiService.hasRestrictedAccess() ) {
log.debug "Access not restricted, valid signature is not required."
} else {
log.debug "The message has a valid signature."
}
def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
if( !"extended".equals(mode) ) {
log.debug "LTI service running in simple mode."
result = doJoinMeeting(params)
} else {
log.debug "LTI service running in extended mode."
if ( !Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault() ) {
log.debug "Parameter custom_record was not sent; immediately redirecting to BBB session!"
result = doJoinMeeting(params)
}
}
} else {
log.debug "The message has NOT a valid signature."
result.put("resultMessageKey", "InvalidSignature")
result.put("resultMessage", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
}
} else {
result.put("resultMessageKey", "ConsumerNotFound")
result.put("resultMessage", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
}
} else {
String missingStr = ""
for(String str:missingParams) {
missingStr += str + ", ";
}
result.put("resultMessageKey", "MissingRequiredParameter")
result.put("resultMessage", "Missing parameters [$missingStr]")
}
if( result.containsKey("resultMessageKey") ) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
} else {
session["params"] = params
render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
}
} else {
// On get requests render the common cartridge.
if (request.get) {
render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
return
}
// On post request proceed with the launch.
def endPoint = ltiService.getScheme(request) + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null ? "." + params.get("format") : "")
log.info "endPoint: " + endPoint
ArrayList<String> missingParams = new ArrayList<String>()
if (!hasAllRequiredParams(params, missingParams)) {
String missingStr = ""
for (String str:missingParams) {
missingStr += str + ", ";
}
return renderError("MissingRequiredParameter", "Missing parameters [$missingStr]")
}
def sanitizedParams = sanitizePrametersForBaseString(params)
def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
if (ltiService.hasRestrictedAccess()) {
if (consumer == null) {
return renderError("ConsumerNotFound", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
}
log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
}
def validSignature = checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))
if (ltiService.hasRestrictedAccess()) {
if (!validSignature) {
log.debug "The message has NOT a valid signature."
return renderError("InvalidSignature", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
}
log.debug "The message has a valid signature."
} else {
log.debug "Access not restricted, valid signature is not required."
}
def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
if (!"extended".equals(mode)) {
log.debug "LTI service running in simple mode."
def result = doJoinMeeting(params)
return
}
log.debug "LTI service running in extended mode."
if (!Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault()) {
log.debug "Parameter custom_record was not sent; immediately redirecting to BBB session!"
def result = doJoinMeeting(params)
return
}
session["params"] = params
render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
}
def join = {
if( ltiService.consumerMap == null) ltiService.initConsumerMap()
log.debug CONTROLLER_NAME + "#join"
Map<String, String> result
def result
def sessionParams = session["params"]
if( sessionParams != null ) {
log.debug "params: " + params
log.debug "sessionParams: " + sessionParams
result = doJoinMeeting(sessionParams)
} else {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidSession")
result.put("resultMessage", "Invalid session. User can not execute this action.")
result.put("messageKey", "InvalidSession")
result.put("message", "Invalid session. User can not execute this action.")
}
if( result.containsKey("resultMessageKey")) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
if (result != null && result.containsKey("messageKey")) {
log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
}
}
def publish = {
log.debug CONTROLLER_NAME + "#publish"
Map<String, String> result
def sessionParams = session["params"]
if( sessionParams == null ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidSession")
result.put("resultMessage", "Invalid session. User can not execute this action.")
result.put("messageKey", "InvalidSession")
result.put("message", "Invalid session. User can not execute this action.")
} else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "NotAllowed")
result.put("resultMessage", "User not allowed to execute this action.")
result.put("messageKey", "NotAllowed")
result.put("message", "User not allowed to execute this action.")
} else {
//Execute the publish command
// Execute the publish command
result = bigbluebuttonService.doPublishRecordings(params)
}
if( result.containsKey("resultMessageKey")) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
if( result.containsKey("messageKey")) {
log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
} else {
render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
}
@ -175,25 +153,22 @@ class ToolController {
def delete = {
log.debug CONTROLLER_NAME + "#delete"
Map<String, String> result
def sessionParams = session["params"]
if( sessionParams == null ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidSession")
result.put("resultMessage", "Invalid session. User can not execute this action.")
result.put("messageKey", "InvalidSession")
result.put("message", "Invalid session. User can not execute this action.")
} else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "NotAllowed")
result.put("resultMessage", "User not allowed to execute this action.")
result.put("messageKey", "NotAllowed")
result.put("message", "User not allowed to execute this action.")
} else {
//Execute the delete command
// Execute the delete command.
result = bigbluebuttonService.doDeleteRecordings(params)
}
if( result.containsKey("resultMessageKey")) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
if( result.containsKey("messageKey")) {
log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
} else {
render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
}
@ -203,48 +178,39 @@ class ToolController {
String locale = params.get(Parameter.LAUNCH_LOCALE)
locale = (locale == null || locale.equals("")?"en":locale)
String[] localeCodes = locale.split("_")
//Localize the default welcome message
if( localeCodes.length > 1 )
// Localize the default welcome message
session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
if (localeCodes.length > 1) {
session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0], localeCodes[1])
else
session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
}
}
private Object doJoinMeeting(Map<String, String> params) {
Map<String, String> result = new HashMap<String, String>()
setLocalization(params)
String welcome = message(code: "bigbluebutton.welcome.header", args: ["\"{0}\"", "\"{1}\""]) + "<br>"
// Check for [custom_]welcome parameter being passed from the LTI
if ( params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null ) {
if (params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null) {
welcome = params.get(Parameter.CUSTOM_WELCOME) + "<br>"
log.debug "Overriding default welcome message with: [" + welcome + "]"
}
if ( params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault() ) {
if (params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()) {
welcome += "<br><b>" + message(code: "bigbluebutton.welcome.record") + "</b><br>"
log.debug "Adding record warning to welcome message, welcome is now: [" + welcome + "]"
}
if ( params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0 ) {
if (params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0) {
welcome += "<br><b>" + message(code: "bigbluebutton.welcome.duration", args: [params.get(Parameter.CUSTOM_DURATION)]) + "</b><br>"
log.debug "Adding duration warning to welcome message, welcome is now: [" + welcome + "]"
}
welcome += "<br>" + message(code: "bigbluebutton.welcome.footer") + "<br>"
String destinationURL = bigbluebuttonService.getJoinURL(params, welcome, ltiService.mode)
log.debug "redirecting to " + destinationURL
if( destinationURL != null ) {
redirect(url:destinationURL)
} else {
result.put("resultMessageKey", "BigBlueButtonServerError")
result.put("resultMessage", "The join could not be completed")
if (destinationURL == null) {
Map<String, String> result = new HashMap<String, String>()
result.put("messageKey", "BigBlueButtonServerError")
result.put("message", "The join could not be completed")
return result
}
return result
log.debug "It is redirecting to " + destinationURL
redirect(url:destinationURL)
}
/**
@ -258,14 +224,15 @@ class ToolController {
if (key == "action" || key == "controller" || key == "format") {
// Ignore as these are the grails controller and action tied to this request.
continue
} else if (key == "oauth_signature") {
// We don't need this as part of the base string
continue
} else if (key == "request_method") {
// As this is was added by the controller, we don't want it as part of the base string
}
if (key == "oauth_signature") {
// We don't need this as part of the base string.
continue
}
if (key == "request_method") {
// As this is was added by the controller, we don't want it as part of the base string.
continue
}
reqProp.setProperty(key, params.get(key));
}
return reqProp
@ -279,24 +246,19 @@ class ToolController {
*/
private boolean hasAllRequiredParams(Map<String, String> params, ArrayList<String> missingParams) {
log.debug "Checking for required parameters"
boolean hasAllParams = true
if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID) ) {
if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID)) {
missingParams.add(Parameter.CONSUMER_ID);
hasAllParams = false;
return false
}
if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
missingParams.add(Parameter.OAUTH_SIGNATURE);
hasAllParams = false;
return false
}
if ( !params.containsKey(Parameter.RESOURCE_LINK_ID) ) {
if (!params.containsKey(Parameter.RESOURCE_LINK_ID)) {
missingParams.add(Parameter.RESOURCE_LINK_ID);
hasAllParams = false;
return false
}
return hasAllParams
return true
}
/**
@ -309,32 +271,23 @@ class ToolController {
* @return - TRUE if the signatures matches the calculated signature
*/
private boolean checkValidSignature(String method, String url, String conSecret, Properties postProp, String signature) {
def validSignature = false
if ( ltiService.hasRestrictedAccess() ) {
try {
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
//log.debug "OAuthMessage oam = " + oam.toString()
HMAC_SHA1 hmac = new HMAC_SHA1()
//log.debug "HMAC_SHA1 hmac = " + hmac.toString()
hmac.setConsumerSecret(conSecret)
log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
log.debug "Calculated: " + calculatedSignature + " Received: " + signature
validSignature = calculatedSignature.equals(signature)
} catch( Exception e ) {
log.debug "Exception error: " + e.message
}
} else {
validSignature = true
if (!ltiService.hasRestrictedAccess()) {
return true;
}
try {
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
//log.debug "OAuthMessage oam = " + oam.toString()
HMAC_SHA1 hmac = new HMAC_SHA1()
//log.debug "HMAC_SHA1 hmac = " + hmac.toString()
hmac.setConsumerSecret(conSecret)
log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
log.debug "Calculated: " + calculatedSignature + " Received: " + signature
return calculatedSignature.equals(signature)
} catch( Exception e ) {
log.debug "Exception error: " + e.message
return false
}
return validSignature
}
/**
@ -343,26 +296,50 @@ class ToolController {
* @return the key:val pairs needed for Basic LTI
*/
private List<Object> getSanitizedRecordings(Map<String, String> params) {
List<Object> recordings = bigbluebuttonService.getRecordings(params)
for(Map<String, Object> recording: recordings){
/// Calculate duration
def recordings = new ArrayList<Object>()
def getRecordingsResponse = bigbluebuttonService.getRecordings(params)
if (getRecordingsResponse == null) {
return recordings
}
Object response = (Object)getRecordingsResponse.get("recording")
if (response instanceof Map<?,?>) {
recordings.add(response)
}
if (response instanceof Collection<?>) {
recordings = response
}
// Sanitize recordings
for (recording in recordings) {
// Calculate duration.
long endTime = Long.parseLong((String)recording.get("endTime"))
endTime -= (endTime % 1000)
long startTime = Long.parseLong((String)recording.get("startTime"))
startTime -= (startTime % 1000)
int duration = (endTime - startTime) / 60000
/// Add duration
// Add duration.
recording.put("duration", duration )
/// Calculate reportDate
// Calculate reportDate.
DateFormat df = new SimpleDateFormat(message(code: "tool.view.dateFormat"))
String reportDate = df.format(new Date(startTime))
/// Add reportDate
// Add reportDate.
recording.put("reportDate", reportDate)
recording.put("unixDate", startTime / 1000)
// Add sanitized thumbnails
recording.put("thumbnails", sanitizeThumbnails(recording.playback.format))
}
return recordings
}
private List<Object> sanitizeThumbnails(Object format) {
if (format.preview == null || format.preview.images == null || format.preview.images.image == null) {
return new ArrayList()
}
if (format.preview.images.image instanceof Map<?,?>) {
return new ArrayList(format.preview.images.image)
}
return format.preview.images.image
}
private String getCartridgeXML(){
def lti_endpoint = ltiService.retrieveBasicLtiEndpoint() + '/' + grailsApplication.metadata['app.name']
def launch_url = 'http://' + lti_endpoint + '/tool'
@ -399,4 +376,9 @@ class ToolController {
return cartridge
}
private void renderError(key, message) {
log.debug "Error [resultMessageKey:'" + key + "', resultMessage:'" + message + "']"
render(view: "error", model: ['resultMessageKey': key, 'resultMessage': message])
}
}

View File

@ -17,7 +17,7 @@
#
# The welcome.header can be static, however if you want the name of the activity (meeting) to be injected use {0} as part of the text
# {1} can be used to inject the name of the course
# {1} can be used to inject the name of the course
bigbluebutton.welcome.header=Welcome to <b>{0}</b>!
bigbluebutton.welcome.footer=To understand how BigBlueButton works see our <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the headset icon (upper-left hand corner). <b>Please use a headset to avoid causing noise for others.
bigbluebutton.welcome.record=This meeting is being recorded
@ -38,9 +38,10 @@ tool.view.recording.unpublish=Unpublish
tool.view.recording.delete=Delete
tool.view.activity=Activity
tool.view.description=Description
tool.view.preview=Preview
tool.view.date=Date
tool.view.duration=Duration
tool.view.actions=Actions
tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
tool.error.general=Connection could not be established.
tool.error.general=Connection could not be established.

View File

@ -39,9 +39,10 @@ tool.view.recording.confirmation.yes=Si
tool.view.recording.confirmation.no=No
tool.view.activity=Actividad
tool.view.description=Descripci&#243;n
tool.view.preview=Vista preliminar
tool.view.date=Fecha
tool.view.duration=Duraci&#243;n
tool.view.actions=Acciones
tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
tool.error.general=No pudo estableserce la conexi&#243;n.
tool.error.general=No pudo estableserce la conexi&#243;n.

View File

@ -1,42 +1,43 @@
#
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
#
# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
#
bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla<6C>t utiliser le casque pour &#233;viter de causer du bruit.</b>
tool.view.app=BigBlueButton
tool.view.title=LTI Interface pour BigBlueButton
tool.view.join=Saisie de la r&#233;union
tool.view.recording=Enregistrement
tool.view.recording.format.presentation=presentation
tool.view.recording.format.video=video
tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
tool.view.recording.delete.confirmation.warning=Attention
tool.view.recording.delete.confirmation.yes=Oui
tool.view.recording.delete.confirmation.no=Non
tool.view.recording.publish=Publier
tool.view.recording.unpublish=D&#233;publier
tool.view.recording.delete=Supprimer
tool.view.activity=Activit&#233;
tool.view.description=Description
tool.view.date=Date
tool.view.duration=Dur&#233;e
tool.view.actions=Actions
tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
tool.error.general=Pas possible &#233;tablir la connection.
#
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
#
# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
#
bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla<6C>t utiliser le casque pour &#233;viter de causer du bruit.</b>
tool.view.app=BigBlueButton
tool.view.title=LTI Interface pour BigBlueButton
tool.view.join=Saisie de la r&#233;union
tool.view.recording=Enregistrement
tool.view.recording.format.presentation=presentation
tool.view.recording.format.video=video
tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
tool.view.recording.delete.confirmation.warning=Attention
tool.view.recording.delete.confirmation.yes=Oui
tool.view.recording.delete.confirmation.no=Non
tool.view.recording.publish=Publier
tool.view.recording.unpublish=D&#233;publier
tool.view.recording.delete=Supprimer
tool.view.activity=Activit&#233;
tool.view.description=Description
tool.view.preview=Apre&#231;u
tool.view.date=Date
tool.view.duration=Dur&#233;e
tool.view.actions=Actions
tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
tool.error.general=Pas possible &#233;tablir la connection.

View File

@ -34,6 +34,10 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@ -63,20 +67,20 @@ class BigbluebuttonService {
try {
docBuilder = docBuilderFactory.newDocumentBuilder()
} catch (ParserConfigurationException e) {
logger.error("Failed to initialise BaseProxy", e)
log.error("Failed to initialise BaseProxy", e)
}
//Instantiate bbbProxy and initialize it with default url and salt
bbbProxy = new Proxy(url, salt)
}
public String getJoinURL(params, welcome, mode){
//Set the injected values
if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
String joinURL = null
// Set the injected values
if (!url.equals(bbbProxy.url) && !url.equals("")) {
bbbProxy.setUrl(url)
}
if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
bbbProxy.setSalt(salt)
}
String meetingName = getValidatedMeetingName(params.get(Parameter.RESOURCE_LINK_TITLE))
String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
String attendeePW = DigestUtils.shaHex("ap" + params.get(Parameter.RESOURCE_LINK_ID) + params.get(Parameter.CONSUMER_ID))
@ -86,7 +90,6 @@ class BigbluebuttonService {
String userFullName = getValidatedUserFullName(params, isModerator)
String courseTitle = getValidatedCourseTitle(params.get(Parameter.COURSE_TITLE))
String userID = getValidatedUserId(params.get(Parameter.USER_ID))
Integer voiceBridge = 0
String record = false
Integer duration = 0
@ -95,98 +98,93 @@ class BigbluebuttonService {
record = getValidatedBBBRecord(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()
duration = getValidatedBBBDuration(params.get(Parameter.CUSTOM_DURATION))
}
Boolean allModerators = Boolean.valueOf(false)
if ( params.containsKey(Parameter.CUSTOM_ALL_MODERATORS) ) {
allModerators = Boolean.parseBoolean(params.get(Parameter.CUSTOM_ALL_MODERATORS))
}
String[] values = [meetingName, courseTitle]
String welcomeMsg = MessageFormat.format(welcome, values)
String meta = getMonitoringMetaData(params)
String createURL = getCreateURL( meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta )
log.debug "createURL: " + createURL
Map<String, Object> createResponse = doAPICall(createURL)
log.debug "createResponse: " + createResponse
if( createResponse != null){
String returnCode = (String) createResponse.get("returncode")
String messageKey = (String) createResponse.get("messageKey")
if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
(Proxy.APIRESPONSE_FAILED.equals(returnCode) && (Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) || Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey)) ) ){
joinURL = bbbProxy.getJoinURL( userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) createResponse.get("createTime"), userID);
}
String createURL = getCreateURL(meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta)
Map<String, Object> responseAPICall = doAPICall(createURL)
log.info "responseAPICall: " + responseAPICall
if (responseAPICall == null) {
return null
}
Object response = (Object)responseAPICall.get("response")
String returnCode = (String)response.get("returncode")
String messageKey = (String)response.get("messageKey")
if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
!Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) &&
!Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey) &&
!"".equals(messageKey)) {
return null
}
def joinURL = bbbProxy.getJoinURL(userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) response.get("createTime"), userID)
log.info "joinURL: " + joinURL
return joinURL
}
public Object getRecordings(params){
//Set the injected values
if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
String recordingsURL = bbbProxy.getGetRecordingsURL( meetingID )
log.debug "recordingsURL: " + recordingsURL
Map<String, Object> recordings = doAPICall(recordingsURL)
if( recordings != null){
String returnCode = (String) recordings.get("returncode")
String messageKey = (String) recordings.get("messageKey")
if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) && messageKey == null ){
return recordings.get("recordings")
}
public Object getRecordings(params) {
// Set the injected values
if (!url.equals(bbbProxy.url) && !url.equals("")) {
bbbProxy.setUrl(url)
}
return null
if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
bbbProxy.setSalt(salt)
}
String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
String recordingsURL = bbbProxy.getGetRecordingsURL(meetingID)
Map<String, Object> responseAPICall = doAPICall(recordingsURL)
if (responseAPICall == null) {
return null
}
Object response = (Object)responseAPICall.get("response")
String returnCode = (String)response.get("returncode")
String messageKey = (String)response.get("messageKey")
if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) || messageKey != null) {
return null
}
Object recordings = (Object)response.get("recordings")
return recordings
}
public Object doDeleteRecordings(params){
//Set the injected values
if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
Map<String, Object> result
String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
if( !recordingId.equals("") ){
String deleteRecordingsURL = bbbProxy.getDeleteRecordingsURL( recordingId )
log.debug "deleteRecordingsURL: " + deleteRecordingsURL
result = doAPICall(deleteRecordingsURL)
} else {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidRecordingId")
result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
// Set the injected values
if (!url.equals(bbbProxy.url) && !url.equals("")) {
bbbProxy.setUrl(url)
}
if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
bbbProxy.setSalt(salt)
}
String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
if (!recordingId.equals("")) {
String deleteRecordingsURL = bbbProxy.getDeleteRecordingsURL( recordingId )
return doAPICall(deleteRecordingsURL)
}
def result = new HashMap<String, String>()
result.put("messageKey", "InvalidRecordingId")
result.put("message", "RecordingId is invalid. The recording can not be deleted.")
return result
}
public Object doPublishRecordings(params){
//Set the injected values
if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
Map<String, Object> result
// Set the injected values
if (!url.equals(bbbProxy.url) && !url.equals("")) {
bbbProxy.setUrl(url)
}
if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
bbbProxy.setSalt(salt)
}
String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
String publish = getValidatedBBBRecordingPublished(params.get(Parameter.BBB_RECORDING_PUBLISHED))
if( !recordingId.equals("") ){
String publishRecordingsURL = bbbProxy.getPublishRecordingsURL( recordingId, "true".equals(publish)?"false":"true" )
log.debug "publishRecordingsURL: " + publishRecordingsURL
result = doAPICall(publishRecordingsURL)
} else {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidRecordingId")
result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
return doAPICall(publishRecordingsURL)
}
def result = new HashMap<String, String>()
result.put("messageKey", "InvalidRecordingId")
result.put("message", "RecordingId is invalid. The recording can not be deleted.")
return result
}
@ -219,14 +217,14 @@ class BigbluebuttonService {
String userFirstName = params.get(Parameter.USER_FIRSTNAME)
String userLastName = params.get(Parameter.USER_LASTNAME)
if( userFullName == null || userFullName == "" ){
if( userFirstName != null && userFirstName != "" ){
if (userFirstName != null && userFirstName != "") {
userFullName = userFirstName
}
if( userLastName != null && userLastName != "" ){
if (userLastName != null && userLastName != "") {
userFullName += userFullName.length() > 0? " ": ""
userFullName += userLastName
}
if( userFullName == null || userFullName == "" ){
if (userFullName == null || userFullName == "") {
userFullName = isModerator? "Moderator" : "Attendee"
}
}
@ -263,8 +261,7 @@ class BigbluebuttonService {
private String getMonitoringMetaData(params){
String meta
meta = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
meta = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
meta += "&meta_originVersion=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_VERSION) == null? "": params.get(Parameter.TOOL_CONSUMER_VERSION))
meta += "&meta_originServerCommonName=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION))
meta += "&meta_originServerUrl=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL))
@ -272,25 +269,21 @@ class BigbluebuttonService {
meta += "&meta_contextId=" + bbbProxy.getStringEncoded(params.get(Parameter.COURSE_ID) == null? "": params.get(Parameter.COURSE_ID))
meta += "&meta_contextActivity=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_TITLE) == null? "": params.get(Parameter.RESOURCE_LINK_TITLE))
meta += "&meta_contextActivityDescription=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_DESCRIPTION) == null? "": params.get(Parameter.RESOURCE_LINK_DESCRIPTION))
return meta
}
/** Make an API call */
private Map<String, Object> doAPICall(String query) {
StringBuilder urlStr = new StringBuilder(query);
try {
// open connection
//log.debug("doAPICall.call: " + query );
log.debug("doAPICall.call: " + query );
URL url = new URL(urlStr.toString());
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setUseCaches(false);
httpConnection.setDoOutput(true);
httpConnection.setRequestMethod("GET");
httpConnection.connect();
int responseCode = httpConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// read response
@ -302,35 +295,27 @@ class BigbluebuttonService {
reader = new BufferedReader(isr);
String line = reader.readLine();
while (line != null) {
if( !line.startsWith("<?xml version=\"1.0\"?>"))
if( !line.startsWith("<?xml version=\"1.0\"?>")) {
xml.append(line.trim());
}
line = reader.readLine();
}
} finally {
if (reader != null)
if (reader != null) {
reader.close();
if (isr != null)
}
if (isr != null) {
isr.close();
}
}
httpConnection.disconnect();
// parse response
// Parse response.
//log.debug("doAPICall.responseXml: " + xml);
//Patch to fix the NaN error
String stringXml = xml.toString();
stringXml = stringXml.replaceAll(">.\\s+?<", "><");
Document dom = null;
dom = docBuilder.parse(new InputSource( new StringReader(stringXml)));
Map<String, Object> response = getNodesAsMap(dom, "response");
//log.debug("doAPICall.responseMap: " + response);
String returnCode = (String) response.get("returncode");
if (Proxy.APIRESPONSE_FAILED.equals(returnCode)) {
log.debug("doAPICall." + (String) response.get("messageKey") + ": Message=" + (String) response.get("message"));
}
JSONObject rootJSON = XML.toJSONObject(stringXml);
Map<String, Object> response = jsonToMap(rootJSON);
return response;
} else {
log.debug("doAPICall.HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode);
@ -346,43 +331,43 @@ class BigbluebuttonService {
}
}
/** Get all nodes under the specified element tag name as a Java map */
protected Map<String, Object> getNodesAsMap(Document dom, String elementTagName) {
Node firstNode = dom.getElementsByTagName(elementTagName).item(0);
return processNode(firstNode);
protected Map<String, Object> jsonToMap(JSONObject json) throws JSONException {
Map<String, Object> retMap = new HashMap<String, Object>();
if(json != JSONObject.NULL) {
retMap = toMap(json);
}
return retMap;
}
protected Map<String, Object> processNode(Node _node) {
protected Map<String, Object> toMap(JSONObject object) throws JSONException {
Map<String, Object> map = new HashMap<String, Object>();
NodeList responseNodes = _node.getChildNodes();
for (int i = 0; i < responseNodes.getLength(); i++) {
Node node = responseNodes.item(i);
String nodeName = node.getNodeName().trim();
if (node.getChildNodes().getLength() == 1
&& ( node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.TEXT_NODE || node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.CDATA_SECTION_NODE) ) {
String nodeValue = node.getTextContent();
map.put(nodeName, nodeValue != null ? nodeValue.trim() : null);
} else if (node.getChildNodes().getLength() == 0
&& node.getNodeType() != org.w3c.dom.Node.TEXT_NODE
&& node.getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE) {
map.put(nodeName, "");
} else if ( node.getChildNodes().getLength() >= 1
&& node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.TEXT_NODE
&& node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE ) {
List<Object> list = new ArrayList<Object>();
for (int c = 0; c < node.getChildNodes().getLength(); c++) {
Node n = node.getChildNodes().item(c);
list.add(processNode(n));
}
map.put(nodeName, list);
} else {
map.put(nodeName, processNode(node));
Iterator<String> keysItr = object.keys();
while(keysItr.hasNext()) {
String key = keysItr.next();
Object value = object.get(key);
if(value instanceof JSONArray) {
value = toList((JSONArray) value);
}
else if(value instanceof JSONObject) {
value = toMap((JSONObject) value);
}
map.put(key, value);
}
return map;
}
protected List<Object> toList(JSONArray array) throws JSONException {
List<Object> list = new ArrayList<Object>();
for(int i = 0; i < array.length(); i++) {
Object value = array.get(i);
if(value instanceof JSONArray) {
value = toList((JSONArray) value);
}
else if(value instanceof JSONObject) {
value = toMap((JSONObject) value);
}
list.add(value);
}
return list;
}
}

View File

@ -46,42 +46,37 @@ class LtiService {
private Map<String, String> getConsumer(consumerId) {
Map<String, String> consumer = null
if( this.consumerMap.containsKey(consumerId) ){
if (this.consumerMap.containsKey(consumerId)) {
consumer = new HashMap<String, String>()
consumer.put("key", consumerId);
consumer.put("secret", this.consumerMap.get(consumerId))
}
return consumer
}
private void initConsumerMap(){
private void initConsumerMap() {
this.consumerMap = new HashMap<String, String>()
String[] consumers = this.consumers.split(",")
//for( int i=0; i < consumers.length; i++){
if ( consumers.length > 0 ){
if ( consumers.length > 0 ) {
int i = 0;
String[] consumer = consumers[i].split(":")
if( consumer.length == 2 ){
this.consumerMap.put(consumer[0], consumer[1])
}
}
}
public String sign(String sharedSecret, String data) throws Exception
{
public String sign(String sharedSecret, String data)
throws Exception {
Mac mac = setKey(sharedSecret)
// Signed String must be BASE64 encoded.
byte[] signBytes = mac.doFinal(data.getBytes("UTF8"));
String signature = encodeBase64(signBytes);
return signature;
}
private Mac setKey(String sharedSecret) throws Exception
{
private Mac setKey(String sharedSecret)
throws Exception {
Mac mac = Mac.getInstance("HmacSHA1");
byte[] keyBytes = sharedSecret.getBytes("UTF8");
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
@ -110,7 +105,6 @@ class LtiService {
def boolean isSSLEnabled(String query) {
def ssl_enabled = false
log.debug("Pinging SSL connection")
try {
// open connection
@ -122,14 +116,12 @@ class LtiService {
httpConnection.setRequestMethod("HEAD")
httpConnection.setConnectTimeout(5000)
httpConnection.connect()
int responseCode = httpConnection.getResponseCode()
if (responseCode == HttpURLConnection.HTTP_OK) {
ssl_enabled = true
} else {
log.debug("HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode)
}
} catch(IOException e) {
log.debug("IOException: Message=" + e.getMessage())
} catch(IllegalArgumentException e) {
@ -148,4 +140,8 @@ class LtiService {
def boolean allRecordedByDefault() {
return Boolean.parseBoolean(this.recordedByDefault);
}
def String getScheme(request) {
return request.isSecure() ? "https" : "http"
}
}

View File

@ -1,35 +1,35 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
<title>Error</title>
<asset:stylesheet src="bootstrap.css"/>
<asset:stylesheet src="tool.css"/>
<asset:javascript src="jquery.js"/>
<asset:javascript src="bootstrap.js"/>
</head>
<body>
<div class="body">
<br/><br/>
<div class="container">
<g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
<div class="alert alert-warning">
${resultMessage}
</div>
</g:if>
<g:else>
<div class="alert alert-danger">
<g:message code="tool.error.general" />
</div>
</g:else>
</div>
</div>
<!-- {
"error": {
"messageKey": "${resultMessageKey}",
"message": "${resultMessage}"
}
}
-->
<br/><br/>
</body>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
<title>Error</title>
<asset:stylesheet src="bootstrap.css"/>
<asset:stylesheet src="tool.css"/>
<asset:javascript src="jquery.js"/>
<asset:javascript src="bootstrap.js"/>
</head>
<body>
<div class="body">
<br/><br/>
<div class="container">
<g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
<div class="alert alert-warning">
${resultMessage}
</div>
</g:if>
<g:else>
<div class="alert alert-danger">
<g:message code="tool.error.general" />
</div>
</g:else>
</div>
</div>
<!-- {
"error": {
"messageKey": "${resultMessageKey}",
"message": "${resultMessage}"
}
}
-->
<br/><br/>
</body>
</html>

View File

@ -1,77 +1,88 @@
<html>
<head>
<title><g:message code="tool.view.title" /></title>
<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
<asset:stylesheet src="bootstrap.css"/>
<asset:stylesheet src="dataTables.bootstrap.min.css"/>
<asset:javascript src="jquery.js"/>
<asset:javascript src="jquery.dataTables.min.js"/>
<asset:javascript src="dataTables.bootstrap.min.js"/>
<asset:javascript src="dataTables.plugin.datetime.js"/>
<asset:javascript src="moment-with-locales.min.js"/>
<asset:javascript src="bootstrap.js"/>
<asset:javascript src="bootstrap-confirmation.min.js"/>
<asset:javascript src="tool.js"/>
</head>
<body>
<h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
<br><br>
<div class="container">
<table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
<thead>
<tr>
<th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
<th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
<th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
<th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
<th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
<g:if test="${ismoderator}">
<th class="header c5 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
</g:if>
</tr>
</thead>
<tbody>
<g:each in="${recordingList}" var="r">
<g:if test="${ismoderator || r.published == 'true'}">
<tr class="r0 lastrow">
<td class="cell c0" style="text-align:center;">
<g:if test="${r.published == 'true'}">
<g:each in="${r.playback}" var="p">
<a title="<g:message code="tool.view.recording.format.${p.type}" />" target="_new" href="${p.url}"><g:message code="tool.view.recording.format.${p.type}" /></a>&#32;
</g:each>
</g:if>
</td>
<td class="cell c1" style="text-align:center;">${r.name}</td>
<td class="cell c2" style="text-align:center;">${r.metadata.contextactivitydescription}</td>
<td class="cell c3" style="text-align:center;">${r.unixDate}</td>
<td class="cell c4" style="text-align:center;">${r.duration}</td>
<g:if test="${ismoderator}">
<td class="cell c5 lastcol" style="text-align:center;">
<g:if test="${r.published == 'true'}">
<a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
</g:if>
<g:else>
<a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
</g:else>
<a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
data-toggle="confirmation"
data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
data-content="<g:message code="tool.view.recording.delete.confirmation" />"
data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
data-placement="left"
href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
</a>
</td>
</g:if>
</tr>
</g:if>
</g:each>
</tbody>
</table>
</div>
</body>
<g:javascript>
var locale = '${params.launch_presentation_locale}';
</g:javascript>
</html>
<html>
<head>
<title><g:message code="tool.view.title" /></title>
<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
<asset:stylesheet src="bootstrap.css"/>
<asset:stylesheet src="dataTables.bootstrap.min.css"/>
<asset:stylesheet src="tool.css"/>
<asset:javascript src="jquery.js"/>
<asset:javascript src="jquery.dataTables.min.js"/>
<asset:javascript src="dataTables.bootstrap.min.js"/>
<asset:javascript src="dataTables.plugin.datetime.js"/>
<asset:javascript src="moment-with-locales.min.js"/>
<asset:javascript src="bootstrap.js"/>
<asset:javascript src="bootstrap-confirmation.min.js"/>
<asset:javascript src="tool.js"/>
</head>
<body>
<h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
<br><br>
<div class="container">
<table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
<thead>
<tr>
<th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
<th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
<th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
<th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.preview" /></th>
<th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
<th class="header c5" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
<g:if test="${ismoderator}">
<th class="header c6 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
</g:if>
</tr>
</thead>
<tbody>
<g:each in="${recordingList}" var="r">
<g:if test="${ismoderator || r.published == 'true'}">
<tr class="r0 lastrow">
<td class="cell c0" style="text-align:center;">
<g:if test="${r.published}">
<g:each in="${r.playback}" var="format">
<a title="<g:message code="tool.view.recording.format.${format.getValue().type}" />" target="_new" href="${format.getValue().url}"><g:message code="tool.view.recording.format.${format.getValue().type}" /></a>&#32;
</g:each>
</g:if>
</td>
<td class="cell c1" style="text-align:left;">${r.name}</td>
<td class="cell c2" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
<td class="cell c3" style="text-align:left;">
<g:if test="${r.published}">
<div>
<g:each in="${r.thumbnails}" var="thumbnail">
<img src="${thumbnail.content}" class="thumbnail"></img>
</g:each>
</div>
</g:if>
</td>
<td class="cell c4" style="text-align:left;">${r.unixDate}</td>
<td class="cell c5" style="text-align:right;">${r.duration}</td>
<g:if test="${ismoderator}">
<td class="cell c6 lastcol" style="text-align:center;">
<g:if test="${r.published}">
<a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
</g:if>
<g:else>
<a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
</g:else>
<a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
data-toggle="confirmation"
data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
data-content="<g:message code="tool.view.recording.delete.confirmation" />"
data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
data-placement="left"
href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
</a>
</td>
</g:if>
</tr>
</g:if>
</g:each>
</tbody>
</table>
</div>
</body>
<g:javascript>
var locale = '${params.launch_presentation_locale}';
</g:javascript>
</html>

View File

@ -25,8 +25,8 @@ package org.bigbluebutton.main.model.users.events
public class RequestPresenterGroupEvent extends Event {
public static const REQUEST_PRESENTER_GROUP:String = "REQUEST_PRESENTER_GROUP";
public function RequestPresenterGroupEvent(type:String) {
super(type, true, false);
public function RequestPresenterGroupEvent(type:String, bubbles:Boolean, cancelable:Boolean) {
super(type, bubbles, cancelable);
}
}
}

View File

@ -40,10 +40,8 @@ package org.bigbluebutton.modules.present.business
import org.bigbluebutton.modules.present.events.PresentationUploadTokenPass;
import org.bigbluebutton.modules.present.events.PresenterCommands;
import org.bigbluebutton.modules.present.events.RemovePresentationEvent;
import org.bigbluebutton.modules.present.events.RequestAllPodsEvent;
import org.bigbluebutton.modules.present.events.RequestClosePresentationPodEvent;
import org.bigbluebutton.modules.present.events.RequestNewPresentationPodEvent;
import org.bigbluebutton.modules.present.events.RequestPresentationInfoPodEvent;
import org.bigbluebutton.modules.present.events.SetPresentationDownloadableEvent;
import org.bigbluebutton.modules.present.events.SetPresenterInPodReqEvent;
import org.bigbluebutton.modules.present.events.UploadEvent;
@ -61,7 +59,6 @@ package org.bigbluebutton.modules.present.business
private var host:String;
private var conference:String;
private var room:String;
private var userid:Number;
private var uploadService:FileUploadService;
private var sender:MessageSender;
@ -77,25 +74,18 @@ package org.bigbluebutton.modules.present.business
service = new PresentationService();
}
public function getCurrentPresentationInfo():void {
podManager.requestAllPodsPresentationInfo();
}
public function handleRequestAllPodsEvent(e: RequestAllPodsEvent):void {
public function getPresentationPodsInfo():void {
sender.requestAllPodsEvent();
}
public function connect(e:PresentModuleEvent):void {
extractAttributes(e.data);
podManager.requestAllPodsPresentationInfo();
}
private function extractAttributes(a:Object):void{
host = a.host as String;
conference = a.conference as String;
room = a.room as String;
userid = a.userid as Number;
}
public function handleGetListOfPresentationsRequest(event: GetListOfPresentationsRequest):void {
@ -113,10 +103,6 @@ package org.bigbluebutton.modules.present.business
var dispatcher:Dispatcher = new Dispatcher();
dispatcher.dispatchEvent(new GetListOfPresentationsReply(idAndName));
}
public function handleRequestPresentationInfoPodEvent(e: RequestPresentationInfoPodEvent): void {
sender.getPresentationInfo(e.podId);
}
public function handleChangePresentationCommand(cmd:ChangePresentationCommand):void {
var presModel: PresentationModel = podManager.getPod(cmd.podId);

View File

@ -1,34 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.modules.present.events
{
import flash.events.Event;
import flash.net.FileReference;
public class RequestAllPodsEvent extends Event {
public static const REQUEST_ALL_PODS:String = "REQUEST_ALL_PODS";
public function RequestAllPodsEvent(type:String) {
super(type, true, false);
}
}
}

View File

@ -1,36 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.modules.present.events
{
import flash.events.Event;
import flash.net.FileReference;
public class RequestPresentationInfoPodEvent extends Event {
public static const REQUEST_PRES_INFO:String = "REQUEST_PRES_INFO";
public var podId: String;
public function RequestPresentationInfoPodEvent(type:String) {
super(type, true, false);
}
}
}

View File

@ -37,7 +37,6 @@ package org.bigbluebutton.modules.present.managers
import org.bigbluebutton.modules.present.events.NewPresentationPodCreated;
import org.bigbluebutton.modules.present.events.PresentModuleEvent;
import org.bigbluebutton.modules.present.events.PresentationPodRemoved;
import org.bigbluebutton.modules.present.events.RequestAllPodsEvent;
import org.bigbluebutton.modules.present.events.UploadEvent;
import org.bigbluebutton.modules.present.model.PresentOptions;
import org.bigbluebutton.modules.present.model.PresentationPodManager;
@ -68,12 +67,6 @@ package org.bigbluebutton.modules.present.managers
presentOptions = Options.getOptions(PresentOptions) as PresentOptions;
winManager.initCollection(presentOptions.maxNumWindows);
var requestAllPodsEvent:RequestAllPodsEvent = new RequestAllPodsEvent(RequestAllPodsEvent.REQUEST_ALL_PODS);
globalDispatcher.dispatchEvent(requestAllPodsEvent);
var requestPresenterGroupEvent:RequestPresenterGroupEvent = new RequestPresenterGroupEvent(RequestPresenterGroupEvent.REQUEST_PRESENTER_GROUP);
globalDispatcher.dispatchEvent(requestPresenterGroupEvent);
}
public function handleAddPresentationPod(e: NewPresentationPodCreated): void {
@ -122,6 +115,15 @@ package org.bigbluebutton.modules.present.managers
public function handleGetAllPodsRespEvent(e: GetAllPodsRespEvent): void {
var podsAC:ArrayCollection = e.pods as ArrayCollection;
// For now the best option for cleaning up old pods is to just delete everything we
// know about. In the future we'll want to update existing pods to a new state
// rather than recreating.
var activePodIds:Array = podsManager.getAllPodIds();
for (var i:int=0; i<activePodIds.length; i++) {
handlePresentationPodRemovedHelper(activePodIds[i]);
}
podsManager.handleGetAllPodsResp(podsAC);
}

View File

@ -25,6 +25,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import mx.events.FlexEvent;
import org.bigbluebutton.main.events.BBBEvent;
import org.bigbluebutton.main.model.users.events.RequestPresenterGroupEvent;
import org.bigbluebutton.modules.present.business.PresentProxy;
import org.bigbluebutton.modules.present.commands.ChangePageCommand;
import org.bigbluebutton.modules.present.commands.ChangePresentationCommand;
@ -43,10 +44,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.modules.present.events.PresentationUploadTokenPass;
import org.bigbluebutton.modules.present.events.PresenterCommands;
import org.bigbluebutton.modules.present.events.RemovePresentationEvent;
import org.bigbluebutton.modules.present.events.RequestAllPodsEvent;
import org.bigbluebutton.modules.present.events.RequestClosePresentationPodEvent;
import org.bigbluebutton.modules.present.events.RequestNewPresentationPodEvent;
import org.bigbluebutton.modules.present.events.RequestPresentationInfoPodEvent;
import org.bigbluebutton.modules.present.events.SetPresentationDownloadableEvent;
import org.bigbluebutton.modules.present.events.SetPresenterInPodReqEvent;
import org.bigbluebutton.modules.present.events.UploadEvent;
@ -63,7 +62,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<EventHandlers type="{PresentModuleEvent.START_MODULE}" >
<MethodInvoker generator="{PresentManager}" method="handleStartModuleEvent" arguments="{event}"/>
<MethodInvoker generator="{PresentProxy}" method="getCurrentPresentationInfo" />
<MethodInvoker generator="{PresentProxy}" method="getPresentationPodsInfo" />
<EventAnnouncer generator="{RequestPresenterGroupEvent}" type="{RequestPresenterGroupEvent.REQUEST_PRESENTER_GROUP}" />
<MethodInvoker generator="{PresentProxy}" method="connect" arguments="{event}" />
</EventHandlers>
@ -84,7 +84,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
</EventHandlers>
<EventHandlers type="{BBBEvent.RECONNECT_BIGBLUEBUTTON_SUCCEEDED_EVENT}" >
<MethodInvoker generator="{PresentProxy}" method="getCurrentPresentationInfo" />
<MethodInvoker generator="{PresentProxy}" method="getPresentationPodsInfo" />
</EventHandlers>
<EventHandlers type="{UploadEvent.OPEN_UPLOAD_WINDOW}" >
@ -180,14 +180,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<MethodInvoker generator="{PresentManager}" method="handlePresentationPodRemoved" arguments="{event}" />
</EventHandlers>
<EventHandlers type="{RequestPresentationInfoPodEvent.REQUEST_PRES_INFO}" >
<MethodInvoker generator="{PresentProxy}" method="handleRequestPresentationInfoPodEvent" arguments="{event}" />
</EventHandlers>
<EventHandlers type="{RequestAllPodsEvent.REQUEST_ALL_PODS}" >
<MethodInvoker generator="{PresentProxy}" method="handleRequestAllPodsEvent" arguments="{event}" />
</EventHandlers>
<EventHandlers type="{GetAllPodsRespEvent.GET_ALL_PODS_RESP}" >
<MethodInvoker generator="{PresentManager}" method="handleGetAllPodsRespEvent" arguments="{event}" />
</EventHandlers>

View File

@ -9,7 +9,6 @@ package org.bigbluebutton.modules.present.model {
import org.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.core.EventConstants;
import org.bigbluebutton.modules.present.events.NewPresentationPodCreated;
import org.bigbluebutton.modules.present.events.RequestPresentationInfoPodEvent;
import org.bigbluebutton.modules.present.services.PresentationService;
import org.bigbluebutton.modules.present.services.messages.PresentationPodVO;
@ -81,6 +80,15 @@ package org.bigbluebutton.modules.present.model {
var pod: PresentationModel = getPod(DEFAULT_POD_ID);
return pod;
}
public function getAllPodIds():Array {
var podIds:Array = [];
for (var i:int = 0; i < _presentationPods.length; i++) {
var pod: PresentationModel = _presentationPods.getItemAt(i) as PresentationModel;
podIds.push(pod.getPodId())
}
return podIds;
}
public function handleAddPresentationPod(podId: String): void {
for (var i:int = 0; i < _presentationPods.length; i++) {
@ -106,16 +114,6 @@ package org.bigbluebutton.modules.present.model {
}
}
}
public function requestAllPodsPresentationInfo(): void {
for (var i:int = 0; i < _presentationPods.length; i++) {
var pod: PresentationModel = _presentationPods.getItemAt(i) as PresentationModel;
var event:RequestPresentationInfoPodEvent = new RequestPresentationInfoPodEvent(RequestPresentationInfoPodEvent.REQUEST_PRES_INFO);
event.podId = pod.getPodId();
globalDispatcher.dispatchEvent(event);
}
}
public function handleGetAllPodsResp(podsAC: ArrayCollection): void {
for (var j:int = 0; j < podsAC.length; j++) {

View File

@ -73,20 +73,6 @@ package org.bigbluebutton.modules.present.services.messaging
JSON.stringify(message)
);
}
public function getPresentationInfo(podId: String):void {
var message:Object = {
header: {name: "GetPresentationInfoReqMsg", meetingId: UsersUtil.getInternalMeetingID(), userId: UsersUtil.getMyUserID()},
body: {podId: podId}
};
var _nc:ConnectionManager = BBB.initConnectionManager();
_nc.sendMessage2x(
function(result:String):void { },
function(status:String):void { LOGGER.error(status); },
JSON.stringify(message)
);
}
public function requestAllPodsEvent():void {
var message:Object = {

View File

@ -7,4 +7,4 @@ import handleUserVoted from './handlers/userVoted';
RedisPubSub.on('PollShowResultEvtMsg', handlePollPublished);
RedisPubSub.on('PollStartedEvtMsg', handlePollStarted);
RedisPubSub.on('PollStoppedEvtMsg', handlePollStopped);
RedisPubSub.on('UserRespondedToPollEvtMsg', handleUserVoted);
RedisPubSub.on('PollUpdatedEvtMsg', handleUserVoted);

View File

@ -3,11 +3,20 @@ import updateVotes from '../modifiers/updateVotes';
export default function userVoted({ body }, meetingId) {
const { poll } = body;
const { presenterId } = body;
check(meetingId, String);
check(poll, Object);
check(presenterId, String);
check(poll, {
id: String,
answers: [
{
id: Number,
key: String,
numVotes: Number,
},
],
numRespondents: Number,
numResponders: Number,
});
return updateVotes(poll, meetingId, presenterId);
return updateVotes(poll, meetingId);
}

View File

@ -10,6 +10,11 @@ export default function publishVote(credentials, id, pollAnswerId) { // TODO dis
const { meetingId, requesterUserId } = credentials;
/*
We keep an array of people who were in the meeting at the time the poll
was started. The poll is published to them only.
Once they vote - their ID is removed and they cannot see the poll anymore
*/
const currentPoll = Polls.findOne({
users: requesterUserId,
meetingId,

View File

@ -3,19 +3,17 @@ import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import flat from 'flat';
export default function updateVotes(poll, meetingId, requesterId) {
export default function updateVotes(poll, meetingId) {
check(meetingId, String);
check(requesterId, String);
check(poll, Object);
const {
id,
answers,
numResponders,
numRespondents,
} = poll;
const { numResponders } = poll;
const { numRespondents } = poll;
check(id, String);
check(answers, Array);
@ -24,15 +22,11 @@ export default function updateVotes(poll, meetingId, requesterId) {
const selector = {
meetingId,
requester: requesterId,
id,
};
const modifier = {
$set: Object.assign(
{ requester: requesterId },
flat(poll, { safe: true }),
),
$set: flat(poll, { safe: true }),
};
const cb = (err) => {

View File

@ -7,6 +7,7 @@ import handlePresentationConversionUpdate from './handlers/presentationConversio
RedisPubSub.on('SyncGetPresentationInfoRespMsg', handlePresentationInfoReply);
RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);
RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove);

View File

@ -49,6 +49,7 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
statusModifier.id = presentationId;
statusModifier.name = presentationName;
statusModifier['conversion.error'] = true;
statusModifier['conversion.done'] = true;
break;
case GENERATED_SLIDE_KEY:

View File

@ -1,5 +1,7 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
import addVoiceUser from '../modifiers/addVoiceUser';
export default function handleJoinVoiceUser({ body }, meetingId) {
@ -7,6 +9,74 @@ export default function handleJoinVoiceUser({ body }, meetingId) {
voiceUser.joined = true;
check(meetingId, String);
check(voiceUser, {
voiceConf: String,
intId: String,
voiceUserId: String,
callerName: String,
callerNum: String,
muted: Boolean,
talking: Boolean,
callingWith: String,
listenOnly: Boolean,
joined: Boolean,
});
const {
intId,
callerName,
} = voiceUser;
if (intId.toString().startsWith('v_')) {
/* voice-only user - called into the conference */
const selector = {
meetingId,
userId: intId,
};
const USER_CONFIG = Meteor.settings.public.user;
const ROLE_VIEWER = USER_CONFIG.role_viewer;
const modifier = {
$set: {
meetingId,
connectionStatus: 'online',
roles: [ROLE_VIEWER.toLowerCase()],
sortName: callerName.trim().toLowerCase(),
color: '#ffffff', // TODO
intId,
extId: intId, // TODO
name: callerName,
role: ROLE_VIEWER.toLowerCase(),
guest: false,
authed: true,
waitingForAcceptance: false,
emoji: 'none',
presenter: false,
locked: false, // TODO
avatar: '',
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding call-in user to VoiceUser collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.info(`Added a call-in user id=${intId} meeting=${meetingId}`);
}
return Logger.info(`Upserted a call-in user id=${intId} meeting=${meetingId}`);
};
Users.upsert(selector, modifier, cb);
} else {
/* there is a corresponding web user in Users collection -- no need to add new one */
}
return addVoiceUser(meetingId, voiceUser);
}

View File

@ -1,11 +1,20 @@
import { check } from 'meteor/check';
import removeVoiceUser from '../modifiers/removeVoiceUser';
import removeVoiceUser from '/imports/api/voice-users/server/modifiers/removeVoiceUser';
import removeUser from '/imports/api/users/server/modifiers/removeUser';
export default function handleVoiceUpdate({ body }, meetingId) {
const voiceUser = body;
check(meetingId, String);
check(voiceUser, {
voiceConf: String,
intId: String,
voiceUserId: String,
});
const { intId } = voiceUser;
removeUser(meetingId, intId);
return removeVoiceUser(meetingId, voiceUser);
}

View File

@ -2,10 +2,12 @@ import { Meteor } from 'meteor/meteor';
import mapToAcl from '/imports/startup/mapToAcl';
import listenOnlyToggle from './methods/listenOnlyToggle';
import muteToggle from './methods/muteToggle';
import ejectUserFromVoice from './methods/ejectUserFromVoice';
Meteor.methods(mapToAcl(['methods.listenOnlyToggle', 'methods.toggleSelfVoice', 'methods.toggleVoice',
], {
Meteor.methods(mapToAcl(['methods.listenOnlyToggle', 'methods.toggleSelfVoice',
'methods.toggleVoice', 'methods.ejectUserFromVoice'], {
listenOnlyToggle,
toggleSelfVoice: (credentials) => { muteToggle(credentials, credentials.requesterUserId); },
toggleVoice: muteToggle,
ejectUserFromVoice,
}));

View File

@ -0,0 +1,22 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
export default function ejectUserFromVoice(credentials, userId) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'EjectUserFromVoiceCmdMsg';
const { requesterUserId, meetingId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
const payload = {
userId,
ejectedBy: requesterUserId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -61,6 +61,8 @@ class ActionsDropdown extends Component {
<Dropdown ref={(ref) => { this._dropdown = ref; }} >
<DropdownTrigger tabIndex={0} >
<Button
hideLabel
aria-label={intl.formatMessage(intlMessages.actionsLabel)}
className={styles.button}
label={intl.formatMessage(intlMessages.actionsLabel)}
icon="plus"

View File

@ -42,17 +42,21 @@ const EmojiSelect = ({
const statuses = Object.keys(options);
const lastStatus = statuses.pop();
const statusLabel = statuses.indexOf(selected) === -1 ?
intl.formatMessage(intlMessages.statusTriggerLabel)
: intl.formatMessage({ id: `app.actionsBar.emojiMenu.${selected}Label` });
return (
<Dropdown autoFocus>
<DropdownTrigger tabIndex={0}>
<Button
className={styles.button}
label={intl.formatMessage(intlMessages.statusTriggerLabel)}
aria-label={intl.formatMessage(intlMessages.changeStatusLabel)}
label={statusLabel}
aria-label={statusLabel}
aria-describedby="currentStatus"
icon={options[selected !== lastStatus ? selected : statuses[1]]}
ghost={false}
hideLabel={false}
hideLabel
circle
size="lg"
color="primary"

View File

@ -1,9 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, intlShape, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import styles from './styles';
import cx from 'classnames';
const intlMessages = defineMessages({
joinAudio: {
id: 'app.audio.joinAudio',
description: 'Join audio button label',
},
leaveAudio: {
id: 'app.audio.leaveAudio',
description: 'Leave audio button label',
},
muteAudio: {
id: 'app.actionsBar.muteLabel',
description: 'Mute audio button label',
},
unmuteAudio: {
id: 'app.actionsBar.unmuteLabel',
description: 'Unmute audio button label',
},
});
const propTypes = {
handleToggleMuteMicrophone: PropTypes.func.isRequired,
handleJoinAudio: PropTypes.func.isRequired,
@ -12,6 +33,7 @@ const propTypes = {
unmute: PropTypes.bool.isRequired,
mute: PropTypes.bool.isRequired,
join: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
};
const AudioControls = ({
@ -23,6 +45,7 @@ const AudioControls = ({
disable,
glow,
join,
intl,
}) => (
<span className={styles.container}>
{mute ?
@ -30,7 +53,9 @@ const AudioControls = ({
className={glow ? cx(styles.button, styles.glow) : styles.button}
onClick={handleToggleMuteMicrophone}
disabled={disable}
label={unmute ? 'Unmute' : 'Mute'}
hideLabel
label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
aria-label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
color={'primary'}
icon={unmute ? 'mute' : 'unmute'}
size={'lg'}
@ -40,7 +65,9 @@ const AudioControls = ({
className={styles.button}
onClick={join ? handleLeaveAudio : handleJoinAudio}
disabled={disable}
label={join ? 'Leave Audio' : 'Join Audio'}
hideLabel
aria-label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)}
label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)}
color={join ? 'danger' : 'primary'}
icon={join ? 'audio_off' : 'audio_on'}
size={'lg'}
@ -50,4 +77,4 @@ const AudioControls = ({
AudioControls.propTypes = propTypes;
export default AudioControls;
export default injectIntl(AudioControls);

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Tooltip from '/imports/ui/components/tooltip/component';
import styles from './styles';
import Icon from '../icon/component';
import BaseButton from './base/component';
@ -86,7 +87,6 @@ const defaultProps = {
};
export default class Button extends BaseButton {
_getClassNames() {
const {
size,
@ -113,8 +113,26 @@ export default class Button extends BaseButton {
}
render() {
const renderFuncName = this.props.circle ?
'renderCircle' : 'renderDefault';
const {
circle,
hideLabel,
label,
'aria-label' : ariaLabel
} = this.props;
const renderFuncName = circle ? 'renderCircle' : 'renderDefault';
if (hideLabel) {
const tooltipLabel = label ? label : ariaLabel;
return (
<Tooltip
title={tooltipLabel}
>
{this[renderFuncName]()}
</Tooltip>
);
}
return this[renderFuncName]();
}

View File

@ -43,7 +43,7 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
display: inline-block;
border-radius: $border-size;
font-weight: $btn-font-weight;
line-height: 1.5;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: middle;

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Link } from 'react-router';
import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Button from '/imports/ui/components/button/component';
import styles from './styles';
import MessageForm from './message-form/component';
import MessageList from './message-list/component';
@ -56,10 +57,16 @@ const Chat = (props) => {
<Link
to="/users"
role="button"
className={styles.closeIcon}
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
tabIndex={-1}
>
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
<Button
className={styles.closeBtn}
icon="close"
size="md"
hideLabel
onClick={() => actions.handleClosePrivateChat(chatID)}
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
/>
</Link> :
<ChatDropdown />
}

View File

@ -27,7 +27,11 @@ $padding: $md-padding-x;
margin-right: -$padding;
padding-right: $padding;
padding-top: 0;
padding-bottom: $padding;
&:after {
content: "";
display: block;
height: $padding;
}
}
.unreadButton {

View File

@ -1,6 +1,8 @@
@import "/imports/ui/stylesheets/mixins/focus";
@import "/imports/ui/stylesheets/variables/_all";
$icon-offset: -.4em;
.chat {
background-color: #fff;
padding: $md-padding-x;
@ -36,7 +38,22 @@
flex: 1;
}
.closeIcon {
.closeBtn {
background-color: $color-white;
flex: 0 0;
margin-left: $sm-padding-x / 2;
padding: 0 0.25rem !important;
i {
font-size: 0.85em;
color: $color-gray-dark !important;
top: $icon-offset;
}
&:focus,
&:hover{
background-color: $color-white !important;
i{
color: $color-gray;
}
}
}

View File

@ -4,7 +4,9 @@ import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Dropzone from 'react-dropzone';
import update from 'immutability-helper';
import cx from 'classnames';
import _ from 'lodash';
import { notify } from '/imports/ui/services/notification';
import ModalFullscreen from '/imports/ui/components/modal/fullscreen/component';
import Icon from '/imports/ui/components/icon/component';
import ButtonBase from '/imports/ui/components/button/base/component';
@ -28,10 +30,12 @@ const propTypes = {
};
const defaultProps = {
defaultFileName: 'default.pdf',
};
const intlMessages = defineMessages({
current: {
id: 'app.presentationUploder.currentBadge',
},
title: {
id: 'app.presentationUploder.title',
description: 'title of the modal',
@ -68,6 +72,10 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.fileToUpload',
description: 'message used in the file selected for upload',
},
genericError: {
id: 'app.presentationUploder.genericError',
description: 'generic error while uploading/converting',
},
uploadProcess: {
id: 'app.presentationUploder.upload.progress',
description: 'message that indicates the percentage of the upload',
@ -84,9 +92,12 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.conversion.genericConversionStatus',
description: 'indicates that file is being converted',
},
TIMEOUT: {
id: 'app.presentationUploder.conversion.timeout',
},
GENERATING_THUMBNAIL: {
id: 'app.presentationUploder.conversion.generatingThumbnail',
description: 's that it is generating thumbnails',
description: 'indicatess that it is generating thumbnails',
},
GENERATING_SVGIMAGES: {
id: 'app.presentationUploder.conversion.generatingSvg',
@ -96,14 +107,21 @@ const intlMessages = defineMessages({
id: 'app.presentationUploder.conversion.generatedSlides',
description: 'warns that were slides generated',
},
PAGE_COUNT_EXCEEDED: {
id: 'app.presentationUploder.conversion.pageCountExceeded',
description: 'warns the user that the conversion failed because of the page count',
},
});
class PresentationUploader extends Component {
constructor(props) {
super(props);
const currentPres = props.presentations.find(p => p.isCurrent);
this.state = {
presentations: props.presentations,
oldCurrentId: currentPres ? currentPres.id : -1,
preventClosing: false,
disableActions: false,
};
@ -118,27 +136,19 @@ class PresentationUploader extends Component {
this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this);
}
componentWillReceiveProps(nextProps) {
const nextPresentations = nextProps.presentations;
// Update only the conversion state when receiving new props
nextPresentations.forEach((file) => {
this.updateFileKey(file.filename, 'id', file.id);
this.deepMergeUpdateFileKey(file.id, 'conversion', file.conversion);
});
}
updateFileKey(id, key, value, operation = '$set') {
this.setState(({ presentations }) => {
// Compare id and filename since non-uploaded files dont have a real id
const fileIndex = presentations.findIndex(f => f.id === id || f.filename === id);
const fileIndex = presentations.findIndex(f => f.id === id);
return fileIndex === -1 ? false : {
presentations: update(presentations, {
[fileIndex]: { $apply: file =>
update(file, { [key]: {
[operation]: value,
} }),
[fileIndex]: {
$apply: file =>
update(file, {
[key]: {
[operation]: value,
},
}),
},
}),
};
@ -150,26 +160,54 @@ class PresentationUploader extends Component {
this.updateFileKey(id, key, applyValue, '$apply');
}
isDefault(presentation) {
const { defaultFileName } = this.props;
return presentation.filename === defaultFileName
&& !presentation.id.includes(defaultFileName);
}
handleConfirm() {
const { presentations } = this.state;
const presentationsToSave = this.state.presentations
.filter(p => !p.upload.error && !p.conversion.error);
this.setState({
disableActions: true,
preventClosing: true,
presentations: presentationsToSave,
});
return this.props.handleSave(presentations)
return this.props.handleSave(presentationsToSave)
.then(() => {
this.setState({
disableActions: false,
preventClosing: false,
});
})
.catch((error) => {
const hasError = this.state.presentations.some(p => p.upload.error || p.conversion.error);
if (!hasError) {
this.setState({
disableActions: false,
preventClosing: false,
});
return;
}
// if theres error we dont want to close the modal
this.setState({
disableActions: false,
preventClosing: true,
}, () => {
// if the selected current has error we revert back to the old one
const newCurrent = this.state.presentations.find(p => p.isCurrent);
if (newCurrent.upload.error || newCurrent.conversion.error) {
this.handleCurrentChange(this.state.oldCurrentId);
}
});
})
.catch((error) => {
notify(this.props.intl.formatMessage(intlMessages.genericError), 'error');
console.error(error);
this.setState({
disableActions: false,
preventClosing: true,
error,
});
});
}
@ -184,52 +222,62 @@ class PresentationUploader extends Component {
}
handleFiledrop(files) {
const presentationsToUpload = files.map(file => ({
file,
id: file.name,
filename: file.name,
isCurrent: false,
conversion: { done: false, error: false },
upload: { done: false, error: false, progress: 0 },
onProgress: (event) => {
if (!event.lengthComputable) {
this.deepMergeUpdateFileKey(file.name, 'upload', {
progress: 100,
done: true,
const presentationsToUpload = files.map((file) => {
const id = _.uniqueId(file.name);
return {
file,
id,
filename: file.name,
isCurrent: false,
conversion: { done: false, error: false },
upload: { done: false, error: false, progress: 0 },
onProgress: (event) => {
if (!event.lengthComputable) {
this.deepMergeUpdateFileKey(id, 'upload', {
progress: 100,
done: true,
});
return;
}
this.deepMergeUpdateFileKey(id, 'upload', {
progress: (event.loaded / event.total) * 100,
done: event.loaded === event.total,
});
return;
}
this.deepMergeUpdateFileKey(file.name, 'upload', {
progress: (event.loaded / event.total) * 100,
done: event.loaded === event.total,
});
},
onError: (error) => {
this.deepMergeUpdateFileKey(file.name, 'upload', { error });
},
}));
},
onConversion: (conversion) => {
this.deepMergeUpdateFileKey(id, 'conversion', conversion);
},
onUpload: (upload) => {
this.deepMergeUpdateFileKey(id, 'upload', upload);
},
onDone: (newId) => {
this.updateFileKey(id, 'id', newId);
},
};
});
this.setState(({ presentations }) => ({
presentations: presentations.concat(presentationsToUpload),
}));
}
handleCurrentChange(item) {
handleCurrentChange(id) {
const { presentations, disableActions } = this.state;
if (disableActions) return;
const currentIndex = presentations.findIndex(p => p.isCurrent);
const newCurrentIndex = presentations.indexOf(item);
const newCurrentIndex = presentations.findIndex(p => p.id === id);
const commands = {};
// we can end up without a current presentation
if (currentIndex !== -1) {
commands[currentIndex] = {
$apply: (_) => {
const p = _;
$apply: (presentation) => {
const p = presentation;
p.isCurrent = false;
return p;
},
@ -237,8 +285,8 @@ class PresentationUploader extends Component {
}
commands[newCurrentIndex] = {
$apply: (_) => {
const p = _;
$apply: (presentation) => {
const p = presentation;
p.isCurrent = true;
return p;
},
@ -256,14 +304,6 @@ class PresentationUploader extends Component {
if (disableActions) return;
const toRemoveIndex = presentations.indexOf(item);
const toRemove = presentations[toRemoveIndex];
if (toRemove.isCurrent) {
const defaultPresentation =
presentations.find(_ => _.filename === this.props.defaultFileName);
this.handleCurrentChange(defaultPresentation);
}
this.setState({
presentations: update(presentations, {
@ -276,7 +316,17 @@ class PresentationUploader extends Component {
const { presentations } = this.state;
const presentationsSorted = presentations
.sort((a, b) => b.filename === this.props.defaultFileName);
.sort((a, b) => {
// Sort by ID first so files with the same name have the same order
if (a.id > b.id) {
return 1;
}
if (a.id < b.id) {
return -1;
}
return 0;
})
.sort((a, b) => this.isDefault(b));
return (
<div className={styles.fileList}>
@ -303,12 +353,12 @@ class PresentationUploader extends Component {
}
if (item.upload.done && item.upload.error) {
const errorMessage = intlMessages[item.upload.error.code] || intlMessages.genericError;
const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
return intl.formatMessage(errorMessage);
}
if (!item.conversion.done && item.conversion.error) {
const errorMessage = intlMessages[status] || intlMessages.genericError;
if (item.conversion.done && item.conversion.error) {
const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericError;
return intl.formatMessage(errorMessage);
}
@ -329,19 +379,23 @@ class PresentationUploader extends Component {
}
renderPresentationItem(item) {
const { disableActions } = this.state;
const { disableActions, oldCurrentId } = this.state;
const isProcessing = (!item.conversion.done && item.upload.done)
|| (!item.upload.done && item.upload.progress > 0);
const itemClassName = {};
const isActualCurrent = item.id === oldCurrentId;
const isUploading = !item.upload.done && item.upload.progress > 0;
const isConverting = !item.conversion.done && item.upload.done;
const hasError = item.conversion.error || item.upload.error;
const isProcessing = (isUploading || isConverting) && !hasError;
itemClassName[styles.tableItemNew] = item.id === item.filename;
itemClassName[styles.tableItemUploading] = !item.upload.done;
itemClassName[styles.tableItemProcessing] = !item.conversion.done && item.upload.done;
itemClassName[styles.tableItemError] = item.conversion.error || item.upload.error;
itemClassName[styles.tableItemAnimated] = isProcessing;
const itemClassName = {
[styles.tableItemNew]: item.id.indexOf(item.filename) !== -1,
[styles.tableItemUploading]: isUploading,
[styles.tableItemConverting]: isConverting,
[styles.tableItemError]: hasError,
[styles.tableItemAnimated]: isProcessing,
};
const hideRemove = isProcessing || item.filename === this.props.defaultFileName;
const hideRemove = this.isDefault(item);
return (
<tr
@ -349,33 +403,44 @@ class PresentationUploader extends Component {
className={cx(itemClassName)}
>
<td className={styles.tableItemIcon}>
<Icon iconName={'file'} />
<Icon iconName="file" />
</td>
<th className={styles.tableItemName}>
{
isActualCurrent ?
<th className={styles.tableItemCurrent}>
<span className={styles.currentLabel}>
{this.props.intl.formatMessage(intlMessages.current)}
</span>
</th>
: null
}
<th className={styles.tableItemName} colSpan={!isActualCurrent ? 2 : 0}>
<span>{item.filename}</span>
</th>
<td className={styles.tableItemStatus}>
<td className={styles.tableItemStatus} colSpan={hasError ? 2 : 0}>
{this.renderPresentationItemStatus(item)}
</td>
<td className={styles.tableItemActions}>
<Checkbox
disabled={disableActions}
ariaLabel={'Set as current presentation'}
className={styles.itemAction}
checked={item.isCurrent}
onChange={() => this.handleCurrentChange(item)}
/>
{ hideRemove ? null : (
<ButtonBase
{ hasError ? null : (
<td className={styles.tableItemActions}>
<Checkbox
disabled={disableActions}
className={cx(styles.itemAction, styles.itemActionRemove)}
label={'Remove presentation'}
onClick={() => this.handleRemove(item)}
>
<Icon iconName={'delete'} />
</ButtonBase>
)}
</td>
ariaLabel="Set as current presentation"
className={styles.itemAction}
checked={item.isCurrent}
onChange={() => this.handleCurrentChange(item.id)}
/>
{ hideRemove ? null : (
<ButtonBase
disabled={disableActions}
className={cx(styles.itemAction, styles.itemActionRemove)}
label="Remove presentation"
onClick={() => this.handleRemove(item)}
>
<Icon iconName="delete" />
</ButtonBase>
)}
</td>
)}
</tr>
);
}
@ -404,7 +469,7 @@ class PresentationUploader extends Component {
disablePreview
onDrop={this.handleFiledrop}
>
<Icon className={styles.dropzoneIcon} iconName={'upload'} />
<Icon className={styles.dropzoneIcon} iconName="upload" />
<p className={styles.dropzoneMessage}>
{intl.formatMessage(intlMessages.dropzoneLabel)}&nbsp;
<span className={styles.dropzoneLink}>

View File

@ -1,6 +1,7 @@
import Presentations from '/imports/api/presentations';
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
import _ from 'lodash';
const CONVERSION_TIMEOUT = 300000;
@ -29,7 +30,9 @@ const futch = (url, opts = {}, onProgress) => new Promise((res, rej) => {
const getPresentations = () =>
Presentations
.find()
.find({
'conversion.error': false,
})
.fetch()
.map(presentation => ({
id: presentation.id,
@ -39,36 +42,40 @@ const getPresentations = () =>
conversion: presentation.conversion || { done: true, error: false },
}));
const observePresentationConversion = (meetingId, filename) => new Promise((resolve, reject) => {
const conversionTimeout = setTimeout(() => {
reject({
filename,
message: 'Conversion timeout.',
});
}, CONVERSION_TIMEOUT);
const observePresentationConversion = (meetingId, filename, onConversion) =>
new Promise((resolve) => {
const conversionTimeout = setTimeout(() => {
onConversion({
done: true,
error: true,
status: 'TIMEOUT',
});
}, CONVERSION_TIMEOUT);
const didValidate = (doc) => {
clearTimeout(conversionTimeout);
resolve(doc);
};
const didValidate = (doc) => {
clearTimeout(conversionTimeout);
resolve(doc);
};
Tracker.autorun((c) => {
/* FIXME: With two presentations with the same name this will not work as expected */
const query = Presentations.find({ meetingId });
Tracker.autorun((c) => {
const query = Presentations.find({ meetingId });
query.observe({
changed: (newDoc) => {
if (newDoc.name !== filename) return;
if (newDoc.conversion.done) {
c.stop();
didValidate(newDoc);
}
},
query.observe({
changed: (newDoc) => {
if (newDoc.name !== filename) return;
onConversion(newDoc.conversion);
if (newDoc.conversion.done) {
c.stop();
didValidate(newDoc);
}
},
});
});
});
});
const uploadAndConvertPresentation = (file, meetingID, endpoint, onError, onProgress) => {
const uploadAndConvertPresentation = (file, meetingID, endpoint, onUpload, onProgress, onConversion) => {
const data = new FormData();
data.append('presentation_name', file.name);
data.append('Filename', file.name);
@ -84,19 +91,17 @@ const uploadAndConvertPresentation = (file, meetingID, endpoint, onError, onProg
};
return futch(endpoint, opts, onProgress)
.then(() => observePresentationConversion(meetingID, file.name))
.then(() => observePresentationConversion(meetingID, file.name, onConversion))
// Trap the error so we can have parallel upload
.catch((error) => {
onError(error);
return observePresentationConversion(meetingID, file.name);
onUpload({ error: true, done: true, status: error.code });
return Promise.resolve();
});
};
const uploadAndConvertPresentations = (presentationsToUpload, meetingID, uploadEndpoint) =>
Promise.all(
presentationsToUpload.map(p =>
uploadAndConvertPresentation(p.file, meetingID, uploadEndpoint, p.onError, p.onProgress)),
);
Promise.all(presentationsToUpload.map(p =>
uploadAndConvertPresentation(p.file, meetingID, uploadEndpoint, p.onUpload, p.onProgress, p.onConversion)));
const setPresentation = presentationID => makeCall('setPresentation', presentationID);
@ -106,27 +111,42 @@ const removePresentations = presentationsToRemove =>
Promise.all(presentationsToRemove.map(p => removePresentation(p.id)));
const persistPresentationChanges = (oldState, newState, uploadEndpoint) => {
const presentationsToUpload = newState.filter(_ => !oldState.includes(_));
const presentationsToRemove = oldState.filter(_ => !newState.includes(_));
const currentPresentation = newState.find(_ => _.isCurrent);
const presentationsToUpload = newState.filter(p => !p.upload.done);
const presentationsToRemove = oldState.filter(p => !_.find(newState, ['id', p.id]));
return new Promise((resolve, reject) =>
uploadAndConvertPresentations(presentationsToUpload, Auth.meetingID, uploadEndpoint)
.then((presentations) => {
if (!presentations.length && !currentPresentation) return Promise.resolve();
let currentPresentation = newState.find(p => p.isCurrent);
// If its a newly uploaded presentation we need to get its id from promise result
const currentPresentationId =
currentPresentation.id !== currentPresentation.filename ?
currentPresentation.id :
presentations[presentationsToUpload.findIndex(_ => _ === currentPresentation)].id;
return uploadAndConvertPresentations(presentationsToUpload, Auth.meetingID, uploadEndpoint)
.then((presentations) => {
if (!presentations.length && !currentPresentation) return Promise.resolve();
return setPresentation(currentPresentationId);
})
.then(removePresentations.bind(null, presentationsToRemove))
.then(resolve)
.catch(reject),
);
// Update the presentation with their new ids
presentations.forEach((p, i) => {
if (p === undefined) return;
presentationsToUpload[i].onDone(p.id);
});
return Promise.resolve(presentations);
})
.then((presentations) => {
if (currentPresentation === undefined) {
return Promise.resolve();
}
// If its a newly uploaded presentation we need to get it from promise result
if (!currentPresentation.conversion.done) {
const currentIndex = presentationsToUpload.findIndex(p => p === currentPresentation);
currentPresentation = presentations[currentIndex];
}
// skip setting as current if error happened
if (currentPresentation.conversion.error) {
return Promise.resolve();
}
return setPresentation(currentPresentation.id);
})
.then(removePresentations.bind(null, presentationsToRemove));
};
export default {

View File

@ -57,12 +57,14 @@ $item-height: 1rem;
.tableItemIcon,
.tableItemActions,
.tableItemStatus {
.tableItemStatus,
.tableItemCurrent {
width: 1%;
}
.tableItemActions {
min-width: 68px; // size of the 2 icons (check/trash)
text-align: right;
}
.tableItemIcon > i {
@ -75,7 +77,7 @@ $item-height: 1rem;
position: relative;
&:before {
content: '&nbsp;';
content: "\00a0";
visibility: hidden;
}
@ -84,10 +86,13 @@ $item-height: 1rem;
position: absolute;
left: 0;
right: 0;
padding: 0 $sm-padding-x;
}
}
.tableItemCurrent {
padding-left: 0;
}
.tableItemStatus {
text-align: right;
}
@ -100,7 +105,7 @@ $item-height: 1rem;
background-color: transparentize($color-primary, .75);
}
.tableItemProcessing {
.tableItemConverting {
background-color: transparentize($color-success, .75);
}
@ -183,3 +188,18 @@ $item-height: 1rem;
font-size: 80%;
display: block;
}
.currentLabel {
display: inline;
padding: .25em .5em;
font-size: 75%;
font-weight: 700;
line-height: 1;
color: $color-white;
background: $color-primary;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25em;
text-transform: uppercase;
}

View File

@ -195,8 +195,7 @@ class ApplicationMenu extends BaseMenu {
{ availableLocales.map((locale, index) =>
(<option key={index} value={locale.locale}>
{locale.name}
</option>),
) }
</option>)) }
</select>
: null }
</label>

View File

@ -0,0 +1,86 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Tippy from 'tippy.js';
import _ from 'lodash';
import cx from 'classnames';
import { ESCAPE } from '/imports/utils/keyCodes';
const propTypes = {
title: PropTypes.string.isRequired,
position: PropTypes.oneOf(['bottom']),
children: PropTypes.element.isRequired,
className: PropTypes.string,
};
const defaultProps = {
position: 'bottom',
className: null,
};
class Tooltip extends Component {
constructor(props) {
super(props);
this.tippySelectorId = _.uniqueId('tippy-');
this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
this.handleEscapeHide = this.handleEscapeHide.bind(this);
this.delay = [250, 100];
this.dynamicTitle = true;
}
componentDidMount() {
const {
position,
} = this.props;
const options = {
position,
dynamicTitle: this.dynamicTitle,
delay: this.delay,
onShow: this.onShow,
onHide: this.onHide,
};
this.tooltip = Tippy(`#${this.tippySelectorId}`, options);
}
onShow() {
document.addEventListener('keyup', this.handleEscapeHide);
}
onHide() {
document.removeEventListener('keyup', this.handleEscapeHide);
}
handleEscapeHide(e) {
if (e.keyCode !== ESCAPE) return;
this.tooltip.tooltips[0].hide();
}
render() {
const {
children,
className,
title,
...restProps
} = this.props;
const WrappedComponent = React.Children.only(children);
const WrappedComponentBound = React.cloneElement(WrappedComponent, {
...restProps,
title,
id: this.tippySelectorId,
className: cx(children.props.className, className),
});
return WrappedComponentBound;
}
}
export default Tooltip;
Tooltip.defaultProps = defaultProps;
Tooltip.propTypes = propTypes;

View File

@ -5,7 +5,6 @@ import { withRouter } from 'react-router';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import styles from './styles';
import UserListHeader from './user-list-header/component';
import UserContent from './user-list-content/component';
const propTypes = {
@ -27,6 +26,7 @@ const propTypes = {
kickUser: PropTypes.func.isRequired,
toggleVoice: PropTypes.func.isRequired,
changeRole: PropTypes.func.isRequired,
roving: PropTypes.func.isRequired,
};
const defaultProps = {
@ -54,10 +54,6 @@ class UserList extends Component {
render() {
return (
<div className={styles.userList}>
{/* <UserListHeader
intl={this.props.intl}
compact={this.state.compact}
/> */}
{<UserContent
intl={this.props.intl}
openChats={this.props.openChats}
@ -75,6 +71,7 @@ class UserList extends Component {
normalizeEmojiName={this.props.normalizeEmojiName}
isMeetingLocked={this.props.isMeetingLocked}
isPublicChat={this.props.isPublicChat}
roving={this.props.roving}
/>}
</div>
);

View File

@ -1,10 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createContainer } from 'meteor/react-meteor-data';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import Meetings from '/imports/api/meetings';
import Service from './service';
import UserList from './component';
const propTypes = {
openChats: PropTypes.arrayOf(String).isRequired,
openChat: PropTypes.string.isRequired,
users: PropTypes.arrayOf(Object).isRequired,
currentUser: PropTypes.shape({}).isRequired,
meeting: PropTypes.shape({}).isRequired,
isBreakoutRoom: PropTypes.bool.isRequired,
getAvailableActions: PropTypes.func.isRequired,
normalizeEmojiName: PropTypes.func.isRequired,
isMeetingLocked: PropTypes.func.isRequired,
isPublicChat: PropTypes.func.isRequired,
setEmojiStatus: PropTypes.func.isRequired,
assignPresenter: PropTypes.func.isRequired,
kickUser: PropTypes.func.isRequired,
toggleVoice: PropTypes.func.isRequired,
changeRole: PropTypes.func.isRequired,
roving: PropTypes.func.isRequired,
userActions: PropTypes.func.isRequired,
children: PropTypes.Object,
};
const defaultProps = {
children: {},
};
const UserListContainer = (props) => {
const {
users,
@ -24,7 +50,8 @@ const UserListContainer = (props) => {
kickUser,
toggleVoice,
changeRole,
} = props;
roving,
} = props;
return (
<UserList
@ -44,12 +71,16 @@ const UserListContainer = (props) => {
normalizeEmojiName={normalizeEmojiName}
isMeetingLocked={isMeetingLocked}
isPublicChat={isPublicChat}
roving={roving}
>
{children}
</UserList>
);
};
UserListContainer.propTypes = propTypes;
UserListContainer.defaultProps = defaultProps;
export default createContainer(({ params }) => ({
users: Service.getUsers(),
meeting: Meetings.findOne({}),
@ -67,4 +98,5 @@ export default createContainer(({ params }) => ({
kickUser: Service.kickUser,
toggleVoice: Service.toggleVoice,
changeRole: Service.changeRole,
roving: Service.roving,
}), UserListContainer);

View File

@ -8,6 +8,7 @@ import mapUser from '/imports/ui/services/user/mapUser';
import { EMOJI_STATUSES } from '/imports/utils/statuses';
import { makeCall } from '/imports/ui/services/api';
import _ from 'lodash';
import KEY_CODES from '/imports/utils/keyCodes';
const APP_CONFIG = Meteor.settings.public.app;
const ALLOW_MODERATOR_TO_UNMUTE_AUDIO = APP_CONFIG.allowModeratorToUnmuteAudio;
@ -41,18 +42,21 @@ const sortUsersByName = (a, b) => {
};
const sortUsersByEmoji = (a, b) => {
const emojiA = a in EMOJI_STATUSES ? EMOJI_STATUSES[a] : a;
const emojiB = b in EMOJI_STATUSES ? EMOJI_STATUSES[b] : b;
const { status: statusA } = a.emoji;
const { status: statusB } = b.emoji;
if (emojiA && emojiB) {
const emojiA = statusA in EMOJI_STATUSES ? EMOJI_STATUSES[statusA] : statusA;
const emojiB = statusB in EMOJI_STATUSES ? EMOJI_STATUSES[statusB] : statusB;
if (emojiA && emojiB && (emojiA !== EMOJI_STATUSES.none && emojiB !== EMOJI_STATUSES.none)) {
if (a.emoji.changedAt < b.emoji.changedAt) {
return -1;
} else if (a.emoji.changedAt > b.emoji.changedAt) {
return 1;
}
} else if (emojiA) {
} else if (emojiA && emojiA !== EMOJI_STATUSES.none) {
return -1;
} else if (emojiB) {
} else if (emojiB && emojiB !== EMOJI_STATUSES.none) {
return 1;
}
return 0;
@ -72,7 +76,7 @@ const sortUsersByModerator = (a, b) => {
const sortUsersByPhoneUser = (a, b) => {
if (!a.isPhoneUser && !b.isPhoneUser) {
return sortUsersByName(a, b);
return 0;
} else if (!a.isPhoneUser) {
return -1;
} else if (!b.isPhoneUser) {
@ -211,27 +215,46 @@ const getOpenChats = (chatID) => {
.sort(sortChats);
};
const isVoiceOnlyUser = userId => userId.toString().startsWith('v_');
const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
const isDialInUser = isVoiceOnlyUser(user.id) || user.isPhoneUser;
const hasAuthority = currentUser.isModerator || user.isCurrent;
const allowedToChatPrivately = !user.isCurrent;
const allowedToChatPrivately = !user.isCurrent && !isDialInUser;
const allowedToMuteAudio = hasAuthority
&& user.isVoiceUser
&& !user.isMuted
&& !user.isListenOnly;
const allowedToUnmuteAudio = hasAuthority
&& user.isVoiceUser
&& !user.isListenOnly
&& user.isMuted
&& (ALLOW_MODERATOR_TO_UNMUTE_AUDIO || user.isCurrent);
const allowedToResetStatus = hasAuthority && user.emoji.status !== EMOJI_STATUSES.none;
const allowedToResetStatus = hasAuthority
&& user.emoji.status !== EMOJI_STATUSES.none
&& !isDialInUser;
// if currentUser is a moderator, allow kicking other users
const allowedToKick = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
const allowedToSetPresenter = currentUser.isModerator && !user.isPresenter;
const allowedToSetPresenter = currentUser.isModerator
&& !user.isPresenter
&& !isDialInUser;
const allowedToPromote = currentUser.isModerator && !user.isCurrent && !user.isModerator;
const allowedToDemote = currentUser.isModerator && !user.isCurrent && user.isModerator;
const allowedToPromote = currentUser.isModerator
&& !user.isCurrent
&& !user.isModerator
&& !isDialInUser;
const allowedToDemote = currentUser.isModerator
&& !user.isCurrent
&& user.isModerator
&& !isDialInUser;
return {
allowedToChatPrivately,
@ -278,12 +301,54 @@ const setEmojiStatus = (userId) => { makeCall('setEmojiStatus', userId, 'none');
const assignPresenter = (userId) => { makeCall('assignPresenter', userId); };
const kickUser = (userId) => { makeCall('kickUser', userId); };
const kickUser = (userId) => {
if (isVoiceOnlyUser(userId)) {
makeCall('ejectUserFromVoice', userId);
} else {
makeCall('kickUser', userId);
}
};
const toggleVoice = (userId) => { makeCall('toggleVoice', userId); };
const changeRole = (userId, role) => { makeCall('changeRole', userId, role); };
const roving = (event, itemCount, changeState) => {
if (this.selectedIndex === undefined) {
this.selectedIndex = -1;
}
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
document.activeElement.blur();
this.selectedIndex = -1;
changeState(this.selectedIndex);
}
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
this.selectedIndex += 1;
if (this.selectedIndex === itemCount) {
this.selectedIndex = 0;
}
changeState(this.selectedIndex);
}
if (event.keyCode === KEY_CODES.ARROW_UP) {
this.selectedIndex -= 1;
if (this.selectedIndex < 0) {
this.selectedIndex = itemCount - 1;
}
changeState(this.selectedIndex);
}
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE].includes(event.keyCode)) {
document.activeElement.firstChild.click();
}
};
export default {
setEmojiStatus,
assignPresenter,
@ -297,4 +362,5 @@ export default {
normalizeEmojiName,
isMeetingLocked,
isPublicChat,
roving,
};

View File

@ -41,9 +41,6 @@ $item-focus-border: $color-blue-lighter;
%list-item {
display: flex;
flex-flow: row;
padding-top: $sm-padding-y;
padding-bottom: $sm-padding-y;
padding-left: $sm-padding-y;
transition: all 0.3s;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
@ -62,7 +59,7 @@ $item-focus-border: $color-blue-lighter;
&:hover {
background-color: $list-item-bg-hover;
}
}
/* Styling
@ -116,7 +113,7 @@ $item-focus-border: $color-blue-lighter;
font-weight: 600;
text-transform: uppercase;
padding: 0 $sm-padding-x;
margin: ($md-padding-x / 2) 0 0 0;
margin: 0 0 ($lg-padding-x / 2) 0;
color: $color-gray-light;
}

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import KEY_CODES from '/imports/utils/keyCodes';
import styles from './styles';
import UserParticipants from './user-participants/component';
import UserMessages from './user-messages/component';
@ -24,6 +23,7 @@ const propTypes = {
kickUser: PropTypes.func.isRequired,
toggleVoice: PropTypes.func.isRequired,
changeRole: PropTypes.func.isRequired,
roving: PropTypes.func.isRequired,
};
const defaultProps = {
@ -35,82 +35,6 @@ const defaultProps = {
};
class UserContent extends Component {
static focusElement(active, element) {
const modifiedActive = active;
const modifiedElement = element;
if (!modifiedActive.getAttribute('role') === 'tabpanel') {
modifiedActive.tabIndex = -1;
}
modifiedElement.tabIndex = 0;
modifiedElement.focus();
}
static removeFocusFromChildren(children, numberOfItems) {
const modifiedChildren = children;
for (let i = 0; i < numberOfItems; i += 1) {
modifiedChildren.childNodes[i].tabIndex = -1;
}
}
constructor(props) {
super(props);
this.rovingIndex = this.rovingIndex.bind(this);
this.focusList = this.focusList.bind(this);
this.focusedItemIndex = -1;
}
focusList(list) {
const focusList = list;
document.activeElement.tabIndex = -1;
this.focusedItemIndex = -1;
focusList.tabIndex = 0;
focusList.focus();
}
rovingIndex(event, list, items, numberOfItems) {
const active = document.activeElement;
const changedItems = items;
if (event.keyCode === KEY_CODES.TAB) {
if (this.focusedItemIndex !== -1) {
this.focusedItemIndex = 0;
UserContent.removeFocusFromChildren(changedItems, numberOfItems);
}
}
if (event.keyCode === KEY_CODES.ESCAPE
|| this.focusedItemIndex < 0
|| this.focusedItemIndex > numberOfItems) {
this.focusList(list);
}
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.ARROW_SPACE].includes(event.keyCode)) {
active.firstChild.click();
}
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
this.focusedItemIndex += 1;
if (this.focusedItemIndex === numberOfItems) {
this.focusedItemIndex = 0;
}
UserContent.focusElement(active, changedItems.childNodes[this.focusedItemIndex]);
}
if (event.keyCode === KEY_CODES.ARROW_UP) {
this.focusedItemIndex -= 1;
if (this.focusedItemIndex < 0) {
this.focusedItemIndex = numberOfItems - 1;
}
UserContent.focusElement(active, changedItems.childNodes[this.focusedItemIndex]);
}
}
render() {
return (
<div className={styles.content}>
@ -119,7 +43,7 @@ class UserContent extends Component {
openChats={this.props.openChats}
compact={this.props.compact}
intl={this.props.intl}
rovingIndex={this.rovingIndex}
roving={this.props.roving}
/>
<UserParticipants
users={this.props.users}
@ -135,8 +59,8 @@ class UserContent extends Component {
changeRole={this.props.changeRole}
getAvailableActions={this.props.getAvailableActions}
normalizeEmojiName={this.props.normalizeEmojiName}
rovingIndex={this.rovingIndex}
isMeetingLocked={this.props.isMeetingLocked}
roving={this.props.roving}
/>
</div>
);

View File

@ -10,14 +10,18 @@
}
.scrollableList {
pointer-events: none;
@include elementFocus($list-item-bg-hover);
@include scrollbox-vertical($user-list-bg);
&:active {
box-shadow: none;
border-radius: none;
}
}
.list {
margin-left: $md-padding-y;
pointer-events: all;
margin-bottom: 1px;
}
.smallTitle {

View File

@ -13,8 +13,8 @@ const propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
rovingIndex: PropTypes.func.isRequired,
isPublicChat: PropTypes.func.isRequired,
roving: PropTypes.func.isRequired,
};
const defaultProps = {
@ -32,80 +32,102 @@ const listTransition = {
};
const intlMessages = defineMessages({
usersTitle: {
id: 'app.userList.usersTitle',
description: 'Title for the Header',
},
messagesTitle: {
id: 'app.userList.messagesTitle',
description: 'Title for the messages list',
},
participantsTitle: {
id: 'app.userList.participantsTitle',
description: 'Title for the Users list',
},
toggleCompactView: {
id: 'app.userList.toggleCompactView.label',
description: 'Toggle user list view mode',
},
ChatLabel: {
id: 'app.userList.menu.chat.label',
description: 'Save the changes and close the settings menu',
},
ClearStatusLabel: {
id: 'app.userList.menu.clearStatus.label',
description: 'Clear the emoji status of this user',
},
MakePresenterLabel: {
id: 'app.userList.menu.makePresenter.label',
description: 'Set this user to be the presenter in this meeting',
},
KickUserLabel: {
id: 'app.userList.menu.kickUser.label',
description: 'Forcefully remove this user from the meeting',
},
MuteUserAudioLabel: {
id: 'app.userList.menu.muteUserAudio.label',
description: 'Forcefully mute this user',
},
UnmuteUserAudioLabel: {
id: 'app.userList.menu.unmuteUserAudio.label',
description: 'Forcefully unmute this user',
},
PromoteUserLabel: {
id: 'app.userList.menu.promoteUser.label',
description: 'Forcefully promote this viewer to a moderator',
},
DemoteUserLabel: {
id: 'app.userList.menu.demoteUser.label',
description: 'Forcefully demote this moderator to a viewer',
},
});
class UserMessages extends Component {
constructor() {
super();
this.state = {
index: -1,
};
this.openChatRefs = [];
this.selectedIndex = -1;
this.focusOpenChatItem = this.focusOpenChatItem.bind(this);
this.changeState = this.changeState.bind(this);
}
componentDidMount() {
if (!this.props.compact) {
this._msgsList.addEventListener(
'keydown',
event => this.props.rovingIndex(
event => this.props.roving(
event,
this._msgsList,
this._msgItems,
this.props.openChats.length,
this.changeState,
),
);
}
}
render() {
componentDidUpdate(prevProps, prevState) {
if (this.state.index === -1) {
return;
}
if (this.state.index !== prevState.index) {
this.focusOpenChatItem(this.state.index);
}
}
getOpenChats() {
const {
openChats,
openChat,
intl,
compact,
isPublicChat,
} = this.props;
let index = -1;
return openChats.map(chat => (
<CSSTransition
classNames={listTransition}
appear
enter
exit={false}
timeout={0}
component="div"
className={cx(styles.chatsList)}
key={chat.id}
>
<div ref={(node) => { this.openChatRefs[index += 1] = node; }}>
<ChatListItem
isPublicChat={isPublicChat}
compact={compact}
openChat={openChat}
chat={chat}
tabIndex={-1}
/>
</div>
</CSSTransition>
));
}
changeState(newIndex) {
this.setState({ index: newIndex });
}
focusOpenChatItem(index) {
if (!this.openChatRefs[index]) {
return;
}
this.openChatRefs[index].firstChild.focus();
}
render() {
const {
intl,
compact,
} = this.props;
return (
<div className={styles.messages}>
{
@ -120,28 +142,9 @@ class UserMessages extends Component {
className={styles.scrollableList}
ref={(ref) => { this._msgsList = ref; }}
>
<div ref={(ref) => { this._msgItems = ref; }} className={styles.list}>
<TransitionGroup>
{openChats.map(chat => (
<CSSTransition
classNames={listTransition}
appear
enter
exit={false}
timeout={0}
component="div"
className={cx(styles.chatsList)}
key={chat.id}
>
<ChatListItem
isPublicChat={isPublicChat}
compact={compact}
openChat={openChat}
chat={chat}
tabIndex={-1}
/>
</CSSTransition>
))}
<div className={styles.list}>
<TransitionGroup ref={(ref) => { this._msgItems = ref; }} >
{ this.getOpenChats() }
</TransitionGroup>
</div>
</div>

View File

@ -23,7 +23,7 @@ const propTypes = {
getAvailableActions: PropTypes.func.isRequired,
normalizeEmojiName: PropTypes.func.isRequired,
isMeetingLocked: PropTypes.func.isRequired,
rovingIndex: PropTypes.func.isRequired,
roving: PropTypes.func.isRequired,
};
const defaultProps = {
@ -48,18 +48,6 @@ const intlMessages = defineMessages({
id: 'app.userList.usersTitle',
description: 'Title for the Header',
},
messagesTitle: {
id: 'app.userList.messagesTitle',
description: 'Title for the messages list',
},
participantsTitle: {
id: 'app.userList.participantsTitle',
description: 'Title for the Users list',
},
toggleCompactView: {
id: 'app.userList.toggleCompactView.label',
description: 'Toggle user list view mode',
},
ChatLabel: {
id: 'app.userList.menu.chat.label',
description: 'Save the changes and close the settings menu',
@ -98,88 +86,157 @@ class UserParticipants extends Component {
constructor() {
super();
this.state = {
index: -1,
};
this.userRefs = [];
this.selectedIndex = -1;
this.getScrollContainerRef = this.getScrollContainerRef.bind(this);
this.focusUserItem = this.focusUserItem.bind(this);
this.changeState = this.changeState.bind(this);
this.getUsers = this.getUsers.bind(this);
}
componentDidMount() {
if (!this.props.compact) {
this.refScrollContainer.addEventListener(
'keydown',
event => this.props.rovingIndex(
event => this.props.roving(
event,
this.refScrollContainer,
this.refScrollItems,
this.props.users.length,
this.changeState,
),
);
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.index === -1) {
return;
}
if (this.state.index !== prevState.index) {
this.focusUserItem(this.state.index);
}
}
getScrollContainerRef() {
return this.refScrollContainer;
}
render() {
getUsers() {
const {
users,
currentUser,
compact,
isBreakoutRoom,
intl,
currentUser,
meeting,
getAvailableActions,
normalizeEmojiName,
isMeetingLocked,
compact,
setEmojiStatus,
users,
intl,
changeRole,
assignPresenter,
setEmojiStatus,
kickUser,
toggleVoice,
changeRole,
} = this.props;
const userActions =
{
openChat: {
label: () => intl.formatMessage(intlMessages.ChatLabel),
handler: (router, user) => router.push(`/users/chat/${user.id}`),
icon: 'chat',
},
clearStatus: {
label: () => intl.formatMessage(intlMessages.ClearStatusLabel),
handler: user => setEmojiStatus(user.id, 'none'),
icon: 'clear_status',
},
setPresenter: {
label: () => intl.formatMessage(intlMessages.MakePresenterLabel),
handler: user => assignPresenter(user.id),
icon: 'presentation',
},
kick: {
label: user => intl.formatMessage(intlMessages.KickUserLabel, { 0: user.name }),
handler: user => kickUser(user.id),
icon: 'circle_close',
},
mute: {
label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel),
handler: user => toggleVoice(user.id),
icon: 'audio_off',
},
unmute: {
label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel),
handler: user => toggleVoice(user.id),
icon: 'audio_on',
},
promote: {
label: user => intl.formatMessage(intlMessages.PromoteUserLabel, { 0: user.name }),
handler: user => changeRole(user.id, 'MODERATOR'),
icon: 'promote',
},
demote: {
label: user => intl.formatMessage(intlMessages.DemoteUserLabel, { 0: user.name }),
handler: user => changeRole(user.id, 'VIEWER'),
icon: 'user',
},
};
{
openChat: {
label: () => intl.formatMessage(intlMessages.ChatLabel),
handler: (router, user) => router.push(`/users/chat/${user.id}`),
icon: 'chat',
},
clearStatus: {
label: () => intl.formatMessage(intlMessages.ClearStatusLabel),
handler: user => setEmojiStatus(user.id, 'none'),
icon: 'clear_status',
},
setPresenter: {
label: () => intl.formatMessage(intlMessages.MakePresenterLabel),
handler: user => assignPresenter(user.id),
icon: 'presentation',
},
kick: {
label: user => intl.formatMessage(intlMessages.KickUserLabel, { 0: user.name }),
handler: user => kickUser(user.id),
icon: 'circle_close',
},
mute: {
label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel),
handler: user => toggleVoice(user.id),
icon: 'audio_off',
},
unmute: {
label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel),
handler: user => toggleVoice(user.id),
icon: 'audio_on',
},
promote: {
label: user => intl.formatMessage(intlMessages.PromoteUserLabel, { 0: user.name }),
handler: user => changeRole(user.id, 'MODERATOR'),
icon: 'promote',
},
demote: {
label: user => intl.formatMessage(intlMessages.DemoteUserLabel, { 0: user.name }),
handler: user => changeRole(user.id, 'VIEWER'),
icon: 'user',
},
};
let index = -1;
return users.map(user => (
<CSSTransition
classNames={listTransition}
appear
enter
exit
timeout={0}
component="div"
className={cx(styles.participantsList)}
key={user.id}
>
<div ref={(node) => { this.userRefs[index += 1] = node; }}>
<UserListItem
compact={compact}
isBreakoutRoom={isBreakoutRoom}
user={user}
currentUser={currentUser}
userActions={userActions}
meeting={meeting}
getAvailableActions={getAvailableActions}
normalizeEmojiName={normalizeEmojiName}
isMeetingLocked={isMeetingLocked}
getScrollContainerRef={this.getScrollContainerRef}
/>
</div>
</CSSTransition>
));
}
focusUserItem(index) {
if (!this.userRefs[index]) {
return;
}
this.userRefs[index].firstChild.focus();
}
changeState(newIndex) {
this.setState({ index: newIndex });
}
render() {
const {
users,
intl,
compact,
} = this.props;
return (
<div className={styles.participants}>
@ -196,33 +253,9 @@ class UserParticipants extends Component {
tabIndex={0}
ref={(ref) => { this.refScrollContainer = ref; }}
>
<div ref={(ref) => { this.refScrollItems = ref; }} className={styles.list}>
<TransitionGroup>
{ users.map(user => (
<CSSTransition
classNames={listTransition}
appear
enter
exit
timeout={0}
component="div"
className={cx(styles.participantsList)}
key={user.id}
>
<UserListItem
compact={compact}
isBreakoutRoom={isBreakoutRoom}
user={user}
currentUser={currentUser}
userActions={userActions}
meeting={meeting}
getAvailableActions={getAvailableActions}
normalizeEmojiName={normalizeEmojiName}
isMeetingLocked={isMeetingLocked}
getScrollContainerRef={this.getScrollContainerRef}
/>
</CSSTransition>
))}
<div className={styles.list}>
<TransitionGroup ref={(ref) => { this.refScrollItems = ref; }}>
{ this.getUsers() }
</TransitionGroup>
</div>
</div>

View File

@ -25,7 +25,7 @@ const propTypes = {
}).isRequired,
userActions: PropTypes.shape({}).isRequired,
router: PropTypes.shape({}).isRequired,
isBreakoutRoom: PropTypes.bool.isRequired,
isBreakoutRoom: PropTypes.bool,
getAvailableActions: PropTypes.func.isRequired,
meeting: PropTypes.shape({}).isRequired,
isMeetingLocked: PropTypes.func.isRequired,
@ -34,12 +34,10 @@ const propTypes = {
};
const defaultProps = {
shouldShowActions: false,
isBreakoutRoom: false,
};
class UserListItem extends Component {
static createAction(action, ...options) {
return (
<UserAction

View File

@ -57,7 +57,6 @@ const propTypes = {
class UserListContent extends Component {
/**
* Return true if the content fit on the screen, false otherwise.
*
@ -140,8 +139,10 @@ class UserListContent extends Component {
};
const isDropdownVisible =
UserListContent.checkIfDropdownIsVisible(dropdownContent.offsetTop,
dropdownContent.offsetHeight);
UserListContent.checkIfDropdownIsVisible(
dropdownContent.offsetTop,
dropdownContent.offsetHeight,
);
if (!isDropdownVisible) {
const offsetPageTop =
@ -201,13 +202,15 @@ class UserListContent extends Component {
? intl.formatMessage(messages.presenter)
: '';
const userAriaLabel = intl.formatMessage(messages.userAriaLabel,
const userAriaLabel = intl.formatMessage(
messages.userAriaLabel,
{
0: user.name,
1: presenter,
2: you,
3: user.emoji.status,
});
},
);
const contents = (
<div

View File

@ -72,10 +72,6 @@
@extend %list-item;
flex-flow: column;
flex-shrink: 0;
padding-top: $lg-padding-y;
padding-bottom: $lg-padding-y;
padding-left: $lg-padding-y;
}
.userAvatar {
@ -86,6 +82,10 @@
flex-grow: 0;
display: flex;
flex-flow: row;
padding-top: $lg-padding-y;
padding-bottom: $lg-padding-y;
padding-left: $lg-padding-y;
}
.dropdown {

View File

@ -1,43 +0,0 @@
import React from 'react';
import { defineMessages } from 'react-intl';
import PropTypes from 'prop-types';
import styles from './styles';
const intlMessages = defineMessages({
participantsTitle: {
id: 'app.userList.participantsTitle',
description: 'Title for the Users list',
},
});
const propTypes = {
compact: PropTypes.bool,
intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired }).isRequired,
};
const defaultProps = {
compact: false,
};
const UserListHeader = props => (
<div className={styles.header}>
{
!props.compact ?
<div className={styles.headerTitle} role="banner">
{props.intl.formatMessage(intlMessages.participantsTitle)}
</div> : null
}
{/* <Button
label={intl.formatMessage(intlMessages.toggleCompactView)}
hideLabel
icon={!this.state.compact ? 'left_arrow' : 'right_arrow'}
className={styles.btnToggle}
onClick={this.handleToggleCompactView}
/> */}
</div>
);
UserListHeader.propTypes = propTypes;
UserListHeader.defaultProps = defaultProps;
export default UserListHeader;

View File

@ -1,18 +0,0 @@
@import "/imports/ui/components/user-list/styles.scss";
.header {
@extend %flex-column;
justify-content: left;
flex-grow: 0;
display: flex;
flex-direction: row;
padding: 0 $md-padding-x;
margin: $md-padding-x 0;
}
.headerTitle {
flex: 0;
font-size: 1rem;
font-weight: 600;
color: $color-heading;
}

View File

@ -60,13 +60,14 @@ export default class TextDrawComponent extends Component {
// that's why we have a separate case for iOS - we don't focus here automatically
// but we focus on the next "tap" invoked by a user
const iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;
const Android = navigator.userAgent.toLowerCase().indexOf('android') > -1;
// unsupported Firefox condition (not iOS though) can be removed when FF 59 is released
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1409113
const unsupportedFirefox = navigator.userAgent.indexOf('Firefox/57') !== -1
|| navigator.userAgent.indexOf('Firefox/58') !== -1;
if (iOS || unsupportedFirefox) { return; }
if (iOS || (Android && unsupportedFirefox)) { return; }
if (this.props.isActive && this.props.annotation.status !== DRAW_END) {
this.handleFocus();
@ -183,6 +184,7 @@ export default class TextDrawComponent extends Component {
onChange={this.onChangeHandler}
onBlur={this.handleOnBlur}
style={styles}
spellCheck="false"
/>
</foreignObject>
</g>

View File

@ -80,7 +80,7 @@ export default class ToolbarSubmenu extends Component {
{objectsToRender ? objectsToRender.map(obj =>
(
<ToolbarSubmenuItem
label={label}
label={obj.value}
icon={!customIcon ? obj.icon : null}
customIcon={customIcon ? ToolbarSubmenu.getCustomIcon(type, obj) : null}
onItemClick={this.onItemClick}

View File

@ -32,7 +32,7 @@ class Settings {
});
// Sets default locale to browser locale
defaultValues.application.locale = navigator.languages[0] ||
defaultValues.application.locale = navigator.languages ? navigator.languages[0] : false ||
navigator.language ||
defaultValues.application.locale;

View File

@ -4,13 +4,13 @@ $border-size-large: 3px;
$border-radius: .2rem;
$sm-padding-x: .75rem;
$sm-padding-y: .25rem;
$sm-padding-y: .3rem;
$md-padding-x: 1rem;
$md-padding-y: .375rem;
$md-padding-y: .45rem;
$lg-padding-x: 1.25rem;
$lg-padding-y: .5rem;
$lg-padding-y: 0.6rem;
$jumbo-padding-x: 3.025rem;
$jumbo-padding-y: 1.25rem;
$jumbo-padding-y: 1.5rem;

View File

@ -215,10 +215,6 @@
"integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=",
"dev": true
},
"attr-accept": {
"version": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.0.tgz",
"integrity": "sha1-tc01In8WOTWo8d4Q7T66FpQfa+Y="
},
"autoprefixer": {
"version": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.6.tgz",
"integrity": "sha1-+5MwOfdK90qD5xIlznjZ/Vi6hNc=",
@ -1282,7 +1278,7 @@
},
"fibers": {
"version": "https://registry.npmjs.org/fibers/-/fibers-2.0.0.tgz",
"integrity": "sha1-8m0Krx+ZmV++HLPzQO+sCL2p3Es=",
"integrity": "sha512-sLxo4rZVk7xLgAjb/6zEzHJfSALx6u6coN1z61XCOF7i6CyTdJawF4+RdpjCSeS8AP66eR2InScbYAz9RAVOgA==",
"dev": true
},
"figures": {
@ -1427,7 +1423,7 @@
},
"glob": {
"version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"requires": {
"fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -1593,7 +1589,7 @@
},
"humanize-duration": {
"version": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.10.1.tgz",
"integrity": "sha1-ZbVQwKoJUVbst8NA20TuC99xr0s=",
"integrity": "sha512-FHD+u5OKj8TSsSdMHJxSCC78N5Rt4ecil6sWvI+xPbUKhxvHmkKo/V8imbR1m2dXueZYLIl7PcSYX9i/oEiOIA==",
"dev": true
},
"husky": {
@ -1629,7 +1625,7 @@
},
"immutability-helper": {
"version": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-2.4.0.tgz",
"integrity": "sha1-ANQh4pV8F/DweBR18F/9g35zRY0=",
"integrity": "sha512-rW/L/56ZMo9NStMK85kFrUFFGy4NeJbCdhfrDHIZrFfxYtuwuxD+dT3mWMcdmrNO61hllc60AeGglCRhfZ1dZw==",
"requires": {
"invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz"
}
@ -3503,6 +3499,12 @@
"integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c=",
"dev": true
},
"popper.js": {
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.12.9.tgz",
"integrity": "sha1-DfvC3/lsRRuzMu3Pz6r1ZtMx1bM=",
"dev": true
},
"postcss": {
"version": "https://registry.npmjs.org/postcss/-/postcss-6.0.13.tgz",
"integrity": "sha1-ueyrTuAMids+yTEUW9lZC78/El8=",
@ -3745,14 +3747,6 @@
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
}
},
"react-dropzone": {
"version": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-4.2.1.tgz",
"integrity": "sha1-aV6AvQsGXxGB5p8tD20dXMcmZMk=",
"requires": {
"attr-accept": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.0.tgz",
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
}
},
"react-intl": {
"version": "https://registry.npmjs.org/react-intl/-/react-intl-2.4.0.tgz",
"integrity": "sha1-ZsFNyd+ac7L7v71gIXJugKYT6xU=",
@ -3784,20 +3778,13 @@
"warning": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz"
}
},
"react-tabs": {
"version": "https://registry.npmjs.org/react-tabs/-/react-tabs-2.1.0.tgz",
"integrity": "sha1-uhhKUZ4KCAPPeQoesZvE/bpf0Oo=",
"react-tippy": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/react-tippy/-/react-tippy-1.2.2.tgz",
"integrity": "sha512-xqmymAhKub1JGtLJ+HncUauBpwJjHAp6EkKBLeGtuhneaGQ3GnRp5aEd/YRNc4NmIb6o1lbf/Z6R9G3/VjnjYA==",
"dev": true,
"requires": {
"classnames": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz",
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
}
},
"react-toastify": {
"version": "https://registry.npmjs.org/react-toastify/-/react-toastify-2.1.6.tgz",
"integrity": "sha1-Gkh/rSekjZ6u9FaDXpVevnmxp5A=",
"requires": {
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz",
"react-transition-group": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.2.1.tgz"
"popper.js": "1.12.9"
}
},
"react-toggle": {
@ -4396,7 +4383,7 @@
},
"tiny-emitter": {
"version": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
"integrity": "sha1-gtJ0aKylrejl/R5tIrV91D69+3w="
"integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow=="
},
"tinycolor2": {
"version": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",

View File

@ -41,11 +41,12 @@
"react-modal": "~3.0.4",
"react-router": "~3.0.2",
"react-tabs": "~2.1.0",
"react-toggle": "~4.0.2",
"react-toastify": "~2.1.2",
"react-toggle": "~4.0.2",
"react-transition-group": "~2.2.1",
"redis": "~2.8.0",
"string-hash": "~1.1.3",
"tippy.js": "^2.0.2",
"winston": "~2.4.0",
"xml2js": "~0.4.19"
},

View File

@ -31,6 +31,7 @@ acl:
- 'toggleVoice'
- 'clearPublicChatHistory'
- 'changeRole'
- 'ejectUserFromVoice'
presenter:
methods:
- 'assignPresenter'

View File

@ -12,3 +12,4 @@ redis:
- 'from-akka-apps-wb-redis-channel'
ignored:
- 'CheckAlivePongSysMsg'
- 'DoLatencyTracerMsg'

View File

@ -62,13 +62,17 @@
"app.presentationUploder.dropzoneLabel": "Drag files here to upload",
"app.presentationUploder.browseFilesLabel": "or browse for files",
"app.presentationUploder.fileToUpload": "To be uploaded...",
"app.presentationUploder.currentBadge": "Current",
"app.presentationUploder.genericError": "Ops, something went wrong",
"app.presentationUploder.upload.progress": "Uploading ({progress}%)",
"app.presentationUploder.upload.413": "File is too large.",
"app.presentationUploder.upload.413": "File is too large",
"app.presentationUploder.conversion.conversionProcessingSlides": "Processing page {current} of {total}",
"app.presentationUploder.conversion.genericConversionStatus": "Converting file...",
"app.presentationUploder.conversion.generatingThumbnail": "Generating thumbnails...",
"app.presentationUploder.conversion.generatedSlides": "Slides generated...",
"app.presentationUploder.conversion.generatingSvg": "Generating SVG images...",
"presentationUploder.conversion.generatedSlides": "Slides generated...",
"app.presentationUploder.conversion.pageCountExceeded": "Ops, the page count exceeded the limit",
"app.presentationUploder.conversion.timeout": "Ops, the conversion is taking too long",
"app.polling.pollingTitle": "Polling Options",
"app.failedMessage": "Apologies, trouble connecting to the server.",
"app.connectingMessage": "Connecting...",