Merge branch 'master' into fix-chat-auto-reconnect
This commit is contained in:
commit
e2a1cc786f
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
1
akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala
Normal file → Executable file
1
akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala
Normal file → Executable 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
|
||||
|
@ -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 =>
|
||||
|
@ -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)
|
||||
|
@ -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
1
bbb-lti/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.asscache
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
14
bbb-lti/grails-app/assets/stylesheets/tool.css
Normal file
14
bbb-lti/grails-app/assets/stylesheets/tool.css
Normal 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;
|
||||
}
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -39,9 +39,10 @@ tool.view.recording.confirmation.yes=Si
|
||||
tool.view.recording.confirmation.no=No
|
||||
tool.view.activity=Actividad
|
||||
tool.view.description=Descripción
|
||||
tool.view.preview=Vista preliminar
|
||||
tool.view.date=Fecha
|
||||
tool.view.duration=Duració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ón.
|
||||
tool.error.general=No pudo estableserce la conexión.
|
||||
|
@ -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éo</u></a>.<br><br>Pour activer l'audio cliquez sur l'icône du casque à écouteurs (coin supérieur gauche). <b>S'il vous pla<6C>t utiliser le casque pour éviter de causer du bruit.</b>
|
||||
|
||||
tool.view.app=BigBlueButton
|
||||
tool.view.title=LTI Interface pour BigBlueButton
|
||||
tool.view.join=Saisie de la réunion
|
||||
tool.view.recording=Enregistrement
|
||||
tool.view.recording.format.presentation=presentation
|
||||
tool.view.recording.format.video=video
|
||||
tool.view.recording.delete.confirmation=Veillez à supprimer dé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épublier
|
||||
tool.view.recording.delete=Supprimer
|
||||
tool.view.activity=Activité
|
||||
tool.view.description=Description
|
||||
tool.view.date=Date
|
||||
tool.view.duration=Durée
|
||||
tool.view.actions=Actions
|
||||
tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
|
||||
|
||||
tool.error.general=Pas possible é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éo</u></a>.<br><br>Pour activer l'audio cliquez sur l'icône du casque à écouteurs (coin supérieur gauche). <b>S'il vous pla<6C>t utiliser le casque pour éviter de causer du bruit.</b>
|
||||
|
||||
tool.view.app=BigBlueButton
|
||||
tool.view.title=LTI Interface pour BigBlueButton
|
||||
tool.view.join=Saisie de la réunion
|
||||
tool.view.recording=Enregistrement
|
||||
tool.view.recording.format.presentation=presentation
|
||||
tool.view.recording.format.video=video
|
||||
tool.view.recording.delete.confirmation=Veillez à supprimer dé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épublier
|
||||
tool.view.recording.delete=Supprimer
|
||||
tool.view.activity=Activité
|
||||
tool.view.description=Description
|
||||
tool.view.preview=Apreçu
|
||||
tool.view.date=Date
|
||||
tool.view.duration=Durée
|
||||
tool.view.actions=Actions
|
||||
tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
|
||||
|
||||
tool.error.general=Pas possible établir la connection.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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> 
|
||||
</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> 
|
||||
</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>
|
||||
|
4
bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/RequestPresenterGroupEvent.as
Normal file → Executable file
4
bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/RequestPresenterGroupEvent.as
Normal file → Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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++) {
|
||||
|
@ -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 = {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
}));
|
||||
|
@ -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);
|
||||
}
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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]();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 />
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)}
|
||||
<span className={styles.dropzoneLink}>
|
||||
|
@ -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 {
|
||||
|
@ -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: ' ';
|
||||
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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
47
bigbluebutton-html5/package-lock.json
generated
47
bigbluebutton-html5/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -31,6 +31,7 @@ acl:
|
||||
- 'toggleVoice'
|
||||
- 'clearPublicChatHistory'
|
||||
- 'changeRole'
|
||||
- 'ejectUserFromVoice'
|
||||
presenter:
|
||||
methods:
|
||||
- 'assignPresenter'
|
||||
|
@ -12,3 +12,4 @@ redis:
|
||||
- 'from-akka-apps-wb-redis-channel'
|
||||
ignored:
|
||||
- 'CheckAlivePongSysMsg'
|
||||
- 'DoLatencyTracerMsg'
|
||||
|
@ -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...",
|
||||
|
Loading…
Reference in New Issue
Block a user