Merge tag 'v2.6.0-beta.2' into 26beta2-dev
This commit is contained in:
commit
3ec185ec4f
@ -116,9 +116,8 @@ case class CapturePresentationReqInternalMsg(userId: String, parentMeetingId: St
|
||||
* Sent by parent meeting to breakout room to import shared notes.
|
||||
* @param parentMeetingId
|
||||
* @param meetingName
|
||||
* @param sequence
|
||||
*/
|
||||
case class CaptureSharedNotesReqInternalMsg(parentMeetingId: String, meetingName: String, sequence: Int) extends InMessage
|
||||
case class CaptureSharedNotesReqInternalMsg(parentMeetingId: String, meetingName: String) extends InMessage
|
||||
|
||||
// DeskShare
|
||||
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||
|
@ -20,7 +20,7 @@ trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
|
||||
|
||||
if (liveMeeting.props.breakoutProps.captureNotes) {
|
||||
val meetingName: String = liveMeeting.props.meetingProp.name
|
||||
val captureNotesEvent = BigBlueButtonEvent(msg.breakoutId, CaptureSharedNotesReqInternalMsg(msg.parentId, meetingName, liveMeeting.props.breakoutProps.sequence))
|
||||
val captureNotesEvent = BigBlueButtonEvent(msg.breakoutId, CaptureSharedNotesReqInternalMsg(msg.parentId, meetingName))
|
||||
eventBus.publish(captureNotesEvent)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ trait PreuploadedPresentationsPubMsgHdlr {
|
||||
val pages = new collection.mutable.HashMap[String, PageVO]()
|
||||
|
||||
pres.pages.foreach { p =>
|
||||
val page = new PageVO(p.id, p.num, p.thumbUri, p.swfUri, p.txtUri, p.svgUri, p.current, p.xOffset, p.yOffset,
|
||||
val page = new PageVO(p.id, p.num, p.thumbUri, p.txtUri, p.svgUri, p.current, p.xOffset, p.yOffset,
|
||||
p.widthRatio, p.heightRatio)
|
||||
pages += page.id -> page
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
package org.bigbluebutton.core.apps.presentationpod
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
trait PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
|
||||
this: PresentationPodHdlrs =>
|
||||
|
||||
def handle(
|
||||
msg: PresentationHasInvalidMimeTypeErrorSysPubMsg, state: MeetingState2x,
|
||||
liveMeeting: LiveMeeting, bus: MessageBus
|
||||
): MeetingState2x = {
|
||||
|
||||
def broadcastEvent(msg: PresentationHasInvalidMimeTypeErrorSysPubMsg): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(
|
||||
MessageTypes.BROADCAST_TO_MEETING,
|
||||
liveMeeting.props.meetingProp.intId, msg.header.userId
|
||||
)
|
||||
val envelope = BbbCoreEnvelope(PresentationHasInvalidMimeTypeErrorEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(
|
||||
PresentationHasInvalidMimeTypeErrorEvtMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId, msg.header.userId
|
||||
)
|
||||
|
||||
val body = PresentationHasInvalidMimeTypeErrorEvtMsgBody(msg.body.podId, msg.body.meetingId,
|
||||
msg.body.presentationName, msg.body.temporaryPresentationId,
|
||||
msg.body.presentationId, msg.body.messageKey, msg.body.fileMime, msg.body.fileExtension)
|
||||
val event = PresentationHasInvalidMimeTypeErrorEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
broadcastEvent(msg)
|
||||
state
|
||||
}
|
||||
}
|
@ -26,7 +26,8 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
|
||||
with PresentationPageConvertedSysMsgHdlr
|
||||
with PresentationPageConversionStartedSysMsgHdlr
|
||||
with PresentationConversionEndedSysMsgHdlr
|
||||
with PresentationUploadedFileTimeoutErrorPubMsgHdlr {
|
||||
with PresentationUploadedFileTimeoutErrorPubMsgHdlr
|
||||
with PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
|
||||
|
||||
val log = Logging(context.system, getClass)
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ object PresentationPodsApp {
|
||||
id = page.id,
|
||||
num = page.num,
|
||||
thumbUri = page.urls.getOrElse("thumb", ""),
|
||||
swfUri = page.urls.getOrElse("swf", ""),
|
||||
txtUri = page.urls.getOrElse("text", ""),
|
||||
svgUri = page.urls.getOrElse("svg", ""),
|
||||
current = page.current,
|
||||
@ -80,7 +79,6 @@ object PresentationPodsApp {
|
||||
id = page.id,
|
||||
num = page.num,
|
||||
thumbUri = page.urls.getOrElse("thumb", ""),
|
||||
swfUri = page.urls.getOrElse("swf", ""),
|
||||
txtUri = page.urls.getOrElse("text", ""),
|
||||
svgUri = page.urls.getOrElse("svg", ""),
|
||||
current = page.current,
|
||||
|
@ -43,6 +43,16 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildBroadcastPresAnnStatusMsg(presAnnStatusMsg: PresAnnStatusMsg, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, "not-used")
|
||||
val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(PresAnnStatusEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
|
||||
val body = PresAnnStatusEvtMsgBody(presId = presAnnStatusMsg.body.presId, pageNumber = presAnnStatusMsg.body.pageNumber, totalPages = presAnnStatusMsg.body.totalPages, status = presAnnStatusMsg.body.status, error = presAnnStatusMsg.body.error)
|
||||
val event = PresAnnStatusEvtMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildPresentationUploadTokenSysPubMsg(parentId: String, userId: String, presentationUploadToken: String, filename: String): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(PresentationUploadTokenSysPubMsg.NAME, routing)
|
||||
@ -192,6 +202,10 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
bus.outGW.send(BbbCommonEnvCoreMsg(envelope, event))
|
||||
}
|
||||
|
||||
def handle(m: PresAnnStatusMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
bus.outGW.send(buildBroadcastPresAnnStatusMsg(m, liveMeeting))
|
||||
}
|
||||
|
||||
def handle(m: PadCapturePubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
|
||||
val userId: String = "system"
|
||||
|
@ -288,6 +288,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[PreuploadedPresentationsSysPubMsg](envelope, jsonNode)
|
||||
case PresentationUploadedFileTooLargeErrorSysPubMsg.NAME =>
|
||||
routeGenericMsg[PresentationUploadedFileTooLargeErrorSysPubMsg](envelope, jsonNode)
|
||||
case PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME =>
|
||||
routeGenericMsg[PresentationHasInvalidMimeTypeErrorSysPubMsg](envelope, jsonNode)
|
||||
case PresentationUploadedFileTimeoutErrorSysPubMsg.NAME =>
|
||||
routeGenericMsg[PresentationUploadedFileTimeoutErrorSysPubMsg](envelope, jsonNode)
|
||||
case PresentationConversionUpdateSysPubMsg.NAME =>
|
||||
@ -314,6 +316,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode)
|
||||
case NewPresAnnFileAvailableMsg.NAME =>
|
||||
routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode)
|
||||
case PresAnnStatusMsg.NAME =>
|
||||
routeGenericMsg[PresAnnStatusMsg](envelope, jsonNode)
|
||||
|
||||
// Presentation Pods
|
||||
case CreateNewPresentationPodPubMsg.NAME =>
|
||||
|
@ -507,6 +507,7 @@ class MeetingActor(
|
||||
case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state)
|
||||
case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: NewPresAnnFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
|
||||
case m: PresAnnStatusMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
|
||||
case m: PadCapturePubMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
|
||||
|
||||
// Presentation Pods
|
||||
@ -522,6 +523,7 @@ class MeetingActor(
|
||||
case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationUploadedFileTooLargeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationHasInvalidMimeTypeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationUploadedFileTimeoutErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationPageGeneratedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
|
@ -3,7 +3,7 @@ package org.bigbluebutton.common2.domain
|
||||
case class PresentationVO(id: String, temporaryPresentationId: String, name: String, current: Boolean = false,
|
||||
pages: Vector[PageVO], downloadable: Boolean, removable: Boolean)
|
||||
|
||||
case class PageVO(id: String, num: Int, thumbUri: String = "", swfUri: String,
|
||||
case class PageVO(id: String, num: Int, thumbUri: String = "",
|
||||
txtUri: String, svgUri: String, current: Boolean = false, xOffset: Double = 0,
|
||||
yOffset: Double = 0, widthRatio: Double = 100D, heightRatio: Double = 100D)
|
||||
|
||||
|
@ -18,6 +18,10 @@ object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" }
|
||||
case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg
|
||||
case class NewPresAnnFileAvailableMsgBody(fileURI: String, presId: String)
|
||||
|
||||
object PresAnnStatusMsg { val NAME = "PresAnnStatusMsg" }
|
||||
case class PresAnnStatusMsg(header: BbbClientMsgHeader, body: PresAnnStatusMsgBody) extends StandardMsg
|
||||
case class PresAnnStatusMsgBody(presId: String, pageNumber: Int, totalPages: Int, status: String, error: Boolean);
|
||||
|
||||
// ------------ bbb-common-web to akka-apps ------------
|
||||
|
||||
// ------------ akka-apps to client ------------
|
||||
@ -37,6 +41,10 @@ object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg
|
||||
case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg
|
||||
case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String)
|
||||
|
||||
object PresAnnStatusEvtMsg { val NAME = "PresAnnStatusEvtMsg" }
|
||||
case class PresAnnStatusEvtMsg(header: BbbClientMsgHeader, body: PresAnnStatusEvtMsgBody) extends BbbCoreMsg
|
||||
case class PresAnnStatusEvtMsgBody(presId: String, pageNumber: Int, totalPages: Int, status: String, error: Boolean);
|
||||
|
||||
object CaptureSharedNotesReqEvtMsg { val NAME = "CaptureSharedNotesReqEvtMsg" }
|
||||
case class CaptureSharedNotesReqEvtMsg(header: BbbClientMsgHeader, body: CaptureSharedNotesReqEvtMsgBody) extends BbbCoreMsg
|
||||
case class CaptureSharedNotesReqEvtMsgBody(parentMeetingId: String, meetingName: String)
|
||||
|
@ -166,6 +166,23 @@ case class PresentationUploadedFileTooLargeErrorSysPubMsgBody(
|
||||
maxFileSize: Int
|
||||
)
|
||||
|
||||
object PresentationHasInvalidMimeTypeErrorSysPubMsg { val NAME = "PresentationHasInvalidMimeTypeErrorSysPubMsg" }
|
||||
case class PresentationHasInvalidMimeTypeErrorSysPubMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
body: PresentationHasInvalidMimeTypeErrorSysPubMsgBody
|
||||
) extends StandardMsg
|
||||
case class PresentationHasInvalidMimeTypeErrorSysPubMsgBody(
|
||||
podId: String,
|
||||
meetingId: String,
|
||||
presentationName: String,
|
||||
temporaryPresentationId: String,
|
||||
presentationId: String,
|
||||
messageKey: String,
|
||||
fileMime: String,
|
||||
fileExtension: String,
|
||||
)
|
||||
|
||||
|
||||
object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" }
|
||||
case class PresentationUploadedFileTimeoutErrorSysPubMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
@ -237,6 +254,13 @@ object PresentationUploadedFileTooLargeErrorEvtMsg { val NAME = "PresentationUpl
|
||||
case class PresentationUploadedFileTooLargeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTooLargeErrorEvtMsgBody) extends BbbCoreMsg
|
||||
case class PresentationUploadedFileTooLargeErrorEvtMsgBody(podId: String, messageKey: String, code: String, presentationName: String, presentationToken: String, fileSize: Int, maxFileSize: Int)
|
||||
|
||||
object PresentationHasInvalidMimeTypeErrorEvtMsg { val NAME = "PresentationHasInvalidMimeTypeErrorEvtMsg" }
|
||||
case class PresentationHasInvalidMimeTypeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationHasInvalidMimeTypeErrorEvtMsgBody) extends BbbCoreMsg
|
||||
case class PresentationHasInvalidMimeTypeErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
|
||||
temporaryPresentationId: String, presentationId: String,
|
||||
messageKey: String, fileMime: String, fileExtension: String,
|
||||
)
|
||||
|
||||
object PresentationUploadedFileTimeoutErrorEvtMsg { val NAME = "PresentationUploadedFileTimeoutErrorEvtMsg" }
|
||||
case class PresentationUploadedFileTimeoutErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTimeoutErrorEvtMsgBody) extends BbbCoreMsg
|
||||
case class PresentationUploadedFileTimeoutErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
|
||||
|
@ -19,9 +19,7 @@
|
||||
package org.bigbluebutton.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@ -43,14 +41,12 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.bigbluebutton.api.HTML5LoadBalancingService;
|
||||
import org.bigbluebutton.api.domain.GuestPolicy;
|
||||
import org.bigbluebutton.api.domain.Meeting;
|
||||
import org.bigbluebutton.api.domain.Recording;
|
||||
import org.bigbluebutton.api.domain.RegisteredUser;
|
||||
import org.bigbluebutton.api.domain.User;
|
||||
import org.bigbluebutton.api.domain.UserSession;
|
||||
import org.bigbluebutton.api.domain.MeetingLayout;
|
||||
import org.bigbluebutton.api.messaging.MessageListener;
|
||||
import org.bigbluebutton.api.messaging.converters.messages.DestroyMeetingMessage;
|
||||
import org.bigbluebutton.api.messaging.converters.messages.EndMeetingMessage;
|
||||
@ -60,10 +56,9 @@ import org.bigbluebutton.api.messaging.converters.messages.DeletedRecordingMessa
|
||||
import org.bigbluebutton.api.messaging.messages.*;
|
||||
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||
import org.bigbluebutton.common2.msgs.MeetingCreatedEvtMsg;
|
||||
import org.bigbluebutton.common2.redis.RedisStorageService;
|
||||
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
|
||||
import org.bigbluebutton.presentation.imp.SwfSlidesGenerationProgressNotifier;
|
||||
import org.bigbluebutton.presentation.imp.SlidesGenerationProgressNotifier;
|
||||
import org.bigbluebutton.web.services.WaitingGuestCleanupTimerTask;
|
||||
import org.bigbluebutton.web.services.UserCleanupTimerTask;
|
||||
import org.bigbluebutton.web.services.EnteredUserCleanupTimerTask;
|
||||
@ -77,7 +72,6 @@ import com.google.gson.Gson;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.data.domain.*;
|
||||
|
||||
@ -104,8 +98,7 @@ public class MeetingService implements MessageListener {
|
||||
private StunTurnService stunTurnService;
|
||||
private RedisStorageService storeService;
|
||||
private CallbackUrlService callbackUrlService;
|
||||
private HTML5LoadBalancingService html5LoadBalancingService;
|
||||
private SwfSlidesGenerationProgressNotifier notifier;
|
||||
private SlidesGenerationProgressNotifier notifier;
|
||||
|
||||
private long usersTimeout;
|
||||
private long waitingGuestUsersTimeout;
|
||||
@ -1323,7 +1316,7 @@ public class MeetingService implements MessageListener {
|
||||
enteredUsersTimeout = value;
|
||||
}
|
||||
|
||||
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
|
||||
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.bigbluebutton.api2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bigbluebutton.api.domain.BreakoutRoomsParams;
|
||||
|
@ -94,7 +94,6 @@ public class ConversionUpdateMessage {
|
||||
Map<String, String> page = new HashMap<String, String>();
|
||||
page.put("num", Integer.toString(i));
|
||||
page.put("thumb", basePresUrl + "/thumbnail/" + i);
|
||||
page.put("swf", basePresUrl + "/slide/" + i);
|
||||
page.put("text", basePresUrl + "/textfiles/" + i);
|
||||
|
||||
pages.add(page);
|
||||
|
@ -21,4 +21,5 @@ package org.bigbluebutton.presentation;
|
||||
|
||||
public interface DocumentConversionService {
|
||||
void processDocument(UploadedPresentation pres);
|
||||
void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime, String fileExtension);
|
||||
}
|
||||
|
@ -19,21 +19,25 @@
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||
import org.bigbluebutton.presentation.imp.*;
|
||||
import org.bigbluebutton.presentation.messages.DocConversionRequestReceived;
|
||||
import org.bigbluebutton.presentation.messages.DocInvalidMimeType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
|
||||
|
||||
public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class);
|
||||
|
||||
private IBbbWebApiGWApp gw;
|
||||
private OfficeToPdfConversionService officeToPdfConversionService;
|
||||
private SwfSlidesGenerationProgressNotifier notifier;
|
||||
private SlidesGenerationProgressNotifier notifier;
|
||||
|
||||
private PresentationFileProcessor presentationFileProcessor;
|
||||
|
||||
@ -93,6 +97,9 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
}
|
||||
|
||||
} else {
|
||||
File presentationFile = pres.getUploadedFile();
|
||||
deleteDirectoryFromFileHandlingErrors(presentationFile);
|
||||
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
logData = new HashMap<String, Object>();
|
||||
logData.put("podId", pres.getPodId());
|
||||
@ -124,6 +131,11 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime,
|
||||
String fileExtension) {
|
||||
notifier.sendInvalidMimeTypeMessage(pres, fileMime, fileExtension);
|
||||
}
|
||||
|
||||
private void sendDocConversionRequestReceived(UploadedPresentation pres) {
|
||||
if (! pres.isConversionStarted()) {
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
@ -166,7 +178,7 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
officeToPdfConversionService = s;
|
||||
}
|
||||
|
||||
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
|
||||
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
|
@ -39,5 +39,6 @@ public final class FileTypeConstants {
|
||||
public static final String JPG = "jpg";
|
||||
public static final String JPEG = "jpeg";
|
||||
public static final String PNG = "png";
|
||||
public static final String SVG = "svg";
|
||||
private FileTypeConstants() {} // Prevent instantiation
|
||||
}
|
||||
|
@ -1,94 +0,0 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ImageToSwfSlide {
|
||||
private static Logger log = LoggerFactory.getLogger(ImageToSwfSlide.class);
|
||||
|
||||
private UploadedPresentation pres;
|
||||
private int page;
|
||||
|
||||
private PageConverter imageToSwfConverter;
|
||||
private String BLANK_SLIDE;
|
||||
|
||||
private boolean done = false;
|
||||
private File slide;
|
||||
|
||||
public ImageToSwfSlide(UploadedPresentation pres, int page) {
|
||||
this.pres = pres;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public ImageToSwfSlide createSlide() {
|
||||
File presentationFile = pres.getUploadedFile();
|
||||
slide = new File(presentationFile.getParent() + File.separatorChar + "slide-" + page + ".swf");
|
||||
log.debug("Creating slide {}", slide.getAbsolutePath());
|
||||
imageToSwfConverter.convert(presentationFile, slide, page, pres);
|
||||
|
||||
// If all fails, generate a blank slide.
|
||||
if (!slide.exists()) {
|
||||
log.warn("Creating blank slide for {}", slide.getAbsolutePath());
|
||||
generateBlankSlide();
|
||||
}
|
||||
|
||||
done = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void generateBlankSlide() {
|
||||
if (BLANK_SLIDE != null) {
|
||||
copyBlankSlide(slide);
|
||||
} else {
|
||||
log.error("Blank slide has not been set");
|
||||
}
|
||||
}
|
||||
|
||||
private void copyBlankSlide(File slide) {
|
||||
try {
|
||||
FileUtils.copyFile(new File(BLANK_SLIDE), slide);
|
||||
} catch (IOException e) {
|
||||
log.error("IOException while copying blank slide.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPageConverter(PageConverter converter) {
|
||||
this.imageToSwfConverter = converter;
|
||||
}
|
||||
|
||||
public void setBlankSlide(String blankSlide) {
|
||||
this.BLANK_SLIDE = blankSlide;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public int getPageNumber() {
|
||||
return page;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.bigbluebutton.presentation.FileTypeConstants.*;
|
||||
|
||||
public class MimeTypeUtils {
|
||||
private static final String XLS = "application/vnd.ms-excel";
|
||||
private static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
private static final String DOC = "application/msword";
|
||||
private static final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
private static final String PPT = "application/vnd.ms-powerpoint";
|
||||
private static final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
||||
private static final String ODT = "application/vnd.oasis.opendocument.text";
|
||||
private static final String RTF = "application/rtf";
|
||||
private static final String TXT = "text/plain";
|
||||
private static final String ODS = "application/vnd.oasis.opendocument.spreadsheet";
|
||||
private static final String ODP = "application/vnd.oasis.opendocument.presentation";
|
||||
private static final String PDF = "application/pdf";
|
||||
private static final String JPEG = "image/jpeg";
|
||||
private static final String PNG = "image/png";
|
||||
private static final String SVG = "image/svg+xml";
|
||||
|
||||
private static final HashMap<String,String> EXTENSIONS_MIME = new HashMap<String,String>(16) {
|
||||
{
|
||||
// Add all the supported files
|
||||
put(FileTypeConstants.XLS, XLS);
|
||||
put(FileTypeConstants.XLSX, XLSX);
|
||||
put(FileTypeConstants.DOC, DOC);
|
||||
put(FileTypeConstants.DOCX, DOCX);
|
||||
put(FileTypeConstants.PPT, PPT);
|
||||
put(FileTypeConstants.PPTX, PPTX);
|
||||
put(FileTypeConstants.ODT, ODT);
|
||||
put(FileTypeConstants.RTF, RTF);
|
||||
put(FileTypeConstants.TXT, TXT);
|
||||
put(FileTypeConstants.ODS, ODS);
|
||||
put(FileTypeConstants.ODP, ODP);
|
||||
put(FileTypeConstants.PDF, PDF);
|
||||
put(FileTypeConstants.JPG, JPEG);
|
||||
put(FileTypeConstants.JPEG, JPEG);
|
||||
put(FileTypeConstants.PNG, PNG);
|
||||
put(FileTypeConstants.SVG, SVG);
|
||||
}
|
||||
};
|
||||
|
||||
public Boolean extensionMatchMimeType(String mimeType, String finalExtension) {
|
||||
if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase()) &&
|
||||
EXTENSIONS_MIME.get(finalExtension.toLowerCase()).equalsIgnoreCase(mimeType)) {
|
||||
return true;
|
||||
} else if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase() + 'x') &&
|
||||
EXTENSIONS_MIME.get(finalExtension.toLowerCase() + 'x').equalsIgnoreCase(mimeType)) {
|
||||
//Exception for MS Office files named with old extension but using internally the new mime type
|
||||
//e.g. a file named with extension `ppt` but has the content of a `pptx`
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<String> getValidMimeTypes() {
|
||||
List<String> validMimeTypes = Arrays.asList(XLS, XLSX,
|
||||
DOC, DOCX, PPT, PPTX, ODT, RTF, TXT, ODS, ODP,
|
||||
PDF, JPEG, PNG, SVG
|
||||
);
|
||||
return validMimeTypes;
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
public class PdfToSwfSlide {
|
||||
private static Logger log = LoggerFactory.getLogger(PdfToSwfSlide.class);
|
||||
|
||||
private UploadedPresentation pres;
|
||||
private int page;
|
||||
private PageConverter pdfToSwfConverter;
|
||||
private String BLANK_SLIDE;
|
||||
private int MAX_SWF_FILE_SIZE;
|
||||
|
||||
private volatile boolean done = false;
|
||||
private File slide;
|
||||
private File pageFile;
|
||||
|
||||
public PdfToSwfSlide(UploadedPresentation pres, int page, File pageFile) {
|
||||
this.pres = pres;
|
||||
this.page = page;
|
||||
this.pageFile = pageFile;
|
||||
}
|
||||
|
||||
public PdfToSwfSlide createSlide() {
|
||||
slide = new File(pageFile.getParent() + File.separatorChar + "slide-" + page + ".swf");
|
||||
pdfToSwfConverter.convert(pageFile, slide, page, pres);
|
||||
|
||||
// If all fails, generate a blank slide.
|
||||
if (!slide.exists()) {
|
||||
log.warn("Failed to create slide. Creating blank slide for " + slide.getAbsolutePath());
|
||||
generateBlankSlide();
|
||||
}
|
||||
|
||||
done = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void generateBlankSlide() {
|
||||
if (BLANK_SLIDE != null) {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("page", page);
|
||||
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
|
||||
log.warn("Creating blank slide: data={}", logStr);
|
||||
|
||||
copyBlankSlide(slide);
|
||||
} else {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("page", page);
|
||||
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
|
||||
log.warn("Failed to create blank slide: data={}", logStr);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyBlankSlide(File slide) {
|
||||
try {
|
||||
FileUtils.copyFile(new File(BLANK_SLIDE), slide);
|
||||
} catch (IOException e) {
|
||||
log.error("IOException while copying blank slide.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPageConverter(PageConverter converter) {
|
||||
this.pdfToSwfConverter = converter;
|
||||
}
|
||||
|
||||
public void setBlankSlide(String blankSlide) {
|
||||
this.BLANK_SLIDE = blankSlide;
|
||||
}
|
||||
|
||||
public void setMaxSwfFileSize(int size) {
|
||||
this.MAX_SWF_FILE_SIZE = size;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public int getPageNumber() {
|
||||
return page;
|
||||
}
|
||||
}
|
@ -19,8 +19,15 @@
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.bigbluebutton.presentation.FileTypeConstants.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
@ -28,6 +35,9 @@ import java.util.Collections;
|
||||
@SuppressWarnings("serial")
|
||||
public final class SupportedFileTypes {
|
||||
|
||||
private static Logger log = LoggerFactory.getLogger(SupportedFileTypes.class);
|
||||
private static MimeTypeUtils mimeTypeUtils = new MimeTypeUtils();
|
||||
|
||||
private static final List<String> SUPPORTED_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(15) {
|
||||
{
|
||||
// Add all the supported files
|
||||
@ -76,4 +86,56 @@ public final class SupportedFileTypes {
|
||||
public static boolean isImageFile(String fileExtension) {
|
||||
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase());
|
||||
}
|
||||
|
||||
/*
|
||||
* It was tested native java methods to detect mimetypes, such as:
|
||||
* - URLConnection.guessContentTypeFromStream(InputStream is);
|
||||
* - Files.probeContentType(Path path);
|
||||
* - FileNameMap fileNameMap.getContentTypeFor(String file.getName());
|
||||
* - MimetypesFileTypeMap fileTypeMap.getContentType(File file);
|
||||
* But none of them was as successful as the linux based command
|
||||
*/
|
||||
public static String detectMimeType(File pres) {
|
||||
String mimeType = "";
|
||||
if (pres != null && pres.isFile()){
|
||||
try {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder();
|
||||
processBuilder.command("bash", "-c", "file -b --mime-type " + pres.getAbsolutePath());
|
||||
Process process = processBuilder.start();
|
||||
StringBuilder output = new StringBuilder();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line + "\n");
|
||||
}
|
||||
int exitVal = process.waitFor();
|
||||
if (exitVal == 0) {
|
||||
mimeType = output.toString().trim();
|
||||
} else {
|
||||
log.error("Error while executing command {} for file {}, error: {}",
|
||||
process.toString(), pres.getAbsolutePath(), process.getErrorStream());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Could not read file [{}]", pres.getAbsolutePath(), e.getMessage());
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Flow interrupted for file [{}]", pres.getAbsolutePath(), e.getMessage());
|
||||
}
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) {
|
||||
String mimeType = detectMimeType(pres);
|
||||
|
||||
if(mimeType == null || mimeType == "") return false;
|
||||
|
||||
if(!mimeTypeUtils.getValidMimeTypes().contains(mimeType)) return false;
|
||||
|
||||
if(!mimeTypeUtils.extensionMatchMimeType(mimeType, fileExtension)) {
|
||||
log.error("File with extension [{}] doesn't match with mimeType [{}].", fileExtension, mimeType);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,14 @@
|
||||
|
||||
package org.bigbluebutton.presentation;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class Util {
|
||||
private static Logger log = LoggerFactory.getLogger(Util.class);
|
||||
|
||||
public static void deleteDirectory(File directory) {
|
||||
/**
|
||||
@ -40,4 +45,20 @@ public final class Util {
|
||||
// Now that the directory is empty. Delete it.
|
||||
directory.delete();
|
||||
}
|
||||
|
||||
|
||||
public static void deleteDirectoryFromFileHandlingErrors(File presentationFile) {
|
||||
if ( presentationFile != null ){
|
||||
Path presDir = presentationFile.toPath().getParent();
|
||||
try {
|
||||
File presFileDir = new File(presDir.toString());
|
||||
if (presFileDir.exists()) {
|
||||
deleteDirectory(presFileDir);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Error while trying to delete directory {}", presDir.toString(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,110 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2015 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.presentation.handlers;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* The default command output the anlayse looks like the following: </br>
|
||||
* 20 DEBUG Using</br>
|
||||
* 60 VERBOSE Updating font</br>
|
||||
* 80 VERBOSE Drawing
|
||||
*
|
||||
*/
|
||||
public class Pdf2SwfPageConverterHandler extends AbstractCommandHandler {
|
||||
|
||||
private static Logger log = LoggerFactory
|
||||
.getLogger(Pdf2SwfPageConverterHandler.class);
|
||||
|
||||
private static final String PLACEMENT_OUTPUT = "DEBUG Using";
|
||||
private static final String TEXT_TAG_OUTPUT = "VERBOSE Updating";
|
||||
private static final String IMAGE_TAG_OUTPUT = "VERBOSE Drawing";
|
||||
private static final String DIGITS_AND_WHITESPACES = "\\d+\\s";
|
||||
private static final String PLACEMENT_PATTERN = DIGITS_AND_WHITESPACES + PLACEMENT_OUTPUT;
|
||||
private static final String TEXT_TAG_PATTERN = DIGITS_AND_WHITESPACES + TEXT_TAG_OUTPUT;
|
||||
private static final String IMAGE_TAG_PATTERN = DIGITS_AND_WHITESPACES + IMAGE_TAG_OUTPUT;
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The number of PlaceObject2 tags in the generated SWF
|
||||
*/
|
||||
public long numberOfPlacements() {
|
||||
if (stdoutContains(PLACEMENT_OUTPUT)) {
|
||||
try {
|
||||
String out = stdoutBuilder.toString();
|
||||
Pattern r = Pattern.compile(PLACEMENT_PATTERN);
|
||||
Matcher m = r.matcher(out);
|
||||
m.find();
|
||||
return Integer
|
||||
.parseInt(m.group(0).replace(PLACEMENT_OUTPUT, "").trim());
|
||||
} catch (Exception e) {
|
||||
log.error("Exception counting the number of placements", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The number of text tags in the generated SWF.
|
||||
*/
|
||||
public long numberOfTextTags() {
|
||||
if (stdoutContains(TEXT_TAG_OUTPUT)) {
|
||||
try {
|
||||
String out = stdoutBuilder.toString();
|
||||
Pattern r = Pattern.compile(TEXT_TAG_PATTERN);
|
||||
Matcher m = r.matcher(out);
|
||||
m.find();
|
||||
return Integer.parseInt(m.group(0).replace(TEXT_TAG_OUTPUT, "").trim());
|
||||
} catch (Exception e) {
|
||||
log.error("Exception counting the number of text tags", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The number of image tags in the generated SWF.
|
||||
*/
|
||||
public long numberOfImageTags() {
|
||||
if (stdoutContains(IMAGE_TAG_OUTPUT)) {
|
||||
try {
|
||||
String out = stdoutBuilder.toString();
|
||||
Pattern r = Pattern.compile(IMAGE_TAG_PATTERN);
|
||||
Matcher m = r.matcher(out);
|
||||
m.find();
|
||||
return Integer
|
||||
.parseInt(m.group(0).replace(IMAGE_TAG_OUTPUT, "").trim());
|
||||
} catch (Exception e) {
|
||||
log.error("Exception counting the number of iamge tags", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2015 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.presentation.handlers;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Png2SwfPageConverterHandler extends AbstractCommandHandler {
|
||||
private static Logger log = LoggerFactory.getLogger(Png2SwfPageConverterHandler.class);
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation.imp;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.bigbluebutton.presentation.ImageResizer;
|
||||
import org.bigbluebutton.presentation.PngCreator;
|
||||
import org.bigbluebutton.presentation.SvgImageCreator;
|
||||
import org.bigbluebutton.presentation.TextFileCreator;
|
||||
import org.bigbluebutton.presentation.ThumbnailCreator;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
public class ImageSlidesGenerationService {
|
||||
private static Logger log = LoggerFactory.getLogger(ImageSlidesGenerationService.class);
|
||||
|
||||
private ExecutorService executor;
|
||||
private SlidesGenerationProgressNotifier notifier;
|
||||
private SvgImageCreator svgImageCreator;
|
||||
private ThumbnailCreator thumbnailCreator;
|
||||
private TextFileCreator textFileCreator;
|
||||
private PngCreator pngCreator;
|
||||
private ImageResizer imageResizer;
|
||||
private long maxImageWidth = 2048;
|
||||
private long maxImageHeight = 1536;
|
||||
private long MAX_CONVERSION_TIME = 5*60*1000L;
|
||||
private boolean svgImagesRequired=true;
|
||||
private boolean generatePngs;
|
||||
|
||||
public ImageSlidesGenerationService() {
|
||||
int numThreads = Runtime.getRuntime().availableProcessors();
|
||||
executor = Executors.newFixedThreadPool(numThreads);
|
||||
}
|
||||
|
||||
public void generateSlides(UploadedPresentation pres) {
|
||||
|
||||
for (int page = 1; page <= pres.getNumberOfPages(); page++) {
|
||||
/* adding accessibility */
|
||||
createTextFiles(pres, page);
|
||||
createThumbnails(pres, page);
|
||||
|
||||
if (svgImagesRequired) {
|
||||
try {
|
||||
createSvgImages(pres, page);
|
||||
} catch (TimeoutException e) {
|
||||
log.error("Slide {} was not converted due to TimeoutException, ending process.", page, e);
|
||||
notifier.sendUploadFileTimedout(pres, page);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (generatePngs) {
|
||||
createPngImages(pres, page);
|
||||
}
|
||||
|
||||
notifier.sendConversionUpdateMessage(page, pres, page);
|
||||
}
|
||||
|
||||
System.out.println("****** Conversion complete for " + pres.getName());
|
||||
notifier.sendConversionCompletedMessage(pres);
|
||||
|
||||
}
|
||||
|
||||
private void createTextFiles(UploadedPresentation pres, int page) {
|
||||
log.debug("Creating textfiles for accessibility.");
|
||||
notifier.sendCreatingTextFilesUpdateMessage(pres);
|
||||
textFileCreator.createTextFile(pres, page);
|
||||
}
|
||||
|
||||
private void createThumbnails(UploadedPresentation pres, int page) {
|
||||
log.debug("Creating thumbnails.");
|
||||
notifier.sendCreatingThumbnailsUpdateMessage(pres);
|
||||
thumbnailCreator.createThumbnail(pres, page, pres.getUploadedFile());
|
||||
}
|
||||
|
||||
private void createSvgImages(UploadedPresentation pres, int page) throws TimeoutException{
|
||||
log.debug("Creating SVG images.");
|
||||
|
||||
try {
|
||||
BufferedImage bimg = ImageIO.read(pres.getUploadedFile());
|
||||
if(bimg.getWidth() > maxImageWidth || bimg.getHeight() > maxImageHeight) {
|
||||
log.info("The image exceeds max dimension allowed, it will be resized.");
|
||||
resizeImage(pres, maxImageWidth + "x" + maxImageHeight);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Exception while resizing image {}", pres.getName(), e);
|
||||
}
|
||||
|
||||
notifier.sendCreatingSvgImagesUpdateMessage(pres);
|
||||
svgImageCreator.createSvgImage(pres, page);
|
||||
}
|
||||
|
||||
private void createPngImages(UploadedPresentation pres, int page) {
|
||||
pngCreator.createPng(pres, page, pres.getUploadedFile());
|
||||
}
|
||||
|
||||
private void resizeImage(UploadedPresentation pres, String ratio) {
|
||||
imageResizer.resize(pres, ratio);
|
||||
}
|
||||
|
||||
public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) {
|
||||
this.thumbnailCreator = thumbnailCreator;
|
||||
}
|
||||
|
||||
public void setTextFileCreator(TextFileCreator textFileCreator) {
|
||||
this.textFileCreator = textFileCreator;
|
||||
}
|
||||
|
||||
public void setPngCreator(PngCreator pngCreator) {
|
||||
this.pngCreator = pngCreator;
|
||||
}
|
||||
|
||||
public void setSvgImageCreator(SvgImageCreator svgImageCreator) {
|
||||
this.svgImageCreator = svgImageCreator;
|
||||
}
|
||||
|
||||
public void setGeneratePngs(boolean generatePngs) {
|
||||
this.generatePngs = generatePngs;
|
||||
}
|
||||
|
||||
public void setSvgImagesRequired(boolean svg) {
|
||||
this.svgImagesRequired = svg;
|
||||
}
|
||||
|
||||
public void setMaxConversionTime(int minutes) {
|
||||
MAX_CONVERSION_TIME = minutes * 60 * 1000L;
|
||||
}
|
||||
|
||||
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
public void setImageResizer(ImageResizer imageResizer) {
|
||||
this.imageResizer = imageResizer;
|
||||
}
|
||||
|
||||
public void setMaxImageWidth(long maxImageWidth) {
|
||||
this.maxImageWidth = maxImageWidth;
|
||||
}
|
||||
public void setMaxImageHeight(long maxImageHeight) {
|
||||
this.maxImageHeight = maxImageHeight;
|
||||
}
|
||||
}
|
@ -1,264 +0,0 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation.imp;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletionService;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.bigbluebutton.presentation.FileTypeConstants;
|
||||
import org.bigbluebutton.presentation.ImageResizer;
|
||||
import org.bigbluebutton.presentation.ImageToSwfSlide;
|
||||
import org.bigbluebutton.presentation.PageConverter;
|
||||
import org.bigbluebutton.presentation.PngCreator;
|
||||
import org.bigbluebutton.presentation.SvgImageCreator;
|
||||
import org.bigbluebutton.presentation.TextFileCreator;
|
||||
import org.bigbluebutton.presentation.ThumbnailCreator;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ImageToSwfSlidesGenerationService {
|
||||
private static Logger log = LoggerFactory.getLogger(ImageToSwfSlidesGenerationService.class);
|
||||
|
||||
private ExecutorService executor;
|
||||
private CompletionService<ImageToSwfSlide> completionService;
|
||||
private SwfSlidesGenerationProgressNotifier notifier;
|
||||
private PageConverter jpgToSwfConverter;
|
||||
private PageConverter pngToSwfConverter;
|
||||
private SvgImageCreator svgImageCreator;
|
||||
private ThumbnailCreator thumbnailCreator;
|
||||
private TextFileCreator textFileCreator;
|
||||
private PngCreator pngCreator;
|
||||
private ImageResizer imageResizer;
|
||||
private Long maxImageSize;
|
||||
private long MAX_CONVERSION_TIME = 5*60*1000L;
|
||||
private String BLANK_SLIDE;
|
||||
private boolean swfSlidesRequired;
|
||||
private boolean svgImagesRequired;
|
||||
private boolean generatePngs;
|
||||
|
||||
public ImageToSwfSlidesGenerationService() {
|
||||
int numThreads = Runtime.getRuntime().availableProcessors();
|
||||
executor = Executors.newFixedThreadPool(numThreads);
|
||||
completionService = new ExecutorCompletionService<ImageToSwfSlide>(executor);
|
||||
}
|
||||
|
||||
public void generateSlides(UploadedPresentation pres) {
|
||||
|
||||
for (int page = 1; page <= pres.getNumberOfPages(); page++) {
|
||||
if (swfSlidesRequired) {
|
||||
if (pres.getNumberOfPages() > 0) {
|
||||
PageConverter pageConverter = determinePageConverter(pres);
|
||||
convertImageToSwf(pres, pageConverter);
|
||||
}
|
||||
}
|
||||
|
||||
/* adding accessibility */
|
||||
createTextFiles(pres, page);
|
||||
createThumbnails(pres, page);
|
||||
|
||||
if (svgImagesRequired) {
|
||||
try {
|
||||
createSvgImages(pres, page);
|
||||
} catch (TimeoutException e) {
|
||||
log.error("Slide {} was not converted due to TimeoutException, ending process.", page, e);
|
||||
notifier.sendUploadFileTimedout(pres, page);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (generatePngs) {
|
||||
createPngImages(pres, page);
|
||||
}
|
||||
|
||||
notifier.sendConversionUpdateMessage(page, pres, page);
|
||||
}
|
||||
|
||||
System.out.println("****** Conversion complete for " + pres.getName());
|
||||
notifier.sendConversionCompletedMessage(pres);
|
||||
|
||||
}
|
||||
|
||||
private PageConverter determinePageConverter(UploadedPresentation pres) {
|
||||
String fileType = pres.getFileType().toUpperCase();
|
||||
if ((FileTypeConstants.JPEG.equalsIgnoreCase(fileType)) || (FileTypeConstants.JPG.equalsIgnoreCase(fileType))) {
|
||||
return jpgToSwfConverter;
|
||||
}
|
||||
|
||||
return pngToSwfConverter;
|
||||
}
|
||||
|
||||
private void createTextFiles(UploadedPresentation pres, int page) {
|
||||
log.debug("Creating textfiles for accessibility.");
|
||||
notifier.sendCreatingTextFilesUpdateMessage(pres);
|
||||
textFileCreator.createTextFile(pres, page);
|
||||
}
|
||||
|
||||
private void createThumbnails(UploadedPresentation pres, int page) {
|
||||
log.debug("Creating thumbnails.");
|
||||
notifier.sendCreatingThumbnailsUpdateMessage(pres);
|
||||
thumbnailCreator.createThumbnail(pres, page, pres.getUploadedFile());
|
||||
}
|
||||
|
||||
private void createSvgImages(UploadedPresentation pres, int page) throws TimeoutException{
|
||||
log.debug("Creating SVG images.");
|
||||
notifier.sendCreatingSvgImagesUpdateMessage(pres);
|
||||
svgImageCreator.createSvgImage(pres, page);
|
||||
}
|
||||
|
||||
private void createPngImages(UploadedPresentation pres, int page) {
|
||||
pngCreator.createPng(pres, page, pres.getUploadedFile());
|
||||
}
|
||||
|
||||
private void convertImageToSwf(UploadedPresentation pres, PageConverter pageConverter) {
|
||||
int numPages = pres.getNumberOfPages();
|
||||
// A better implementation is described at the link below
|
||||
// https://stackoverflow.com/questions/4513648/how-to-estimate-the-size-of-jpeg-image-which-will-be-scaled-down
|
||||
if (pres.getUploadedFile().length() > maxImageSize) {
|
||||
DecimalFormat percentFormat= new DecimalFormat("#.##%");
|
||||
// Resize the image and overwrite it
|
||||
resizeImage(pres, percentFormat
|
||||
.format(Double.valueOf(maxImageSize) / Double.valueOf(pres.getUploadedFile().length())));
|
||||
}
|
||||
ImageToSwfSlide[] slides = setupSlides(pres, numPages, pageConverter);
|
||||
generateSlides(slides);
|
||||
handleSlideGenerationResult(pres, slides);
|
||||
}
|
||||
|
||||
private void resizeImage(UploadedPresentation pres, String ratio) {
|
||||
imageResizer.resize(pres, ratio);
|
||||
}
|
||||
|
||||
private void handleSlideGenerationResult(UploadedPresentation pres, ImageToSwfSlide[] slides) {
|
||||
long endTime = System.currentTimeMillis() + MAX_CONVERSION_TIME;
|
||||
|
||||
for (int t = 0; t < slides.length; t++) {
|
||||
Future<ImageToSwfSlide> future = null;
|
||||
ImageToSwfSlide slide = null;
|
||||
try {
|
||||
long timeLeft = endTime - System.currentTimeMillis();
|
||||
future = completionService.take();
|
||||
slide = future.get(timeLeft, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("InterruptedException while creating slide {}", pres.getName(), e);
|
||||
} catch (ExecutionException e) {
|
||||
log.error("ExecutionException while creating slide {}", pres.getName(), e);
|
||||
} catch (TimeoutException e) {
|
||||
log.error("TimeoutException while converting {}", pres.getName(), e);
|
||||
} finally {
|
||||
if ((slide != null) && (! slide.isDone())){
|
||||
log.warn("Creating blank slide for {}", slide.getPageNumber());
|
||||
future.cancel(true);
|
||||
slide.generateBlankSlide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ImageToSwfSlide[] setupSlides(UploadedPresentation pres, int numPages, PageConverter pageConverter) {
|
||||
ImageToSwfSlide[] slides = new ImageToSwfSlide[numPages];
|
||||
|
||||
for (int page = 1; page <= numPages; page++) {
|
||||
ImageToSwfSlide slide = new ImageToSwfSlide(pres, page);
|
||||
slide.setBlankSlide(BLANK_SLIDE);
|
||||
slide.setPageConverter(pageConverter);
|
||||
|
||||
// Array index is zero-based
|
||||
slides[page-1] = slide;
|
||||
}
|
||||
|
||||
return slides;
|
||||
}
|
||||
|
||||
private void generateSlides(ImageToSwfSlide[] slides) {
|
||||
for (int i = 0; i < slides.length; i++) {
|
||||
final ImageToSwfSlide slide = slides[i];
|
||||
completionService.submit(new Callable<ImageToSwfSlide>() {
|
||||
public ImageToSwfSlide call() {
|
||||
return slide.createSlide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void setJpgPageConverter(PageConverter converter) {
|
||||
this.jpgToSwfConverter = converter;
|
||||
}
|
||||
|
||||
public void setPngPageConverter(PageConverter converter) {
|
||||
this.pngToSwfConverter = converter;
|
||||
}
|
||||
|
||||
public void setBlankSlide(String blankSlide) {
|
||||
this.BLANK_SLIDE = blankSlide;
|
||||
}
|
||||
|
||||
public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) {
|
||||
this.thumbnailCreator = thumbnailCreator;
|
||||
}
|
||||
|
||||
public void setTextFileCreator(TextFileCreator textFileCreator) {
|
||||
this.textFileCreator = textFileCreator;
|
||||
}
|
||||
|
||||
public void setPngCreator(PngCreator pngCreator) {
|
||||
this.pngCreator = pngCreator;
|
||||
}
|
||||
|
||||
public void setSvgImageCreator(SvgImageCreator svgImageCreator) {
|
||||
this.svgImageCreator = svgImageCreator;
|
||||
}
|
||||
|
||||
public void setGeneratePngs(boolean generatePngs) {
|
||||
this.generatePngs = generatePngs;
|
||||
}
|
||||
|
||||
public void setSwfSlidesRequired(boolean swf) {
|
||||
this.swfSlidesRequired = swf;
|
||||
}
|
||||
|
||||
public void setSvgImagesRequired(boolean svg) {
|
||||
this.svgImagesRequired = svg;
|
||||
}
|
||||
|
||||
public void setMaxConversionTime(int minutes) {
|
||||
MAX_CONVERSION_TIME = minutes * 60 * 1000L;
|
||||
}
|
||||
|
||||
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
public void setImageResizer(ImageResizer imageResizer) {
|
||||
this.imageResizer = imageResizer;
|
||||
}
|
||||
|
||||
public void setMaxImageSize(Long maxImageSize) {
|
||||
this.maxImageSize = maxImageSize;
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation.imp;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bigbluebutton.presentation.PageConverter;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
public class Jpeg2SwfPageConverter implements PageConverter {
|
||||
private static Logger log = LoggerFactory.getLogger(Jpeg2SwfPageConverter.class);
|
||||
|
||||
private String SWFTOOLS_DIR;
|
||||
|
||||
public boolean convert(File presentationFile, File output, int page, UploadedPresentation pres){
|
||||
|
||||
String COMMAND = SWFTOOLS_DIR + File.separatorChar + "jpeg2swf -o " + output.getAbsolutePath() + " " + presentationFile.getAbsolutePath();
|
||||
|
||||
boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000);
|
||||
|
||||
if (done && output.exists()) {
|
||||
return true;
|
||||
} else {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "jpg_to_swf_conversion_failed");
|
||||
logData.put("message", "Failed to convert: " + output.getAbsolutePath() + " does not exist.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setSwfToolsDir(String dir) {
|
||||
SWFTOOLS_DIR = dir;
|
||||
}
|
||||
|
||||
}
|
@ -35,6 +35,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
|
||||
|
||||
public abstract class Office2PdfPageConverter {
|
||||
private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class);
|
||||
|
||||
@ -95,6 +97,7 @@ public abstract class Office2PdfPageConverter {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
deleteDirectoryFromFileHandlingErrors(presentationFile);
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
|
@ -10,51 +10,39 @@ public class PageToConvert {
|
||||
private UploadedPresentation pres;
|
||||
private int page;
|
||||
|
||||
private boolean swfSlidesRequired;
|
||||
private boolean svgImagesRequired;
|
||||
private boolean svgImagesRequired=true;
|
||||
private boolean generatePngs;
|
||||
private PageExtractor pageExtractor;
|
||||
|
||||
private String BLANK_SLIDE;
|
||||
private int MAX_SWF_FILE_SIZE;
|
||||
|
||||
private TextFileCreator textFileCreator;
|
||||
private SvgImageCreator svgImageCreator;
|
||||
private ThumbnailCreator thumbnailCreator;
|
||||
private PngCreator pngCreator;
|
||||
private PageConverter pdfToSwfConverter;
|
||||
private SwfSlidesGenerationProgressNotifier notifier;
|
||||
private SlidesGenerationProgressNotifier notifier;
|
||||
private File pageFile;
|
||||
private String messageErrorInConversion;
|
||||
|
||||
public PageToConvert(UploadedPresentation pres,
|
||||
int page,
|
||||
File pageFile,
|
||||
boolean swfSlidesRequired,
|
||||
boolean svgImagesRequired,
|
||||
boolean generatePngs,
|
||||
TextFileCreator textFileCreator,
|
||||
SvgImageCreator svgImageCreator,
|
||||
ThumbnailCreator thumbnailCreator,
|
||||
PngCreator pngCreator,
|
||||
PageConverter pdfToSwfConverter,
|
||||
SwfSlidesGenerationProgressNotifier notifier,
|
||||
String blankSlide,
|
||||
int maxSwfFileSize) {
|
||||
SlidesGenerationProgressNotifier notifier) {
|
||||
this.pres = pres;
|
||||
this.page = page;
|
||||
this.pageFile = pageFile;
|
||||
this.swfSlidesRequired = swfSlidesRequired;
|
||||
this.svgImagesRequired = svgImagesRequired;
|
||||
this.generatePngs = generatePngs;
|
||||
this.textFileCreator = textFileCreator;
|
||||
this.svgImageCreator = svgImageCreator;
|
||||
this.thumbnailCreator = thumbnailCreator;
|
||||
this.pngCreator = pngCreator;
|
||||
this.pdfToSwfConverter = pdfToSwfConverter;
|
||||
this.notifier = notifier;
|
||||
this.BLANK_SLIDE = blankSlide;
|
||||
this.MAX_SWF_FILE_SIZE = maxSwfFileSize;
|
||||
}
|
||||
|
||||
public File getPageFile() {
|
||||
@ -83,11 +71,6 @@ public class PageToConvert {
|
||||
|
||||
public PageToConvert convert() {
|
||||
|
||||
// Only create SWF files if the configuration requires it
|
||||
if (swfSlidesRequired) {
|
||||
convertPdfToSwf(pres, page, pageFile);
|
||||
}
|
||||
|
||||
/* adding accessibility */
|
||||
createThumbnails(pres, page, pageFile);
|
||||
|
||||
@ -129,25 +112,4 @@ public class PageToConvert {
|
||||
pngCreator.createPng(pres, page, pageFile);
|
||||
}
|
||||
|
||||
private void convertPdfToSwf(UploadedPresentation pres, int page, File pageFile) {
|
||||
PdfToSwfSlide slide = setupSlide(pres, page, pageFile);
|
||||
generateSlides(pres, slide);
|
||||
}
|
||||
|
||||
|
||||
private void generateSlides(UploadedPresentation pres, PdfToSwfSlide slide) {
|
||||
slide.createSlide();
|
||||
if (!slide.isDone()) {
|
||||
slide.generateBlankSlide();
|
||||
}
|
||||
}
|
||||
|
||||
private PdfToSwfSlide setupSlide(UploadedPresentation pres, int page, File pageFile) {
|
||||
PdfToSwfSlide slide = new PdfToSwfSlide(pres, page, pageFile);
|
||||
slide.setBlankSlide(BLANK_SLIDE);
|
||||
slide.setMaxSwfFileSize(MAX_SWF_FILE_SIZE);
|
||||
slide.setPageConverter(pdfToSwfConverter);
|
||||
|
||||
return slide;
|
||||
}
|
||||
}
|
||||
|
@ -1,239 +0,0 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation.imp;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.bigbluebutton.presentation.PageConverter;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.bigbluebutton.presentation.handlers.Pdf2PngPageConverterHandler;
|
||||
import org.bigbluebutton.presentation.handlers.Pdf2SwfPageConverterHandler;
|
||||
import org.bigbluebutton.presentation.handlers.Png2SwfPageConverterHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.zaxxer.nuprocess.NuProcess;
|
||||
import com.zaxxer.nuprocess.NuProcessBuilder;
|
||||
|
||||
public class Pdf2SwfPageConverter implements PageConverter {
|
||||
private static Logger log = LoggerFactory.getLogger(Pdf2SwfPageConverter.class);
|
||||
|
||||
private String SWFTOOLS_DIR;
|
||||
private String fontsDir;
|
||||
private long placementsThreshold;
|
||||
private long defineTextThreshold;
|
||||
private long imageTagThreshold;
|
||||
private String convTimeout = "7s";
|
||||
private int WAIT_FOR_SEC = 7;
|
||||
|
||||
public boolean convert(File presentation, File output, int page, UploadedPresentation pres) {
|
||||
long convertStart = System.currentTimeMillis();
|
||||
|
||||
String source = presentation.getAbsolutePath();
|
||||
String dest = output.getAbsolutePath();
|
||||
String AVM2SWF = "-T9";
|
||||
|
||||
// Building the command line wrapped in shell to be able to use shell
|
||||
// feature like the pipe
|
||||
NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("timeout",
|
||||
convTimeout, "/bin/sh", "-c",
|
||||
SWFTOOLS_DIR + File.separatorChar + "pdf2swf" + " -vv " + AVM2SWF + " -F "
|
||||
+ fontsDir + " " + source + " -o "
|
||||
+ dest
|
||||
+ " | egrep 'shape id|Updating font|Drawing' | sed 's/ / /g' | cut -d' ' -f 1-3 | sort | uniq -cw 15"));
|
||||
|
||||
Pdf2SwfPageConverterHandler pHandler = new Pdf2SwfPageConverterHandler();
|
||||
pb.setProcessListener(pHandler);
|
||||
|
||||
long pdf2SwfStart = System.currentTimeMillis();
|
||||
|
||||
NuProcess process = pb.start();
|
||||
try {
|
||||
process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("InterruptedException while creating SWF {}", pres.getName(), e);
|
||||
}
|
||||
|
||||
long pdf2SwfEnd = System.currentTimeMillis();
|
||||
log.debug("Pdf2Swf conversion duration: {} sec", (pdf2SwfEnd - pdf2SwfStart) / 1000);
|
||||
|
||||
boolean timedOut = pdf2SwfEnd
|
||||
- pdf2SwfStart >= Integer.parseInt(convTimeout.replaceFirst("s", ""))
|
||||
* 1000;
|
||||
boolean twiceTotalObjects = pHandler.numberOfPlacements()
|
||||
+ pHandler.numberOfTextTags()
|
||||
+ pHandler.numberOfImageTags() >= (placementsThreshold
|
||||
+ defineTextThreshold + imageTagThreshold) * 2;
|
||||
|
||||
File destFile = new File(dest);
|
||||
|
||||
if (pHandler.isCommandSuccessful() && destFile.exists()
|
||||
&& pHandler.numberOfPlacements() < placementsThreshold
|
||||
&& pHandler.numberOfTextTags() < defineTextThreshold
|
||||
&& pHandler.numberOfImageTags() < imageTagThreshold) {
|
||||
return true;
|
||||
} else {
|
||||
// We need t delete the destination file as we are starting a new
|
||||
// conversion process
|
||||
if (destFile.exists()) {
|
||||
destFile.delete();
|
||||
}
|
||||
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("page", page);
|
||||
logData.put("convertSuccess", pHandler.isCommandSuccessful());
|
||||
logData.put("fileExists", destFile.exists());
|
||||
logData.put("numObjectTags", pHandler.numberOfPlacements());
|
||||
logData.put("numTextTags", pHandler.numberOfTextTags());
|
||||
logData.put("numImageTags", pHandler.numberOfImageTags());
|
||||
logData.put("logCode", "problem_with_generated_swf");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
|
||||
File tempPng = null;
|
||||
String basePresentationame = UUID.randomUUID().toString();
|
||||
try {
|
||||
tempPng = File.createTempFile(basePresentationame + "-" + page, ".png");
|
||||
} catch (IOException ioException) {
|
||||
// We should never fall into this if the server is correctly configured
|
||||
logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "failed_to_create_temp_file");
|
||||
logData.put("message", "Unable to create temporary files for pdf to swf.");
|
||||
gson = new Gson();
|
||||
logStr = gson.toJson(logData);
|
||||
log.error(" --analytics-- data={}", logStr, ioException);
|
||||
}
|
||||
|
||||
// long pdfStart = System.currentTimeMillis();
|
||||
|
||||
// Step 1: Convert a PDF page to PNG using a raw pdftocairo
|
||||
NuProcessBuilder pbPng = new NuProcessBuilder(
|
||||
Arrays.asList("timeout", convTimeout, "pdftocairo", "-png",
|
||||
"-singlefile", "-r", timedOut || twiceTotalObjects ? "72" : "150",
|
||||
presentation.getAbsolutePath(), tempPng.getAbsolutePath()
|
||||
.substring(0, tempPng.getAbsolutePath().lastIndexOf('.'))));
|
||||
|
||||
Pdf2PngPageConverterHandler pbPngHandler = new Pdf2PngPageConverterHandler();
|
||||
pbPng.setProcessListener(pbPngHandler);
|
||||
NuProcess processPng = pbPng.start();
|
||||
try {
|
||||
processPng.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("InterruptedException while creating temporary PNG {}", pres.getName(), e);
|
||||
}
|
||||
|
||||
//long pdfEnd = System.currentTimeMillis();
|
||||
//log.debug("pdftocairo conversion duration: {} sec", (pdfEnd - pdfStart) / 1000);
|
||||
|
||||
// long png2swfStart = System.currentTimeMillis();
|
||||
|
||||
// Step 2: Convert a PNG image to SWF
|
||||
// We need to update the file path as pdftocairo adds "-page.png"
|
||||
source = tempPng.getAbsolutePath();
|
||||
NuProcessBuilder pbSwf = new NuProcessBuilder(
|
||||
Arrays.asList("timeout", convTimeout,
|
||||
SWFTOOLS_DIR + File.separatorChar + "png2swf", "-o", dest, source));
|
||||
Png2SwfPageConverterHandler pSwfHandler = new Png2SwfPageConverterHandler();
|
||||
pbSwf.setProcessListener(pSwfHandler);
|
||||
NuProcess processSwf = pbSwf.start();
|
||||
try {
|
||||
processSwf.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("InterruptedException while creating SWF {}", pres.getName(), e);
|
||||
}
|
||||
|
||||
|
||||
//long png2swfEnd = System.currentTimeMillis();
|
||||
//log.debug("SwfTools conversion duration: {} sec", (png2swfEnd - png2swfStart) / 1000);
|
||||
|
||||
// Delete the temporary PNG and PDF files after finishing the image
|
||||
// conversion
|
||||
tempPng.delete();
|
||||
|
||||
boolean doneSwf = pSwfHandler.isCommandSuccessful();
|
||||
|
||||
long convertEnd = System.currentTimeMillis();
|
||||
|
||||
logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("page", page);
|
||||
logData.put("conversionTime(sec)", (convertEnd - convertStart) / 1000);
|
||||
logData.put("logCode", "conversion_took_too_long");
|
||||
logData.put("message", "PDF to SWF conversion took a long time.");
|
||||
logStr = gson.toJson(logData);
|
||||
log.info(" --analytics-- data={}", logStr);
|
||||
|
||||
if (doneSwf && destFile.exists()) {
|
||||
return true;
|
||||
} else {
|
||||
logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("page", page);
|
||||
logData.put("conversionTime(sec)", (convertEnd - convertStart) / 1000);
|
||||
logData.put("logCode", "pdf2swf_conversion_failed");
|
||||
logData.put("message", "Failed to convert: " + destFile + " does not exist.");
|
||||
logStr = gson.toJson(logData);
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSwfToolsDir(String dir) {
|
||||
SWFTOOLS_DIR = dir;
|
||||
}
|
||||
|
||||
public void setFontsDir(String dir) {
|
||||
fontsDir = dir;
|
||||
}
|
||||
|
||||
public void setPlacementsThreshold(long threshold) {
|
||||
placementsThreshold = threshold;
|
||||
}
|
||||
|
||||
public void setDefineTextThreshold(long threshold) {
|
||||
defineTextThreshold = threshold;
|
||||
}
|
||||
|
||||
public void setImageTagThreshold(long threshold) {
|
||||
imageTagThreshold = threshold;
|
||||
}
|
||||
}
|
@ -26,8 +26,8 @@ import org.bigbluebutton.presentation.messages.PageConvertProgressMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PdfToSwfSlidesGenerationService {
|
||||
private static Logger log = LoggerFactory.getLogger(PdfToSwfSlidesGenerationService.class);
|
||||
public class PdfSlidesGenerationService {
|
||||
private static Logger log = LoggerFactory.getLogger(PdfSlidesGenerationService.class);
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
@ -35,7 +35,7 @@ public class PdfToSwfSlidesGenerationService {
|
||||
|
||||
private PresentationConversionCompletionService presentationConversionCompletionService;
|
||||
|
||||
public PdfToSwfSlidesGenerationService(int numConversionThreads) {
|
||||
public PdfSlidesGenerationService(int numConversionThreads) {
|
||||
executor = Executors.newFixedThreadPool(numConversionThreads);
|
||||
}
|
||||
|
@ -1,64 +0,0 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation.imp;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bigbluebutton.presentation.PageConverter;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
public class Png2SwfPageConverter implements PageConverter {
|
||||
private static Logger log = LoggerFactory.getLogger(Png2SwfPageConverter.class);
|
||||
|
||||
private String SWFTOOLS_DIR;
|
||||
|
||||
public boolean convert(File presentationFile, File output, int page, UploadedPresentation pres){
|
||||
String COMMAND = SWFTOOLS_DIR + File.separatorChar + "png2swf -o " + output.getAbsolutePath() + " " + presentationFile.getAbsolutePath();
|
||||
|
||||
boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000);
|
||||
|
||||
if (done && output.exists()) {
|
||||
return true;
|
||||
} else {
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "png_to_swf_failed");
|
||||
logData.put("message", "Failed to convert PNG doc to SWF.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSwfToolsDir(String dir) {
|
||||
SWFTOOLS_DIR = dir;
|
||||
}
|
||||
|
||||
}
|
@ -14,7 +14,7 @@ import java.util.concurrent.*;
|
||||
public class PresentationConversionCompletionService {
|
||||
private static Logger log = LoggerFactory.getLogger(PresentationConversionCompletionService.class);
|
||||
|
||||
private SwfSlidesGenerationProgressNotifier notifier;
|
||||
private SlidesGenerationProgressNotifier notifier;
|
||||
|
||||
private ExecutorService executor;
|
||||
private volatile boolean processProgress = false;
|
||||
@ -105,7 +105,7 @@ public class PresentationConversionCompletionService {
|
||||
processProgress = false;
|
||||
}
|
||||
|
||||
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
|
||||
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +24,10 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
public class PresentationFileProcessor {
|
||||
private static Logger log = LoggerFactory.getLogger(PresentationFileProcessor.class);
|
||||
|
||||
private boolean swfSlidesRequired;
|
||||
private boolean svgImagesRequired;
|
||||
private boolean svgImagesRequired=true;
|
||||
private boolean generatePngs;
|
||||
private PageExtractor pageExtractor;
|
||||
|
||||
private String BLANK_SLIDE;
|
||||
private int MAX_SWF_FILE_SIZE;
|
||||
private long bigPdfSize;
|
||||
private long maxBigPdfPageSize;
|
||||
|
||||
@ -40,12 +37,11 @@ public class PresentationFileProcessor {
|
||||
private SvgImageCreator svgImageCreator;
|
||||
private ThumbnailCreator thumbnailCreator;
|
||||
private PngCreator pngCreator;
|
||||
private PageConverter pdfToSwfConverter;
|
||||
private SwfSlidesGenerationProgressNotifier notifier;
|
||||
private SlidesGenerationProgressNotifier notifier;
|
||||
private PageCounterService counterService;
|
||||
private PresentationConversionCompletionService presentationConversionCompletionService;
|
||||
private ImageToSwfSlidesGenerationService imageToSwfSlidesGenerationService;
|
||||
private PdfToSwfSlidesGenerationService pdfToSwfSlidesGenerationService;
|
||||
private ImageSlidesGenerationService imageSlidesGenerationService;
|
||||
private PdfSlidesGenerationService pdfSlidesGenerationService;
|
||||
|
||||
private ExecutorService executor;
|
||||
private volatile boolean processPresentation = false;
|
||||
@ -88,7 +84,7 @@ public class PresentationFileProcessor {
|
||||
} else if (SupportedFileTypes.isImageFile(pres.getFileType())) {
|
||||
pres.setNumberOfPages(1); // There should be only one image to convert.
|
||||
sendDocPageConversionStartedProgress(pres);
|
||||
imageToSwfSlidesGenerationService.generateSlides(pres);
|
||||
imageSlidesGenerationService.generateSlides(pres);
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,20 +108,16 @@ public class PresentationFileProcessor {
|
||||
pres,
|
||||
page,
|
||||
pageFile,
|
||||
swfSlidesRequired,
|
||||
svgImagesRequired,
|
||||
generatePngs,
|
||||
textFileCreator,
|
||||
svgImageCreator,
|
||||
thumbnailCreator,
|
||||
pngCreator,
|
||||
pdfToSwfConverter,
|
||||
notifier,
|
||||
BLANK_SLIDE,
|
||||
MAX_SWF_FILE_SIZE
|
||||
notifier
|
||||
);
|
||||
|
||||
pdfToSwfSlidesGenerationService.process(pageToConvert);
|
||||
pdfSlidesGenerationService.process(pageToConvert);
|
||||
listOfPagesConverted.add(pageToConvert);
|
||||
PageToConvert timeoutErrorMessage =
|
||||
listOfPagesConverted.stream().filter(item -> {
|
||||
@ -281,7 +273,7 @@ public class PresentationFileProcessor {
|
||||
processPresentation = false;
|
||||
}
|
||||
|
||||
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
|
||||
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
@ -293,26 +285,10 @@ public class PresentationFileProcessor {
|
||||
this.pageExtractor = extractor;
|
||||
}
|
||||
|
||||
public void setPageConverter(PageConverter converter) {
|
||||
this.pdfToSwfConverter = converter;
|
||||
}
|
||||
|
||||
public void setBlankSlide(String blankSlide) {
|
||||
this.BLANK_SLIDE = blankSlide;
|
||||
}
|
||||
|
||||
public void setMaxSwfFileSize(int size) {
|
||||
this.MAX_SWF_FILE_SIZE = size;
|
||||
}
|
||||
|
||||
public void setGeneratePngs(boolean generatePngs) {
|
||||
this.generatePngs = generatePngs;
|
||||
}
|
||||
|
||||
public void setSwfSlidesRequired(boolean swfSlidesRequired) {
|
||||
this.swfSlidesRequired = swfSlidesRequired;
|
||||
}
|
||||
|
||||
public void setBigPdfSize(long bigPdfSize) {
|
||||
this.bigPdfSize = bigPdfSize;
|
||||
}
|
||||
@ -345,15 +321,15 @@ public class PresentationFileProcessor {
|
||||
MAX_CONVERSION_TIME = minutes * 60 * 1000L * 1000L * 1000L;
|
||||
}
|
||||
|
||||
public void setImageToSwfSlidesGenerationService(ImageToSwfSlidesGenerationService s) {
|
||||
imageToSwfSlidesGenerationService = s;
|
||||
public void setImageSlidesGenerationService(ImageSlidesGenerationService s) {
|
||||
imageSlidesGenerationService = s;
|
||||
}
|
||||
|
||||
public void setPresentationConversionCompletionService(PresentationConversionCompletionService s) {
|
||||
this.presentationConversionCompletionService = s;
|
||||
}
|
||||
|
||||
public void setPdfToSwfSlidesGenerationService(PdfToSwfSlidesGenerationService s) {
|
||||
this.pdfToSwfSlidesGenerationService = s;
|
||||
public void setPdfSlidesGenerationService(PdfSlidesGenerationService s) {
|
||||
this.pdfSlidesGenerationService = s;
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ import org.bigbluebutton.presentation.messages.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SwfSlidesGenerationProgressNotifier {
|
||||
private static Logger log = LoggerFactory.getLogger(SwfSlidesGenerationProgressNotifier.class);
|
||||
public class SlidesGenerationProgressNotifier {
|
||||
private static Logger log = LoggerFactory.getLogger(SlidesGenerationProgressNotifier.class);
|
||||
|
||||
private IBbbWebApiGWApp messagingService;
|
||||
private int maxNumberOfAttempts = 3;
|
||||
@ -50,6 +50,20 @@ public class SwfSlidesGenerationProgressNotifier {
|
||||
maxUploadFileSize);
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
public void sendInvalidMimeTypeMessage(UploadedPresentation pres, String fileMime, String fileExtension) {
|
||||
DocInvalidMimeType invalidMimeType = new DocInvalidMimeType(
|
||||
pres.getPodId(),
|
||||
pres.getMeetingId(),
|
||||
pres.getId(),
|
||||
pres.getTemporaryPresentationId(),
|
||||
pres.getName(),
|
||||
pres.getAuthzToken(),
|
||||
"IVALID_MIME_TYPE",
|
||||
fileMime,
|
||||
fileExtension
|
||||
);
|
||||
messagingService.sendDocConversionMsg(invalidMimeType);
|
||||
}
|
||||
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
|
||||
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
|
||||
pres.getPodId(),
|
@ -28,7 +28,7 @@ import com.zaxxer.nuprocess.NuProcessBuilder;
|
||||
public class SvgImageCreatorImp implements SvgImageCreator {
|
||||
private static Logger log = LoggerFactory.getLogger(SvgImageCreatorImp.class);
|
||||
|
||||
private SwfSlidesGenerationProgressNotifier notifier;
|
||||
private SlidesGenerationProgressNotifier notifier;
|
||||
private long imageTagThreshold;
|
||||
private long pathsThreshold;
|
||||
private int convPdfToSvgTimeout = 60;
|
||||
@ -381,8 +381,8 @@ public class SvgImageCreatorImp implements SvgImageCreator {
|
||||
pathsThreshold = threshold;
|
||||
}
|
||||
|
||||
public void setSwfSlidesGenerationProgressNotifier(
|
||||
SwfSlidesGenerationProgressNotifier notifier) {
|
||||
public void setSlidesGenerationProgressNotifier(
|
||||
SlidesGenerationProgressNotifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
package org.bigbluebutton.presentation.messages;
|
||||
|
||||
public class DocInvalidMimeType implements IDocConversionMsg{
|
||||
|
||||
public final String podId;
|
||||
public final String meetingId;
|
||||
public final String presId;
|
||||
public final String temporaryPresentationId;
|
||||
public final String filename;
|
||||
public final String authzToken;
|
||||
public final String messageKey;
|
||||
public final String fileMime;
|
||||
public final String fileExtension;
|
||||
|
||||
public DocInvalidMimeType( String podId,
|
||||
String meetingId,
|
||||
String presId,
|
||||
String temporaryPresentationId,
|
||||
String filename,
|
||||
String authzToken,
|
||||
String messageKey,
|
||||
String fileMime,
|
||||
String fileExtension) {
|
||||
this.podId = podId;
|
||||
this.meetingId = meetingId;
|
||||
this.presId = presId;
|
||||
this.temporaryPresentationId = temporaryPresentationId;
|
||||
this.filename = filename;
|
||||
this.authzToken = authzToken;
|
||||
this.messageKey = messageKey;
|
||||
this.fileMime = fileMime;
|
||||
this.fileExtension = fileExtension;
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package org.bigbluebutton.api2
|
||||
import scala.collection.JavaConverters._
|
||||
import akka.actor.ActorSystem
|
||||
import akka.event.Logging
|
||||
import java.util
|
||||
import org.bigbluebutton.api.domain.{ BreakoutRoomsParams, Group, LockSettingsParams }
|
||||
import org.bigbluebutton.api.messaging.converters.messages._
|
||||
import org.bigbluebutton.api2.bus._
|
||||
@ -347,6 +346,9 @@ class BbbWebApiGWApp(
|
||||
} else if (msg.isInstanceOf[UploadFileTimedoutMessage]) {
|
||||
val event = MsgBuilder.buildPresentationUploadedFileTimedoutErrorSysMsg(msg.asInstanceOf[UploadFileTimedoutMessage])
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
} else if (msg.isInstanceOf[DocInvalidMimeType]) {
|
||||
val event = MsgBuilder.buildPresentationHasInvalidMimeType(msg.asInstanceOf[DocInvalidMimeType])
|
||||
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,13 +68,12 @@ object MsgBuilder {
|
||||
val id = presId + "/" + page
|
||||
val current = if (page == 1) true else false
|
||||
val thumbUrl = presBaseUrl + "/thumbnail/" + page
|
||||
val swfUrl = presBaseUrl + "/slide/" + page
|
||||
|
||||
val txtUrl = presBaseUrl + "/textfiles/" + page
|
||||
val svgUrl = presBaseUrl + "/svg/" + page
|
||||
val pngUrl = presBaseUrl + "/png/" + page
|
||||
|
||||
val urls = Map("swf" -> swfUrl, "thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
|
||||
val urls = Map("thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
|
||||
|
||||
PresentationPageConvertedVO(
|
||||
id = id,
|
||||
@ -164,14 +163,12 @@ object MsgBuilder {
|
||||
val num = i
|
||||
val current = if (i == 1) true else false
|
||||
val thumbnail = presBaseUrl + "/thumbnail/" + i
|
||||
val swfUri = presBaseUrl + "/slide/" + i
|
||||
|
||||
val txtUri = presBaseUrl + "/textfiles/" + i
|
||||
val svgUri = presBaseUrl + "/svg/" + i
|
||||
|
||||
val p = PageVO(id = id, num = num, thumbUri = thumbnail, swfUri = swfUri,
|
||||
txtUri = txtUri, svgUri = svgUri,
|
||||
current = current)
|
||||
val p = PageVO(id = id, num = num, thumbUri = thumbnail,
|
||||
txtUri = txtUri, svgUri = svgUri, current = current)
|
||||
pages += p.id -> p
|
||||
}
|
||||
|
||||
@ -288,6 +285,19 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, req)
|
||||
}
|
||||
|
||||
def buildPresentationHasInvalidMimeType(msg: DocInvalidMimeType): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||
val envelope = BbbCoreEnvelope(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, msg.meetingId, "not-used")
|
||||
|
||||
val body = PresentationHasInvalidMimeTypeErrorSysPubMsgBody(podId = msg.podId, presentationName = msg.filename,
|
||||
temporaryPresentationId = msg.temporaryPresentationId, presentationId = msg.presId, meetingId = msg.meetingId,
|
||||
messageKey = msg.messageKey, fileMime = msg.fileMime, fileExtension = msg.fileExtension)
|
||||
|
||||
val req = PresentationHasInvalidMimeTypeErrorSysPubMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, req)
|
||||
}
|
||||
|
||||
def buildPresentationUploadedFileTimedoutErrorSysMsg(msg: UploadFileTimedoutMessage): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||
val envelope = BbbCoreEnvelope(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, routing)
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
"level": "info",
|
||||
"msgName": "PresAnnStatusMsg"
|
||||
},
|
||||
"shared": {
|
||||
"presDir": "/var/bigbluebutton",
|
||||
|
32
bbb-export-annotations/lib/utils/worker-starter.js
Normal file
32
bbb-export-annotations/lib/utils/worker-starter.js
Normal file
@ -0,0 +1,32 @@
|
||||
const {Worker} = require('worker_threads');
|
||||
const path = require('path');
|
||||
|
||||
const WorkerTypes = Object.freeze({
|
||||
Collector: 'collector',
|
||||
Process: 'process',
|
||||
Notifier: 'notifier',
|
||||
});
|
||||
|
||||
const kickOffWorker = (workerType, workerData) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const workerPath = path.join(__dirname, '..', '..', 'workers', `${workerType}.js`);
|
||||
const worker = new Worker(workerPath, {workerData});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Worker '${workerType}' stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = class WorkerStarter {
|
||||
constructor(workerData) {
|
||||
this.workerData = workerData;
|
||||
}
|
||||
|
||||
collect = () => kickOffWorker(WorkerTypes.Collector, this.workerData);
|
||||
process = () => kickOffWorker(WorkerTypes.Process, this.workerData);
|
||||
notify = () => kickOffWorker(WorkerTypes.Notifier, this.workerData);
|
||||
};
|
@ -1,28 +1,14 @@
|
||||
const Logger = require('./lib/utils/logger');
|
||||
const WorkerStarter = require('./lib/utils/worker-starter');
|
||||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
const redis = require('redis');
|
||||
const {commandOptions} = require('redis');
|
||||
const {Worker} = require('worker_threads');
|
||||
const path = require('path');
|
||||
|
||||
const logger = new Logger('presAnn Master');
|
||||
logger.info('Running bbb-export-annotations');
|
||||
|
||||
const kickOffCollectorWorker = (jobId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const collectorPath = path.join(__dirname, 'workers', 'collector.js');
|
||||
const worker = new Worker(collectorPath, {workerData: jobId});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Collector Worker stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
@ -49,9 +35,10 @@ const kickOffCollectorWorker = (jobId) => {
|
||||
|
||||
logger.info('Received job', job.element);
|
||||
const exportJob = JSON.parse(job.element);
|
||||
const jobId = exportJob.jobId;
|
||||
|
||||
// Create folder in dropbox
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, exportJob.jobId);
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
fs.mkdirSync(dropbox, {recursive: true});
|
||||
|
||||
// Drop job into dropbox as JSON
|
||||
@ -61,8 +48,8 @@ const kickOffCollectorWorker = (jobId) => {
|
||||
}
|
||||
});
|
||||
|
||||
kickOffCollectorWorker(exportJob.jobId);
|
||||
|
||||
const collectorWorker = new WorkerStarter({jobId});
|
||||
collectorWorker.collect();
|
||||
waitForJobs();
|
||||
}
|
||||
|
||||
|
@ -7,36 +7,20 @@ const path = require('path');
|
||||
const redis = require('redis');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const stream = require('stream');
|
||||
const {Worker, workerData} = require('worker_threads');
|
||||
const WorkerStarter = require('../lib/utils/worker-starter');
|
||||
const {workerData} = require('worker_threads');
|
||||
const {promisify} = require('util');
|
||||
|
||||
const WorkerTypes = Object.freeze({
|
||||
Notifier: 'notifier',
|
||||
Process: 'process',
|
||||
});
|
||||
|
||||
const jobId = workerData;
|
||||
const jobId = workerData.jobId;
|
||||
const logger = new Logger('presAnn Collector');
|
||||
logger.info(`Collecting job ${jobId}`);
|
||||
|
||||
const kickOffWorker = (workerType, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(`./workers/${workerType}.js`, {workerData: data});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Worker '${workerType}' stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
|
||||
// Takes the Job from the dropbox
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
const jobType = exportJob.jobType;
|
||||
|
||||
async function collectAnnotationsFromRedis() {
|
||||
const client = redis.createClient({
|
||||
@ -54,8 +38,6 @@ async function collectAnnotationsFromRedis() {
|
||||
// Remove annotations from Redis
|
||||
await client.del(jobId);
|
||||
|
||||
client.disconnect();
|
||||
|
||||
const annotations = JSON.stringify(presAnn);
|
||||
|
||||
const whiteboard = JSON.parse(annotations);
|
||||
@ -72,6 +54,31 @@ async function collectAnnotationsFromRedis() {
|
||||
const presFile = path.join(exportJob.presLocation, exportJob.presId);
|
||||
const pdfFile = `${presFile}.pdf`;
|
||||
|
||||
// Message to display conversion progress toast
|
||||
const statusUpdate = {
|
||||
envelope: {
|
||||
name: config.log.msgName,
|
||||
routing: {
|
||||
sender: exportJob.module,
|
||||
},
|
||||
timestamp: (new Date()).getTime(),
|
||||
},
|
||||
core: {
|
||||
header: {
|
||||
name: config.log.msgName,
|
||||
meetingId: exportJob.parentMeetingId,
|
||||
userId: '',
|
||||
},
|
||||
body: {
|
||||
presId: exportJob.presId,
|
||||
pageNumber: 1,
|
||||
totalPages: pages.length,
|
||||
status: 'COLLECTING',
|
||||
error: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
for (const p of pages) {
|
||||
const pageNumber = p.page;
|
||||
@ -93,19 +100,39 @@ async function collectAnnotationsFromRedis() {
|
||||
pdfFile, outputFile,
|
||||
];
|
||||
|
||||
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
||||
try {
|
||||
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
||||
} catch (error) {
|
||||
const error_reason = `PDFtoCairo failed extracting slide ${pageNumber}`;
|
||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
||||
statusUpdate.core.body.status = error_reason;
|
||||
statusUpdate.core.body.error = true;
|
||||
}
|
||||
|
||||
statusUpdate.core.body.pageNumber = pageNumber;
|
||||
statusUpdate.envelope.timestamp = (new Date()).getTime();
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
statusUpdate.core.body.error = false;
|
||||
}
|
||||
// If PNG file already available
|
||||
} else if (fs.existsSync(`${presFile}.png`)) {
|
||||
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
// If JPEG file available
|
||||
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
||||
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
} else {
|
||||
return logger.error(`Could not find presentation file ${jobId}`);
|
||||
statusUpdate.core.body.error = true;
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
client.disconnect();
|
||||
return logger.error(`Presentation file missing for job ${exportJob.jobId}`);
|
||||
}
|
||||
|
||||
kickOffWorker(WorkerTypes.Process, jobId);
|
||||
client.disconnect();
|
||||
|
||||
const process = new WorkerStarter({jobId, statusUpdate});
|
||||
process.process();
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
@ -152,12 +179,13 @@ async function collectSharedNotes(retries = 3) {
|
||||
}
|
||||
}
|
||||
|
||||
kickOffWorker(WorkerTypes.Notifier, [exportJob.jobType, jobId, filename]);
|
||||
const notifier = new WorkerStarter({jobType, jobId, filename});
|
||||
notifier.notify();
|
||||
}
|
||||
|
||||
switch (exportJob.jobType) {
|
||||
switch (jobType) {
|
||||
case 'PresentationWithAnnotationExportJob': return collectAnnotationsFromRedis();
|
||||
case 'PresentationWithAnnotationDownloadJob': return collectAnnotationsFromRedis();
|
||||
case 'PadCaptureJob': return collectSharedNotes();
|
||||
default: return logger.error(`Unknown job type ${exportJob.jobType}`);
|
||||
default: return logger.error(`Unknown job type ${jobType}`);
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ const axios = require('axios').default;
|
||||
const path = require('path');
|
||||
|
||||
const {workerData} = require('worker_threads');
|
||||
|
||||
const [jobType, jobId, filename] = workerData;
|
||||
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
|
||||
|
||||
const logger = new Logger('presAnn Notifier Worker');
|
||||
|
||||
|
@ -3,31 +3,21 @@ const config = require('../config');
|
||||
const fs = require('fs');
|
||||
const {create} = require('xmlbuilder2', {encoding: 'utf-8'});
|
||||
const cp = require('child_process');
|
||||
const {Worker, workerData} = require('worker_threads');
|
||||
const WorkerStarter = require('../lib/utils/worker-starter');
|
||||
const {workerData} = require('worker_threads');
|
||||
const path = require('path');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
|
||||
const probe = require('probe-image-size');
|
||||
const redis = require('redis');
|
||||
|
||||
const jobId = workerData;
|
||||
const [jobId, statusUpdate] = [workerData.jobId, workerData.statusUpdate];
|
||||
|
||||
const logger = new Logger('presAnn Process Worker');
|
||||
logger.info('Processing PDF for job ' + jobId);
|
||||
statusUpdate.core.body.status = 'PROCESSING';
|
||||
|
||||
const kickOffNotifierWorker = (jobType, filename) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const notifierPath = './workers/notifier.js';
|
||||
const worker = new Worker(notifierPath,
|
||||
{workerData: [jobType, jobId, filename]});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Notifier Worker stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
|
||||
// General utilities for rendering SVGs resembling Tldraw as much as possible
|
||||
function align_to_pango(alignment) {
|
||||
@ -104,7 +94,7 @@ function determine_font_from_family(family) {
|
||||
case 'script': return 'Caveat Brush';
|
||||
case 'sans': return 'Source Sans Pro';
|
||||
case 'serif': return 'Crimson Pro';
|
||||
// Temporary workaround due to typo in messages
|
||||
// Temporary workaround due to typo in messages
|
||||
case 'erif': return 'Crimson Pro';
|
||||
case 'mono': return 'Source Code Pro';
|
||||
|
||||
@ -164,7 +154,14 @@ function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxW
|
||||
path.join(dropbox, `text${id}.png`),
|
||||
]);
|
||||
|
||||
cp.spawnSync(config.shared.imagemagick, commands, {shell: false});
|
||||
try {
|
||||
cp.spawnSync(config.shared.imagemagick, commands, {shell: false});
|
||||
} catch (error) {
|
||||
const error_reason = 'ImageMagick failed to render textbox';
|
||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
||||
statusUpdate.core.body.status = error_reason;
|
||||
statusUpdate.core.body.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
function get_gap(dash, size) {
|
||||
@ -266,14 +263,14 @@ function circleFromThreePoints(A, B, C) {
|
||||
const a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2;
|
||||
|
||||
const b =
|
||||
(x1 * x1 + y1 * y1) * (y3 - y2) +
|
||||
(x2 * x2 + y2 * y2) * (y1 - y3) +
|
||||
(x3 * x3 + y3 * y3) * (y2 - y1);
|
||||
(x1 * x1 + y1 * y1) * (y3 - y2) +
|
||||
(x2 * x2 + y2 * y2) * (y1 - y3) +
|
||||
(x3 * x3 + y3 * y3) * (y2 - y1);
|
||||
|
||||
const c =
|
||||
(x1 * x1 + y1 * y1) * (x2 - x3) +
|
||||
(x2 * x2 + y2 * y2) * (x3 - x1) +
|
||||
(x3 * x3 + y3 * y3) * (x1 - x2);
|
||||
(x1 * x1 + y1 * y1) * (x2 - x3) +
|
||||
(x2 * x2 + y2 * y2) * (x3 - x1) +
|
||||
(x3 * x3 + y3 * y3) * (x1 - x2);
|
||||
|
||||
const x = -b / (2 * a);
|
||||
const y = -c / (2 * a);
|
||||
@ -779,114 +776,146 @@ function overlay_annotations(svg, currentSlideAnnotations) {
|
||||
}
|
||||
|
||||
// Process the presentation pages and annotations into a PDF file
|
||||
|
||||
// 1. Get the job
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
|
||||
// 2. Get the annotations
|
||||
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
|
||||
const whiteboard = JSON.parse(annotations);
|
||||
const pages = JSON.parse(whiteboard.pages);
|
||||
const ghostScriptInput = [];
|
||||
|
||||
// 3. Convert annotations to SVG
|
||||
for (const currentSlide of pages) {
|
||||
const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`);
|
||||
const svgBackgroundSlide = path.join(exportJob.presLocation,
|
||||
'svgs', `slide${currentSlide.page}.svg`);
|
||||
const svgBackgroundExists = fs.existsSync(svgBackgroundSlide);
|
||||
const backgroundFormat = fs.existsSync(`${bgImagePath}.png`) ? 'png' : 'jpeg';
|
||||
|
||||
// Output dimensions in pixels even if stated otherwise (pt)
|
||||
// CairoSVG didn't like attempts to read the dimensions from a stream
|
||||
// that would prevent loading file in memory
|
||||
// Ideally, use dimensions provided by tldraw's background image asset
|
||||
// (this is not yet always provided)
|
||||
const dimensions = svgBackgroundExists ?
|
||||
probe.sync(fs.readFileSync(svgBackgroundSlide)) :
|
||||
probe.sync(fs.readFileSync(`${bgImagePath}.${backgroundFormat}`));
|
||||
|
||||
const slideWidth = parseInt(dimensions.width, 10);
|
||||
const slideHeight = parseInt(dimensions.height, 10);
|
||||
|
||||
// Create the SVG slide with the background image
|
||||
let svg = create({version: '1.0', encoding: 'UTF-8'})
|
||||
.ele('svg', {
|
||||
'xmlns': 'http://www.w3.org/2000/svg',
|
||||
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
|
||||
'width': `${slideWidth}px`,
|
||||
'height': `${slideHeight}px`,
|
||||
})
|
||||
.dtd({
|
||||
pubID: '-//W3C//DTD SVG 1.1//EN',
|
||||
sysID: 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
|
||||
})
|
||||
.ele('image', {
|
||||
'xlink:href': `file://${dropbox}/slide${currentSlide.page}.${backgroundFormat}`,
|
||||
'width': `${slideWidth}px`,
|
||||
'height': `${slideHeight}px`,
|
||||
})
|
||||
.up()
|
||||
.ele('g', {
|
||||
class: 'canvas',
|
||||
});
|
||||
|
||||
// 4. Overlay annotations onto slides
|
||||
overlay_annotations(svg, currentSlide.annotations);
|
||||
|
||||
svg = svg.end({prettyPrint: true});
|
||||
|
||||
// Write annotated SVG file
|
||||
const SVGfile = path.join(dropbox, `annotated-slide${currentSlide.page}.svg`);
|
||||
const PDFfile = path.join(dropbox, `annotated-slide${currentSlide.page}.pdf`);
|
||||
|
||||
fs.writeFileSync(SVGfile, svg, function(err) {
|
||||
if (err) {
|
||||
return logger.error(err);
|
||||
}
|
||||
async function process_presentation_annotations() {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
});
|
||||
|
||||
// Dimensions converted to a pixel size which,
|
||||
// when converted to points, will yield the desired
|
||||
// dimension in pixels when read without conversion
|
||||
await client.connect();
|
||||
|
||||
// e.g. say the background SVG dimensions are set to 1920x1080 pt
|
||||
// Resize output to 2560x1440 px so that the SVG
|
||||
// generates with the original size in pt.
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
const convertAnnotatedSlide = [
|
||||
SVGfile,
|
||||
'--output-width', to_px(slideWidth),
|
||||
'--output-height', to_px(slideHeight),
|
||||
'-o', PDFfile,
|
||||
];
|
||||
// 1. Get the job
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
|
||||
cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false});
|
||||
// 2. Get the annotations
|
||||
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
|
||||
const whiteboard = JSON.parse(annotations);
|
||||
const pages = JSON.parse(whiteboard.pages);
|
||||
const ghostScriptInput = [];
|
||||
|
||||
ghostScriptInput.push(PDFfile);
|
||||
// 3. Convert annotations to SVG
|
||||
for (const currentSlide of pages) {
|
||||
const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`);
|
||||
const svgBackgroundSlide = path.join(exportJob.presLocation,
|
||||
'svgs', `slide${currentSlide.page}.svg`);
|
||||
const svgBackgroundExists = fs.existsSync(svgBackgroundSlide);
|
||||
const backgroundFormat = fs.existsSync(`${bgImagePath}.png`) ? 'png' : 'jpeg';
|
||||
|
||||
// Output dimensions in pixels even if stated otherwise (pt)
|
||||
// CairoSVG didn't like attempts to read the dimensions from a stream
|
||||
// that would prevent loading file in memory
|
||||
// Ideally, use dimensions provided by tldraw's background image asset
|
||||
// (this is not yet always provided)
|
||||
const dimensions = svgBackgroundExists ?
|
||||
probe.sync(fs.readFileSync(svgBackgroundSlide)) :
|
||||
probe.sync(fs.readFileSync(`${bgImagePath}.${backgroundFormat}`));
|
||||
|
||||
const slideWidth = parseInt(dimensions.width, 10);
|
||||
const slideHeight = parseInt(dimensions.height, 10);
|
||||
|
||||
// Create the SVG slide with the background image
|
||||
let svg = create({version: '1.0', encoding: 'UTF-8'})
|
||||
.ele('svg', {
|
||||
'xmlns': 'http://www.w3.org/2000/svg',
|
||||
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
|
||||
'width': `${slideWidth}px`,
|
||||
'height': `${slideHeight}px`,
|
||||
})
|
||||
.dtd({
|
||||
pubID: '-//W3C//DTD SVG 1.1//EN',
|
||||
sysID: 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
|
||||
})
|
||||
.ele('image', {
|
||||
'xlink:href': `file://${dropbox}/slide${currentSlide.page}.${backgroundFormat}`,
|
||||
'width': `${slideWidth}px`,
|
||||
'height': `${slideHeight}px`,
|
||||
})
|
||||
.up()
|
||||
.ele('g', {
|
||||
class: 'canvas',
|
||||
});
|
||||
|
||||
// 4. Overlay annotations onto slides
|
||||
overlay_annotations(svg, currentSlide.annotations);
|
||||
|
||||
svg = svg.end({prettyPrint: true});
|
||||
|
||||
// Write annotated SVG file
|
||||
const SVGfile = path.join(dropbox, `annotated-slide${currentSlide.page}.svg`);
|
||||
const PDFfile = path.join(dropbox, `annotated-slide${currentSlide.page}.pdf`);
|
||||
|
||||
fs.writeFileSync(SVGfile, svg, function(err) {
|
||||
if (err) {
|
||||
return logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Dimensions converted to a pixel size which,
|
||||
// when converted to points, will yield the desired
|
||||
// dimension in pixels when read without conversion
|
||||
|
||||
// e.g. say the background SVG dimensions are set to 1920x1080 pt
|
||||
// Resize output to 2560x1440 px so that the SVG
|
||||
// generates with the original size in pt.
|
||||
|
||||
const convertAnnotatedSlide = [
|
||||
SVGfile,
|
||||
'--output-width', to_px(slideWidth),
|
||||
'--output-height', to_px(slideHeight),
|
||||
'-o', PDFfile,
|
||||
];
|
||||
|
||||
try {
|
||||
cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false});
|
||||
} catch (error) {
|
||||
logger.error(`Processing slide ${currentSlide.page} failed for job ${jobId}: ${error.message}`);
|
||||
statusUpdate.core.body.error = true;
|
||||
}
|
||||
|
||||
statusUpdate.core.body.pageNumber = currentSlide.page;
|
||||
statusUpdate.envelope.timestamp = (new Date()).getTime();
|
||||
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
ghostScriptInput.push(PDFfile);
|
||||
statusUpdate.core.body.error = false;
|
||||
}
|
||||
|
||||
// Create PDF output directory if it doesn't exist
|
||||
const outputDir = path.join(exportJob.presLocation, 'pdfs', jobId);
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, {recursive: true});
|
||||
}
|
||||
|
||||
const filename_with_extension = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.pdf`;
|
||||
|
||||
const mergePDFs = [
|
||||
'-dNOPAUSE',
|
||||
'-sDEVICE=pdfwrite',
|
||||
`-sOUTPUTFILE="${path.join(outputDir, filename_with_extension)}"`,
|
||||
`-dBATCH`].concat(ghostScriptInput);
|
||||
|
||||
// Resulting PDF file is stored in the presentation dir
|
||||
try {
|
||||
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
|
||||
} catch (error) {
|
||||
const error_reason = 'GhostScript failed to merge PDFs';
|
||||
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
|
||||
statusUpdate.core.body.status = error_reason;
|
||||
statusUpdate.core.body.error = true;
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
|
||||
}
|
||||
|
||||
// Launch Notifier Worker depending on job type
|
||||
logger.info(`Saved PDF at ${outputDir}/${jobId}/${filename_with_extension}`);
|
||||
|
||||
const notifier = new WorkerStarter({jobType: exportJob.jobType, jobId, filename: filename_with_extension});
|
||||
notifier.notify();
|
||||
await client.disconnect();
|
||||
}
|
||||
|
||||
// Create PDF output directory if it doesn't exist
|
||||
const outputDir = path.join(exportJob.presLocation, 'pdfs', jobId);
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, {recursive: true});
|
||||
}
|
||||
|
||||
const filename_with_extension = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.pdf`;
|
||||
|
||||
const mergePDFs = [
|
||||
'-dNOPAUSE',
|
||||
'-sDEVICE=pdfwrite',
|
||||
`-sOUTPUTFILE="${path.join(outputDir, filename_with_extension)}"`,
|
||||
`-dBATCH`].concat(ghostScriptInput);
|
||||
|
||||
// Resulting PDF file is stored in the presentation dir
|
||||
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
|
||||
|
||||
// Launch Notifier Worker depending on job type
|
||||
logger.info(`Saved PDF at ${outputDir}/${jobId}/${filename_with_extension}`);
|
||||
|
||||
kickOffNotifierWorker(exportJob.jobType, filename_with_extension);
|
||||
process_presentation_annotations();
|
||||
|
11762
bbb-learning-dashboard/package-lock.json
generated
11762
bbb-learning-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,19 +9,14 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/x-data-grid": "^5.17.10",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-intl": "^5.20.6",
|
||||
"typescript": "^4.3.5",
|
||||
"web-vitals": "^1.1.2"
|
||||
"react-scripts": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@ -48,6 +43,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"autoprefixer": "^10.4.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
@ -57,7 +53,6 @@
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"postcss": "^8.4.5",
|
||||
"react-scripts": "^5.0.0",
|
||||
"tailwindcss": "^3.0.11"
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
@ -321,7 +321,7 @@ const PollsTable = (props) => {
|
||||
const answersSorted = Object.entries(pollVotesCount[v?.pollId])
|
||||
.sort(([, countA], [, countB]) => countB - countA);
|
||||
const isMostCommonAnswer = (
|
||||
answersSorted[0]?.[0]?.toLowerCase() === params?.value[0]?.toLowerCase()
|
||||
answersSorted[0]?.[0]?.toLowerCase() === params?.value?.toLowerCase()
|
||||
&& answersSorted[0]?.[1] > 1
|
||||
);
|
||||
return <GridCellExpand anonymous={v?.anonymous} isMostCommonAnswer={isMostCommonAnswer} value={params?.value || ''} width={params?.colDef?.computedWidth} />;
|
||||
|
@ -3,7 +3,6 @@ import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { UserDetailsProvider } from './components/UserDetails/context';
|
||||
|
||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||
@ -83,5 +82,3 @@ class Dashboard extends React.Component {
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
ReactDOM.render(<Dashboard />, rootElement);
|
||||
|
||||
reportWebVitals();
|
||||
|
@ -1,15 +0,0 @@
|
||||
const reportWebVitals = (onPerfEntry) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({
|
||||
getCLS, getFID, getFCP, getLCP, getTTFB,
|
||||
}) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
@ -1 +1 @@
|
||||
git clone --branch v5.0.0-alpha.3 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
git clone --branch v5.0.0-beta.1 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.9.4 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.9.5 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.6.0-beta.1
|
||||
BIGBLUEBUTTON_RELEASE=2.6.0-beta.2
|
||||
|
@ -832,21 +832,6 @@ check_configuration() {
|
||||
echo "# is not owned by $BBB_USER"
|
||||
fi
|
||||
|
||||
if [ -n "$HTML5_CONFIG" ]; then
|
||||
SVG_IMAGES_REQUIRED=$(cat $BBB_WEB_CONFIG | grep -v '#' | sed -n '/^svgImagesRequired/{s/.*=//;p}')
|
||||
if [ "$SVG_IMAGES_REQUIRED" != "true" ]; then
|
||||
echo
|
||||
echo "# Warning: You have the HTML5 client installed but in"
|
||||
echo "#"
|
||||
echo "# $BBB_WEB_CONFIG"
|
||||
echo "#"
|
||||
echo "# the setting for svgImagesRequired is false. To fix, run the commnad"
|
||||
echo "#"
|
||||
echo "# sed -i 's/^svgImagesRequired=.*/svgImagesRequired=true/' $BBB_WEB_CONFIG "
|
||||
echo "#"
|
||||
fi
|
||||
fi
|
||||
|
||||
CHECK_STUN=$(xmlstarlet sel -t -m '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]' -v @data $FREESWITCH_VARS | sed 's/external_rtp_ip=stun://g')
|
||||
if [ "$CHECK_STUN" == "stun.freeswitch.org" ]; then
|
||||
echo
|
||||
@ -1370,7 +1355,6 @@ if [ $CHECK ]; then
|
||||
echo "$BBB_WEB_CONFIG (bbb-web)"
|
||||
echo " bigbluebutton.web.serverURL: $(get_bbb_web_config_value bigbluebutton.web.serverURL)"
|
||||
echo " defaultGuestPolicy: $(get_bbb_web_config_value defaultGuestPolicy)"
|
||||
echo " svgImagesRequired: $(get_bbb_web_config_value svgImagesRequired)"
|
||||
echo " defaultMeetingLayout: $(get_bbb_web_config_value defaultMeetingLayout)"
|
||||
|
||||
echo
|
||||
|
Binary file not shown.
@ -5,11 +5,13 @@ import handlePresentationCurrentSet from './handlers/presentationCurrentSet';
|
||||
import handlePresentationConversionUpdate from './handlers/presentationConversionUpdate';
|
||||
import handlePresentationDownloadableSet from './handlers/presentationDownloadableSet';
|
||||
import handlePresentationExport from './handlers/presentationExport';
|
||||
import handlePresentationExportToastUpdate from './handlers/presentationExportToastUpdate';
|
||||
|
||||
RedisPubSub.on('PdfConversionInvalidErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationUploadedFileTimeoutErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationHasInvalidMimeTypeErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationUploadedFileTooLargeErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);
|
||||
@ -17,3 +19,4 @@ RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove);
|
||||
RedisPubSub.on('SetCurrentPresentationEvtMsg', handlePresentationCurrentSet);
|
||||
RedisPubSub.on('SetPresentationDownloadableEvtMsg', handlePresentationDownloadableSet);
|
||||
RedisPubSub.on('NewPresAnnFileAvailableEvtMsg', handlePresentationExport);
|
||||
RedisPubSub.on('PresAnnStatusEvtMsg', handlePresentationExportToastUpdate);
|
||||
|
@ -13,6 +13,7 @@ const PDF_HAS_BIG_PAGE_KEY = 'PDF_HAS_BIG_PAGE';
|
||||
const GENERATED_SLIDE_KEY = 'GENERATED_SLIDE';
|
||||
const FILE_TOO_LARGE_KEY = 'FILE_TOO_LARGE';
|
||||
const CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
|
||||
const IVALID_MIME_TYPE_KEY = "IVALID_MIME_TYPE";
|
||||
// const GENERATING_THUMBNAIL_KEY = 'GENERATING_THUMBNAIL';
|
||||
// const GENERATED_THUMBNAIL_KEY = 'GENERATED_THUMBNAIL';
|
||||
// const GENERATING_TEXTFILES_KEY = 'GENERATING_TEXTFILES';
|
||||
@ -50,6 +51,10 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
|
||||
statusModifier['conversion.maxFileSize'] = body.maxFileSize;
|
||||
case UNSUPPORTED_DOCUMENT_KEY:
|
||||
case OFFICE_DOC_CONVERSION_FAILED_KEY:
|
||||
case IVALID_MIME_TYPE_KEY:
|
||||
statusModifier['conversion.error'] = true;
|
||||
statusModifier['conversion.fileMime'] = body.fileMime;
|
||||
statusModifier['conversion.fileExtension'] = body.fileExtension;
|
||||
case OFFICE_DOC_CONVERSION_INVALID_KEY:
|
||||
case PAGE_COUNT_FAILED_KEY:
|
||||
case PAGE_COUNT_EXCEEDED_KEY:
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { check } from 'meteor/check';
|
||||
import setPresentationExporting from '/imports/api/presentations/server/modifiers/setPresentationExporting';
|
||||
|
||||
export default function handlePresentationExportToastUpdate({ body }, meetingId) {
|
||||
check(body, Object);
|
||||
check(meetingId, String);
|
||||
|
||||
const {
|
||||
presId, pageNumber, totalPages, status, error,
|
||||
} = body;
|
||||
|
||||
check(presId, String);
|
||||
check(pageNumber, Number);
|
||||
check(totalPages, Number);
|
||||
check(status, String);
|
||||
check(error, Boolean);
|
||||
|
||||
setPresentationExporting(meetingId, presId, {
|
||||
pageNumber, totalPages, status, error,
|
||||
});
|
||||
}
|
@ -40,7 +40,6 @@ export default function addPresentation(meetingId, podId, presentation) {
|
||||
id: String,
|
||||
num: Number,
|
||||
thumbUri: String,
|
||||
swfUri: String,
|
||||
txtUri: String,
|
||||
svgUri: String,
|
||||
current: Boolean,
|
||||
|
@ -21,9 +21,8 @@ export default function setPresentationExporting(meetingId, presentationId, expo
|
||||
try {
|
||||
const { numberAffected } = Presentations.upsert(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
const status = `isRunning=${exportation.isRunning} error=${exportation.error}`;
|
||||
Logger.info(`Set exporting status on presentation ${presentationId} in meeting ${meetingId} ${status}`);
|
||||
if (numberAffected && ['RUNNING', 'EXPORTED'].includes(exportation?.status)) {
|
||||
Logger.info(`Set exporting status on presentation ${presentationId} in meeting ${meetingId} status=${exportation.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Could not set exporting status on pres ${presentationId} in meeting ${meetingId} ${err}`);
|
||||
|
@ -50,7 +50,6 @@ export default function addSlide(meetingId, podId, presentationId, slide) {
|
||||
id: String,
|
||||
num: Number,
|
||||
thumbUri: String,
|
||||
swfUri: String,
|
||||
txtUri: String,
|
||||
svgUri: String,
|
||||
current: Boolean,
|
||||
|
@ -171,7 +171,7 @@ class Base extends Component {
|
||||
HTML.classList.add('animationsDisabled');
|
||||
}
|
||||
|
||||
if (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true)) {
|
||||
if (Session.equals('layoutReady', true) && (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true))) {
|
||||
if (!checkedUserSettings) {
|
||||
if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) {
|
||||
if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) {
|
||||
@ -304,6 +304,7 @@ class Base extends Component {
|
||||
render() {
|
||||
const {
|
||||
meetingExist,
|
||||
codeError,
|
||||
} = this.props;
|
||||
const { meetingExisted } = this.state;
|
||||
|
||||
@ -311,7 +312,7 @@ class Base extends Component {
|
||||
<>
|
||||
{meetingExist && Auth.loggedIn && <DebugWindow />}
|
||||
{
|
||||
(!meetingExisted && !meetingExist && Auth.loggedIn)
|
||||
(!meetingExisted && !meetingExist && Auth.loggedIn && !codeError)
|
||||
? <LoadingScreen />
|
||||
: this.renderByState()
|
||||
}
|
||||
|
45
bigbluebutton-html5/imports/ui/components/app/component.jsx
Executable file → Normal file
45
bigbluebutton-html5/imports/ui/components/app/component.jsx
Executable file → Normal file
@ -30,7 +30,7 @@ import PresentationAreaContainer from '../presentation/presentation-area/contain
|
||||
import ScreenshareContainer from '../screenshare/container';
|
||||
import ExternalVideoContainer from '../external-video-player/container';
|
||||
import Styled from './styles';
|
||||
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT } from '../layout/enums';
|
||||
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT, PANELS } from '../layout/enums';
|
||||
import {
|
||||
isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
|
||||
} from '../layout/utils';
|
||||
@ -47,13 +47,16 @@ import Notifications from '../notifications/container';
|
||||
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
|
||||
import ActionsBarContainer from '../actions-bar/container';
|
||||
import PushLayoutEngine from '../layout/push-layout/pushLayoutEngine';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
import NotesContainer from '/imports/ui/components/notes/container';
|
||||
import DEFAULT_VALUES from '../layout/defaultValues';
|
||||
|
||||
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
|
||||
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
|
||||
const LAYOUT_CONFIG = Meteor.settings.public.layout;
|
||||
const CONFIRMATION_ON_LEAVE = Meteor.settings.public.app.askForConfirmationOnLeave;
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
userListLabel: {
|
||||
@ -195,6 +198,16 @@ class App extends Component {
|
||||
window.ondragover = (e) => { e.preventDefault(); };
|
||||
window.ondrop = (e) => { e.preventDefault(); };
|
||||
|
||||
if (CONFIRMATION_ON_LEAVE) {
|
||||
window.onbeforeunload = (event) => {
|
||||
AudioService.muteMicrophone();
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.returnValue = '';
|
||||
};
|
||||
}
|
||||
|
||||
if (deviceInfo.isMobile) makeCall('setMobileUser');
|
||||
|
||||
ConnectionStatusService.startRoundTripTime();
|
||||
@ -210,6 +223,11 @@ class App extends Component {
|
||||
mountModal,
|
||||
deviceType,
|
||||
mountRandomUserModal,
|
||||
selectedLayout,
|
||||
sidebarContentIsOpen,
|
||||
layoutContextDispatch,
|
||||
numCameras,
|
||||
presentationIsOpen,
|
||||
} = this.props;
|
||||
|
||||
this.renderDarkMode();
|
||||
@ -243,10 +261,35 @@ class App extends Component {
|
||||
}
|
||||
|
||||
if (deviceType === null || prevProps.deviceType !== deviceType) this.throttledDeviceType();
|
||||
|
||||
if (
|
||||
selectedLayout !== prevProps.selectedLayout
|
||||
&& selectedLayout?.toLowerCase?.()?.includes?.('focus')
|
||||
&& !sidebarContentIsOpen
|
||||
&& deviceType !== DEVICE_TYPE.MOBILE
|
||||
&& numCameras > 0
|
||||
&& presentationIsOpen
|
||||
) {
|
||||
setTimeout(() => {
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||
value: true,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_ID_CHAT_OPEN,
|
||||
value: DEFAULT_VALUES.idChatOpen,
|
||||
});
|
||||
layoutContextDispatch({
|
||||
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||
value: PANELS.CHAT,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleWindowResize, false);
|
||||
window.onbeforeunload = null;
|
||||
ConnectionStatusService.stopRoundTripTime();
|
||||
}
|
||||
|
||||
|
@ -168,6 +168,7 @@ const AppContainer = (props) => {
|
||||
shouldShowPresentation,
|
||||
mountRandomUserModal,
|
||||
isPresenter,
|
||||
numCameras: cameraDockInput.numCameras,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
@ -89,8 +89,6 @@ const CaptionsButton = ({
|
||||
isSupported,
|
||||
isVoiceUser,
|
||||
}) => {
|
||||
if (!enabled) return null;
|
||||
|
||||
const isTranscriptionDisabled = () => (
|
||||
currentSpeechLocale === DISABLED
|
||||
);
|
||||
@ -106,6 +104,8 @@ const CaptionsButton = ({
|
||||
if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue;
|
||||
}, [currentSpeechLocale]);
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
const shouldRenderChevron = isSupported && isVoiceUser;
|
||||
|
||||
const getAvailableLocales = () => (
|
||||
|
@ -64,8 +64,6 @@ const Select = ({
|
||||
locale,
|
||||
voices,
|
||||
}) => {
|
||||
if (!enabled) return null;
|
||||
|
||||
if (voices.length === 0) {
|
||||
return (
|
||||
<div
|
||||
@ -79,7 +77,7 @@ const Select = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (SpeechService.useFixedLocale()) return null;
|
||||
if (!enabled || SpeechService.useFixedLocale()) return null;
|
||||
|
||||
const onChange = (e) => {
|
||||
const { value } = e.target;
|
||||
|
@ -71,6 +71,21 @@ const init = (messages, intl) => {
|
||||
return AudioManager.init(userData, audioEventHandler);
|
||||
};
|
||||
|
||||
const muteMicrophone = () => {
|
||||
const user = VoiceUsers.findOne({
|
||||
meetingId: Auth.meetingID, intId: Auth.userID,
|
||||
}, { fields: { muted: 1 } });
|
||||
|
||||
if (!user.muted) {
|
||||
logger.info({
|
||||
logCode: 'audiomanager_mute_audio',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
}, 'User wants to leave conference. Microphone muted');
|
||||
AudioManager.setSenderTrackEnabled(false);
|
||||
makeCall('toggleVoice');
|
||||
}
|
||||
};
|
||||
|
||||
const isVoiceUser = () => {
|
||||
const voiceUser = VoiceUsers.findOne({ intId: Auth.userID },
|
||||
{ fields: { joined: 1 } });
|
||||
@ -133,6 +148,7 @@ export default {
|
||||
updateAudioConstraints:
|
||||
(constraints) => AudioManager.updateAudioConstraints(constraints),
|
||||
recoverMicState,
|
||||
muteMicrophone: () => muteMicrophone(),
|
||||
isReconnecting: () => AudioManager.isReconnecting,
|
||||
setBreakoutAudioTransferStatus: (status) => AudioManager
|
||||
.setBreakoutAudioTransferStatus(status),
|
||||
|
@ -115,14 +115,15 @@ class MessageForm extends PureComponent {
|
||||
maxMessageLength,
|
||||
} = this.props;
|
||||
|
||||
const message = e.target.value;
|
||||
let message = e.target.value;
|
||||
let error = null;
|
||||
|
||||
if (message.length > maxMessageLength) {
|
||||
error = intl.formatMessage(
|
||||
messages.errorMaxMessageLength,
|
||||
{ 0: message.length - maxMessageLength },
|
||||
{ 0: maxMessageLength },
|
||||
);
|
||||
message = message.substring(0, maxMessageLength);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -227,8 +227,9 @@ class MessageForm extends PureComponent {
|
||||
if (message.length > maxMessageLength) {
|
||||
error = intl.formatMessage(
|
||||
messages.errorMaxMessageLength,
|
||||
{ 0: message.length - maxMessageLength },
|
||||
{ 0: maxMessageLength },
|
||||
);
|
||||
message = message.substring(0, maxMessageLength);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -322,7 +322,8 @@ class TimeWindowChatItem extends PureComponent {
|
||||
<Styled.PollIcon iconName="download" />
|
||||
</UserAvatar>
|
||||
</Styled.AvatarWrapper>
|
||||
<Styled.Content>
|
||||
<Styled.Content
|
||||
data-test="downloadPresentationContainer">
|
||||
<Styled.Meta>
|
||||
<Styled.Time dateTime={dateTime} style={{ margin: 0 }}>
|
||||
<FormattedTime value={dateTime} />
|
||||
|
@ -6,7 +6,7 @@ import Styled from './styles';
|
||||
|
||||
const messages = defineMessages({
|
||||
yesLabel: {
|
||||
id: 'app.endMeeting.yesLabel',
|
||||
id: 'app.confirmationModal.yesLabel',
|
||||
description: 'confirm button label',
|
||||
},
|
||||
noLabel: {
|
||||
@ -46,6 +46,7 @@ class ConfirmationModal extends Component {
|
||||
titleMessageExtra,
|
||||
checkboxMessageId,
|
||||
confirmButtonColor,
|
||||
confirmButtonLabel,
|
||||
confirmButtonDataTest,
|
||||
confirmParam,
|
||||
disableConfirmButton,
|
||||
@ -86,7 +87,7 @@ class ConfirmationModal extends Component {
|
||||
<Styled.Footer>
|
||||
<Styled.ConfirmationButton
|
||||
color={confirmButtonColor}
|
||||
label={intl.formatMessage(messages.yesLabel)}
|
||||
label={confirmButtonLabel ? confirmButtonLabel : intl.formatMessage(messages.yesLabel)}
|
||||
disabled={disableConfirmButton}
|
||||
data-test={confirmButtonDataTest}
|
||||
onClick={() => {
|
||||
|
@ -46,15 +46,15 @@ class Header extends React.Component {
|
||||
|
||||
return (
|
||||
<Styled.Header
|
||||
hideBorder={hideBorder}
|
||||
headerOnTop={headerOnTop}
|
||||
innerHeader={innerHeader}
|
||||
$hideBorder={hideBorder}
|
||||
$headerOnTop={headerOnTop}
|
||||
$innerHeader={innerHeader}
|
||||
{...other}
|
||||
>
|
||||
<Styled.Title
|
||||
hasMarginBottom={innerHeader}
|
||||
headerOnTop={headerOnTop}
|
||||
innerHeader={innerHeader}
|
||||
$hasMarginBottom={innerHeader}
|
||||
$headerOnTop={headerOnTop}
|
||||
$innerHeader={innerHeader}
|
||||
>
|
||||
{children}
|
||||
</Styled.Title>
|
||||
@ -65,8 +65,8 @@ class Header extends React.Component {
|
||||
circle
|
||||
hideLabel
|
||||
aria-describedby="modalDismissDescription"
|
||||
headerOnTop={headerOnTop}
|
||||
innerHeader={innerHeader}
|
||||
$headerOnTop={headerOnTop}
|
||||
$innerHeader={innerHeader}
|
||||
{...closeButtonProps}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -24,17 +24,17 @@ const Header = styled.header`
|
||||
border: none;
|
||||
display: grid;
|
||||
|
||||
${({ headerOnTop }) => headerOnTop && `
|
||||
grid-template-columns: auto max-content;
|
||||
${({ $headerOnTop }) => $headerOnTop && `
|
||||
grid-template-columns: auto min-content;
|
||||
grid-template-rows: min-content;
|
||||
`}
|
||||
|
||||
${({ innerHeader }) => innerHeader && `
|
||||
${({ $innerHeader }) => $innerHeader && `
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: min-content min-content;
|
||||
`}
|
||||
|
||||
${({ hideBorder }) => !hideBorder && `
|
||||
${({ $hideBorder }) => !$hideBorder && `
|
||||
padding: calc(${lineHeightComputed} / 2) 0;
|
||||
border-bottom: ${borderSize} solid ${colorGrayLighter};
|
||||
`}
|
||||
@ -55,15 +55,15 @@ const Title = styled(TitleElipsis)`
|
||||
padding: 0 ${mdPaddingX};
|
||||
}
|
||||
|
||||
${({ headerOnTop }) => headerOnTop && `
|
||||
${({ $headerOnTop }) => $headerOnTop && `
|
||||
grid-area: 1 / 1 / 2 / 3;
|
||||
`}
|
||||
|
||||
${({ innerHeader }) => innerHeader && `
|
||||
grid-area: 2 / 1 / 3 / 3;
|
||||
${({ $innerHeader }) => $innerHeader && `
|
||||
grid-area: 2 / 1 / 3 / 2;
|
||||
`}
|
||||
|
||||
${({ hasMarginBottom }) => hasMarginBottom && `
|
||||
${({ $hasMarginBottom }) => $hasMarginBottom && `
|
||||
margin-bottom: ${mdPaddingX};
|
||||
`}
|
||||
`;
|
||||
@ -76,12 +76,12 @@ const DismissButton = styled(Button)`
|
||||
& > i { color: ${colorText}; }
|
||||
}
|
||||
|
||||
${({ headerOnTop }) => headerOnTop && `
|
||||
${({ $headerOnTop }) => $headerOnTop && `
|
||||
grid-area: 1 / 2 / 2 / 3;
|
||||
`}
|
||||
|
||||
${({ innerHeader }) => innerHeader && `
|
||||
grid-area: 1 / 1 / 2 / 3;
|
||||
${({ $innerHeader }) => $innerHeader && `
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
`}
|
||||
|
||||
justify-self: end;
|
||||
|
@ -47,6 +47,7 @@ class ModalSimple extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
intl,
|
||||
title,
|
||||
hideBorder,
|
||||
@ -79,6 +80,7 @@ class ModalSimple extends Component {
|
||||
|
||||
return (
|
||||
<Styled.SimpleModal
|
||||
id="simpleModal"
|
||||
isOpen={modalisOpen}
|
||||
className={className}
|
||||
onRequestClose={handleRequestClose}
|
||||
|
@ -1,7 +1,7 @@
|
||||
.tippy-tooltip.bbbtip-theme{
|
||||
color:#fff;
|
||||
background-color:#333333;
|
||||
padding: .25rem;
|
||||
padding: .25rem .5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
@ -65,11 +65,14 @@ class Tooltip extends Component {
|
||||
aria: null,
|
||||
allowHTML: false,
|
||||
animation: animations ? DEFAULT_ANIMATION : ANIMATION_NONE,
|
||||
appendTo: document.body,
|
||||
arrow: roundArrow,
|
||||
boundary: 'window',
|
||||
content: title,
|
||||
delay: animations ? ANIMATION_DELAY : [ANIMATION_DELAY[0], 0],
|
||||
duration: animations ? ANIMATION_DURATION : 0,
|
||||
interactive: true,
|
||||
interactiveBorder: 10,
|
||||
onShow: this.onShow,
|
||||
onHide: this.onHide,
|
||||
offset: TIP_OFFSET,
|
||||
@ -83,7 +86,7 @@ class Tooltip extends Component {
|
||||
|
||||
componentDidUpdate() {
|
||||
const { animations } = Settings.application;
|
||||
const { title, fullscreen } = this.props;
|
||||
const { title } = this.props;
|
||||
const elements = document.querySelectorAll('[id^="tippy-"]');
|
||||
|
||||
Array.from(elements).filter((e) => {
|
||||
@ -107,7 +110,7 @@ class Tooltip extends Component {
|
||||
});
|
||||
|
||||
const elem = document.getElementById(this.tippySelectorId);
|
||||
const opts = { content: title, appendTo: fullscreen || document.body };
|
||||
const opts = { content: title, appendTo: document.body };
|
||||
if (elem && elem._tippy) elem._tippy.setProps(opts);
|
||||
}
|
||||
|
||||
|
@ -158,8 +158,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
|
||||
this.help = Service.getHelp();
|
||||
this.state = {
|
||||
selectedTab: '1',
|
||||
dataPage: '1',
|
||||
selectedTab: 0,
|
||||
dataSaving: props.dataSaving,
|
||||
hasNetworkData: false,
|
||||
copyButtonText: intl.formatMessage(intlMessages.copy),
|
||||
@ -187,6 +186,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
|
||||
this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
|
||||
this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
|
||||
this.handleSelectTab = this.handleSelectTab.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
@ -197,12 +197,24 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
Meteor.clearInterval(this.rateInterval);
|
||||
}
|
||||
|
||||
handleSelectTab(tab) {
|
||||
this.setState({
|
||||
selectedTab: tab,
|
||||
});
|
||||
}
|
||||
|
||||
handleDataSavingChange(key) {
|
||||
const { dataSaving } = this.state;
|
||||
dataSaving[key] = !dataSaving[key];
|
||||
this.setState(dataSaving);
|
||||
}
|
||||
|
||||
setButtonMessage(msg) {
|
||||
this.setState({
|
||||
copyButtonText: msg,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring the network data.
|
||||
* @return {Promise} A Promise that resolves when process started.
|
||||
@ -262,6 +274,43 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
}, NETWORK_MONITORING_INTERVAL_MS);
|
||||
}
|
||||
|
||||
displaySettingsStatus(status) {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Styled.ToggleLabel>
|
||||
{status ? intl.formatMessage(intlMessages.on)
|
||||
: intl.formatMessage(intlMessages.off)}
|
||||
</Styled.ToggleLabel>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy network data to clipboard
|
||||
* @return {Promise} A Promise that is resolved after data is copied.
|
||||
*
|
||||
*
|
||||
*/
|
||||
async copyNetworkData() {
|
||||
const { intl } = this.props;
|
||||
const {
|
||||
networkData,
|
||||
hasNetworkData,
|
||||
} = this.state;
|
||||
|
||||
if (!hasNetworkData) return;
|
||||
|
||||
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
|
||||
|
||||
const data = JSON.stringify(networkData, null, 2);
|
||||
|
||||
await navigator.clipboard.writeText(data);
|
||||
|
||||
this.copyNetworkDataTimeout = setTimeout(() => {
|
||||
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
|
||||
}, MIN_TIMEOUT);
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
const { intl } = this.props;
|
||||
|
||||
@ -278,52 +327,6 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
displaySettingsStatus(status) {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Styled.ToggleLabel>
|
||||
{status ? intl.formatMessage(intlMessages.on)
|
||||
: intl.formatMessage(intlMessages.off)}
|
||||
</Styled.ToggleLabel>
|
||||
);
|
||||
}
|
||||
|
||||
setButtonMessage(msg) {
|
||||
this.setState({
|
||||
copyButtonText: msg,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy network data to clipboard
|
||||
* @param {Object} e Event object from click event
|
||||
* @return {Promise} A Promise that is resolved after data is copied.
|
||||
*
|
||||
*
|
||||
*/
|
||||
async copyNetworkData(e) {
|
||||
const { intl } = this.props;
|
||||
const {
|
||||
networkData,
|
||||
hasNetworkData,
|
||||
} = this.state;
|
||||
|
||||
if (!hasNetworkData) return;
|
||||
|
||||
const { target: copyButton } = e;
|
||||
|
||||
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
|
||||
|
||||
const data = JSON.stringify(networkData, null, 2);
|
||||
|
||||
await navigator.clipboard.writeText(data);
|
||||
|
||||
this.copyNetworkDataTimeout = setTimeout(() => {
|
||||
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
|
||||
}, MIN_TIMEOUT);
|
||||
}
|
||||
|
||||
renderConnections() {
|
||||
const {
|
||||
connectionStatus,
|
||||
@ -345,7 +348,7 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
|
||||
return (
|
||||
<Styled.Item
|
||||
key={index}
|
||||
key={`${conn?.name}-${dateTime}`}
|
||||
last={(index + 1) === connections.length}
|
||||
data-test="connectionStatusItemUser"
|
||||
>
|
||||
@ -507,43 +510,17 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaginationClick(action) {
|
||||
if (action === 'next') {
|
||||
this.setState({ dataPage: '2' });
|
||||
}
|
||||
else {
|
||||
this.setState({ dataPage: '1' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.NetworkDataContainer data-test="networkDataContainer">
|
||||
<Styled.Prev>
|
||||
<Styled.ButtonLeft
|
||||
role="button"
|
||||
disabled={dataPage === '1'}
|
||||
aria-label={`${intl.formatMessage(intlMessages.prev)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
|
||||
onClick={handlePaginationClick.bind(this, 'prev')}
|
||||
>
|
||||
<Styled.Chevron
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</Styled.Chevron>
|
||||
</Styled.ButtonLeft>
|
||||
</Styled.Prev>
|
||||
<Styled.Helper page={dataPage}>
|
||||
<ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
|
||||
</Styled.Helper>
|
||||
<Styled.NetworkDataContent page={dataPage}>
|
||||
<Styled.NetworkDataContainer
|
||||
data-test="networkDataContainer"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Styled.HelperWrapper>
|
||||
<Styled.Helper>
|
||||
<ConnectionStatusHelper closeModal={() => closeModal(dataSaving, intl)} />
|
||||
</Styled.Helper>
|
||||
</Styled.HelperWrapper>
|
||||
<Styled.NetworkDataContent>
|
||||
<Styled.DataColumn>
|
||||
<Styled.NetworkData>
|
||||
<div>{`${audioUploadLabel}`}</div>
|
||||
@ -582,28 +559,6 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
</Styled.NetworkData>
|
||||
</Styled.DataColumn>
|
||||
</Styled.NetworkDataContent>
|
||||
<Styled.Next>
|
||||
<Styled.ButtonRight
|
||||
role="button"
|
||||
disabled={dataPage === '2'}
|
||||
aria-label={`${intl.formatMessage(intlMessages.next)} ${intl.formatMessage(intlMessages.ariaTitle)}`}
|
||||
onClick={handlePaginationClick.bind(this, 'next')}
|
||||
>
|
||||
<Styled.Chevron
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</Styled.Chevron>
|
||||
</Styled.ButtonRight>
|
||||
</Styled.Next>
|
||||
</Styled.NetworkDataContainer>
|
||||
);
|
||||
}
|
||||
@ -619,81 +574,23 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { intl } = this.props;
|
||||
|
||||
const { hasNetworkData } = this.state;
|
||||
const { hasNetworkData, copyButtonText } = this.state;
|
||||
return (
|
||||
<Styled.CopyContainer aria-live="polite">
|
||||
<Styled.Copy
|
||||
disabled={!hasNetworkData}
|
||||
role="button"
|
||||
data-test="copyStats"
|
||||
data-test="copyStats"
|
||||
onClick={this.copyNetworkData.bind(this)}
|
||||
onKeyPress={this.copyNetworkData.bind(this)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{this.state.copyButtonText}
|
||||
{copyButtonText}
|
||||
</Styled.Copy>
|
||||
</Styled.CopyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The navigation bar.
|
||||
* @returns {Object} The component to be renderized.
|
||||
*/
|
||||
renderNavigation() {
|
||||
const { intl } = this.props;
|
||||
|
||||
const handleTabClick = (event) => {
|
||||
const activeTabElement = document.querySelector('.activeConnectionStatusTab');
|
||||
const { target } = event;
|
||||
|
||||
if (activeTabElement) {
|
||||
activeTabElement.classList.remove('activeConnectionStatusTab');
|
||||
}
|
||||
|
||||
target.classList.add('activeConnectionStatusTab');
|
||||
this.setState({
|
||||
selectedTab: target.dataset.tab,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.Navigation>
|
||||
<div
|
||||
data-tab="1"
|
||||
className="activeConnectionStatusTab"
|
||||
onClick={handleTabClick}
|
||||
onKeyDown={handleTabClick}
|
||||
role="button"
|
||||
>
|
||||
{intl.formatMessage(intlMessages.connectionStats)}
|
||||
</div>
|
||||
<div
|
||||
data-tab="2"
|
||||
onClick={handleTabClick}
|
||||
onKeyDown={handleTabClick}
|
||||
role="button"
|
||||
>
|
||||
{intl.formatMessage(intlMessages.myLogs)}
|
||||
</div>
|
||||
{Service.isModerator()
|
||||
&& (
|
||||
<div
|
||||
data-tab="3"
|
||||
onClick={handleTabClick}
|
||||
onKeyDown={handleTabClick}
|
||||
role="button"
|
||||
>
|
||||
{intl.formatMessage(intlMessages.sessionLogs)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Styled.Navigation>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
closeModal,
|
||||
@ -715,18 +612,43 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
{intl.formatMessage(intlMessages.title)}
|
||||
</Styled.Title>
|
||||
</Styled.Header>
|
||||
{this.renderNavigation()}
|
||||
<Styled.Main>
|
||||
<Styled.Body>
|
||||
{selectedTab === '1'
|
||||
? this.renderNetworkData()
|
||||
: this.renderConnections()
|
||||
|
||||
<Styled.ConnectionTabs
|
||||
onSelect={this.handleSelectTab}
|
||||
selectedIndex={selectedTab}
|
||||
>
|
||||
<Styled.ConnectionTabList>
|
||||
<Styled.ConnectionTabSelector selectedClassName="is-selected">
|
||||
<span id="connection-status-tab">{intl.formatMessage(intlMessages.title)}</span>
|
||||
</Styled.ConnectionTabSelector>
|
||||
<Styled.ConnectionTabSelector selectedClassName="is-selected">
|
||||
<span id="my-logs-tab">{intl.formatMessage(intlMessages.myLogs)}</span>
|
||||
</Styled.ConnectionTabSelector>
|
||||
{Service.isModerator()
|
||||
&& (
|
||||
<Styled.ConnectionTabSelector selectedClassName="is-selected">
|
||||
<span id="session-logs-tab">{intl.formatMessage(intlMessages.sessionLogs)}</span>
|
||||
</Styled.ConnectionTabSelector>
|
||||
)
|
||||
}
|
||||
</Styled.Body>
|
||||
{selectedTab === '1' &&
|
||||
this.renderCopyDataButton()
|
||||
</Styled.ConnectionTabList>
|
||||
<Styled.ConnectionTabPanel selectedClassName="is-selected">
|
||||
<div>
|
||||
{this.renderNetworkData()}
|
||||
{this.renderCopyDataButton()}
|
||||
</div>
|
||||
</Styled.ConnectionTabPanel>
|
||||
<Styled.ConnectionTabPanel selectedClassName="is-selected">
|
||||
<div>{this.renderConnections()}</div>
|
||||
</Styled.ConnectionTabPanel>
|
||||
{Service.isModerator()
|
||||
&& (
|
||||
<Styled.ConnectionTabPanel selectedClassName="is-selected">
|
||||
<div>{this.renderConnections()}</div>
|
||||
</Styled.ConnectionTabPanel>
|
||||
)
|
||||
}
|
||||
</Styled.Main>
|
||||
</Styled.ConnectionTabs>
|
||||
</Styled.Container>
|
||||
</Styled.ConnectionStatusModal>
|
||||
);
|
||||
|
@ -7,12 +7,13 @@ import {
|
||||
colorGrayLabel,
|
||||
colorGrayLightest,
|
||||
colorPrimary,
|
||||
colorWhite,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
smPaddingX,
|
||||
smPaddingY,
|
||||
mdPaddingY,
|
||||
lgPaddingY,
|
||||
lgPaddingX,
|
||||
titlePositionLeft,
|
||||
mdPaddingX,
|
||||
borderSizeLarge,
|
||||
@ -26,7 +27,14 @@ import {
|
||||
hasPhoneDimentions,
|
||||
mediumDown,
|
||||
hasPhoneWidth,
|
||||
smallOnly,
|
||||
} from '/imports/ui/stylesheets/styled-components/breakpoints';
|
||||
import {
|
||||
ScrollboxVertical,
|
||||
} from '/imports/ui/stylesheets/styled-components/scrollable';
|
||||
import {
|
||||
Tab, Tabs, TabList, TabPanel,
|
||||
} from 'react-tabs';
|
||||
|
||||
const Item = styled.div`
|
||||
display: flex;
|
||||
@ -169,10 +177,22 @@ const Label = styled.span`
|
||||
margin-bottom: ${lgPaddingY};
|
||||
`;
|
||||
|
||||
const NetworkDataContainer = styled.div`
|
||||
const NetworkDataContainer = styled(ScrollboxVertical)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
padding-bottom: 1.25rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,.5);
|
||||
}
|
||||
}
|
||||
|
||||
@media ${mediumDown} {
|
||||
justify-content: space-between;
|
||||
@ -202,6 +222,8 @@ const CopyContainer = styled.div`
|
||||
|
||||
const ConnectionStatusModal = styled(Modal)`
|
||||
padding: 1rem;
|
||||
height: 28rem;
|
||||
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
@ -262,6 +284,19 @@ const Copy = styled.span`
|
||||
`}
|
||||
`;
|
||||
|
||||
const HelperWrapper = styled.div`
|
||||
min-width: 12.5rem;
|
||||
height: 100%;
|
||||
|
||||
@media ${mediumDown} {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
scroll-snap-align: start;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const Helper = styled.div`
|
||||
width: 12.5rem;
|
||||
height: 100%;
|
||||
@ -271,12 +306,7 @@ const Helper = styled.div`
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media ${mediumDown} {
|
||||
${({ page }) => page === '1'
|
||||
? 'display: flex;'
|
||||
: 'display: none;'}
|
||||
}
|
||||
padding: .5rem;
|
||||
`;
|
||||
|
||||
const NetworkDataContent = styled.div`
|
||||
@ -286,9 +316,9 @@ const NetworkDataContent = styled.div`
|
||||
flex-grow: 1;
|
||||
|
||||
@media ${mediumDown} {
|
||||
${({ page }) => page === '2'
|
||||
? 'display: flex;'
|
||||
: 'display: none;'}
|
||||
flex: none;
|
||||
width: 100%;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -302,132 +332,103 @@ const DataColumn = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const Main = styled.div`
|
||||
height: 19.5rem;
|
||||
const ConnectionTabs = styled(Tabs)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-flow: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
flex-flow: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const Body = styled.div`
|
||||
padding: ${jumboPaddingY} 0;
|
||||
const ConnectionTabList = styled(TabList)`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Navigation = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: .5rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid ${colorOffWhite};
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
padding: 0;
|
||||
width: calc(100% / 3);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& :not(:last-child) {
|
||||
margin: 0;
|
||||
margin-right: ${lgPaddingX};
|
||||
}
|
||||
|
||||
.activeConnectionStatusTab {
|
||||
border: none;
|
||||
border-bottom: 2px solid ${colorPrimary};
|
||||
color: ${colorPrimary};
|
||||
}
|
||||
|
||||
& * {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
& :not(:last-child) {
|
||||
margin: 0;
|
||||
margin-left: ${lgPaddingX};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Prev = styled.div`
|
||||
display: none;
|
||||
margin: 0 .5rem 0 .25rem;
|
||||
|
||||
@media ${mediumDown} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
flex-flow: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media ${hasPhoneWidth} {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Next = styled(Prev)`
|
||||
margin: 0 .25rem 0 .5rem;
|
||||
|
||||
@media ${hasPhoneWidth} {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
flex: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media ${hasPhoneWidth} {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: .25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonLeft = styled(Button)`
|
||||
left: calc(50% - 2rem);
|
||||
const ConnectionTabPanel = styled(TabPanel)`
|
||||
display: none;
|
||||
margin: 0 0 0 1rem;
|
||||
height: 13rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: calc(50%);
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
@media ${smallOnly} {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonRight = styled(Button)`
|
||||
right: calc(50% - 2rem);
|
||||
|
||||
[dir="rtl"] & {
|
||||
right: calc(50%);
|
||||
}
|
||||
`;
|
||||
|
||||
const Chevron = styled.svg`
|
||||
const ConnectionTabSelector = styled(Tab)`
|
||||
display: flex;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-flow: row;
|
||||
font-size: 0.9rem;
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-start;
|
||||
border: none !important;
|
||||
padding: ${mdPaddingY} ${mdPaddingX};
|
||||
|
||||
[dir="rtl"] & {
|
||||
transform: rotate(180deg);
|
||||
border-radius: .2rem;
|
||||
cursor: pointer;
|
||||
margin-bottom: ${smPaddingY};
|
||||
align-items: center;
|
||||
flex-grow: 0;
|
||||
min-width: 0;
|
||||
|
||||
& > span {
|
||||
min-width: 0;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media ${smallOnly} {
|
||||
max-width: 100%;
|
||||
margin: 0 ${smPaddingX} 0 0;
|
||||
& > i {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 ${smPaddingX};
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
border-bottom: 2px solid ${colorWhite};
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border: none;
|
||||
color: ${colorPrimary};
|
||||
|
||||
span {
|
||||
border-bottom: 2px solid ${colorPrimary};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -461,14 +462,11 @@ export default {
|
||||
Copy,
|
||||
Helper,
|
||||
NetworkDataContent,
|
||||
Main,
|
||||
Body,
|
||||
Navigation,
|
||||
FullName,
|
||||
DataColumn,
|
||||
Prev,
|
||||
Next,
|
||||
ButtonLeft,
|
||||
ButtonRight,
|
||||
Chevron,
|
||||
HelperWrapper,
|
||||
ConnectionTabs,
|
||||
ConnectionTabList,
|
||||
ConnectionTabSelector,
|
||||
ConnectionTabPanel,
|
||||
};
|
||||
|
@ -20,6 +20,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.endMeeting.contentWarning',
|
||||
description: 'end meeting content warning',
|
||||
},
|
||||
confirmButtonLabel: {
|
||||
id: 'app.endMeeting.yesLabel',
|
||||
description: 'end meeting confirm button label',
|
||||
},
|
||||
});
|
||||
|
||||
const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app;
|
||||
@ -58,6 +62,7 @@ class EndMeetingComponent extends PureComponent {
|
||||
description={description}
|
||||
confirmButtonColor="danger"
|
||||
confirmButtonDataTest="confirmEndMeeting"
|
||||
confirmButtonLabel={intl.formatMessage(intlMessages.confirmButtonLabel)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -211,7 +211,8 @@ class JoinHandler extends Component {
|
||||
}, 'User successfully went through main.joinRouteHandler');
|
||||
} else {
|
||||
const e = new Error(response.message);
|
||||
if (!Session.get('codeError')) Session.set('errorMessageDescription', response.message);
|
||||
JoinHandler.setError('401');
|
||||
Session.set('errorMessageDescription', response.message);
|
||||
logger.error({
|
||||
logCode: 'joinhandler_component_joinroutehandler_error',
|
||||
extraInfo: {
|
||||
|
@ -202,6 +202,7 @@ const CustomLayout = (props) => {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
@ -142,6 +142,7 @@ const PresentationFocusLayout = (props) => {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
@ -138,6 +138,7 @@ const SmartLayout = (props) => {
|
||||
}, INITIAL_INPUT_STATE),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
@ -149,6 +149,7 @@ const VideoFocusLayout = (props) => {
|
||||
),
|
||||
});
|
||||
}
|
||||
Session.set('layoutReady', true);
|
||||
throttledCalculatesLayout();
|
||||
};
|
||||
|
||||
|
32
bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx
Executable file → Normal file
32
bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx
Executable file → Normal file
@ -11,7 +11,7 @@ import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import ShortcutHelpComponent from '/imports/ui/components/shortcut-help/component';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
|
||||
import { colorDanger } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import Styled from './styles';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
@ -293,21 +293,7 @@ class SettingsDropdown extends PureComponent {
|
||||
},
|
||||
);
|
||||
|
||||
if (allowedToEndMeeting && isMeteorConnected) {
|
||||
this.menuItems.push(
|
||||
{
|
||||
key: 'list-item-end-meeting',
|
||||
icon: 'application',
|
||||
label: intl.formatMessage(intlMessages.endMeetingLabel),
|
||||
description: intl.formatMessage(intlMessages.endMeetingDesc),
|
||||
onClick: () => mountModal(<EndMeetingConfirmationContainer />),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (allowLogoutSetting && isMeteorConnected) {
|
||||
const customStyles = { color: colorDanger };
|
||||
|
||||
this.menuItems.push(
|
||||
{
|
||||
key: 'list-item-logout',
|
||||
@ -315,12 +301,26 @@ class SettingsDropdown extends PureComponent {
|
||||
icon: 'logout',
|
||||
label: intl.formatMessage(intlMessages.leaveSessionLabel),
|
||||
description: intl.formatMessage(intlMessages.leaveSessionDesc),
|
||||
customStyles,
|
||||
onClick: () => this.leaveSession(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (allowedToEndMeeting && isMeteorConnected) {
|
||||
const customStyles = { background: colorDanger, color: colorWhite };
|
||||
|
||||
this.menuItems.push(
|
||||
{
|
||||
key: 'list-item-end-meeting',
|
||||
icon: 'application',
|
||||
label: intl.formatMessage(intlMessages.endMeetingLabel),
|
||||
description: intl.formatMessage(intlMessages.endMeetingDesc),
|
||||
customStyles,
|
||||
onClick: () => mountModal(<EndMeetingConfirmationContainer />),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.menuItems;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ const PadContent = ({
|
||||
<Styled.Iframe
|
||||
title="shared notes viewing mode"
|
||||
srcDoc={contentWithStyle}
|
||||
data-test="sharedNotesViewingMode"
|
||||
/>
|
||||
</Styled.Wrapper>
|
||||
);
|
||||
|
@ -570,7 +570,7 @@ class Poll extends Component {
|
||||
</span>
|
||||
</Styled.OptionWrapper>
|
||||
{!hasVal && type !== pollTypes.Response && error ? (
|
||||
<Styled.InputError>{error}</Styled.InputError>
|
||||
<Styled.InputError data-test="errorNoValueInput">{error}</Styled.InputError>
|
||||
) : (
|
||||
<Styled.ErrorSpacer> </Styled.ErrorSpacer>
|
||||
)}
|
||||
@ -677,7 +677,7 @@ class Poll extends Component {
|
||||
<Styled.ResponseArea>
|
||||
{defaultPoll && (
|
||||
<div>
|
||||
<Styled.PollCheckbox>
|
||||
<Styled.PollCheckbox data-test="allowMultiple">
|
||||
<Checkbox
|
||||
onChange={this.toggleIsMultipleResponse}
|
||||
checked={isMultipleResponse}
|
||||
@ -701,13 +701,13 @@ class Poll extends Component {
|
||||
onClick={() => this.handleAddOption()}
|
||||
/>
|
||||
)}
|
||||
<Styled.Row>
|
||||
<Styled.Col aria-hidden="true">
|
||||
<Styled.SectionHeading>
|
||||
<Styled.AnonymousRow>
|
||||
<Styled.AnonymousHeadingCol aria-hidden="true">
|
||||
<Styled.AnonymousHeading>
|
||||
{intl.formatMessage(intlMessages.secretPollLabel)}
|
||||
</Styled.SectionHeading>
|
||||
</Styled.Col>
|
||||
<Styled.Col>
|
||||
</Styled.AnonymousHeading>
|
||||
</Styled.AnonymousHeadingCol>
|
||||
<Styled.AnonymousToggleCol>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<Styled.Toggle>
|
||||
{this.displayToggleStatus(secretPoll)}
|
||||
@ -720,8 +720,8 @@ class Poll extends Component {
|
||||
data-test="anonymousPollBtn"
|
||||
/>
|
||||
</Styled.Toggle>
|
||||
</Styled.Col>
|
||||
</Styled.Row>
|
||||
</Styled.AnonymousToggleCol>
|
||||
</Styled.AnonymousRow>
|
||||
{secretPoll && (
|
||||
<Styled.PollParagraph>
|
||||
{intl.formatMessage(intlMessages.isSecretPollLabel)}
|
||||
@ -739,12 +739,12 @@ class Poll extends Component {
|
||||
return (
|
||||
<>
|
||||
<Styled.CustomInputRow>
|
||||
<Styled.Col aria-hidden="true">
|
||||
<Styled.SectionHeading>
|
||||
<Styled.CustomInputHeadingCol aria-hidden="true">
|
||||
<Styled.CustomInputHeading>
|
||||
{intl.formatMessage(intlMessages.customInputToggleLabel)}
|
||||
</Styled.SectionHeading>
|
||||
</Styled.Col>
|
||||
<Styled.Col>
|
||||
</Styled.CustomInputHeading>
|
||||
</Styled.CustomInputHeadingCol>
|
||||
<Styled.CustomInputToggleCol>
|
||||
<Styled.Toggle>
|
||||
{this.displayAutoOptionToggleStatus(customInput)}
|
||||
<Toggle
|
||||
@ -756,7 +756,7 @@ class Poll extends Component {
|
||||
data-test="autoOptioningPollBtn"
|
||||
/>
|
||||
</Styled.Toggle>
|
||||
</Styled.Col>
|
||||
</Styled.CustomInputToggleCol>
|
||||
</Styled.CustomInputRow>
|
||||
{customInput && (
|
||||
<Styled.PollParagraph>
|
||||
@ -946,7 +946,7 @@ class Poll extends Component {
|
||||
const { intl } = this.props;
|
||||
return (
|
||||
<Styled.NoSlidePanelContainer>
|
||||
<Styled.SectionHeading>
|
||||
<Styled.SectionHeading data-test="noPresentation">
|
||||
{intl.formatMessage(intlMessages.noPresentationSelected)}
|
||||
</Styled.SectionHeading>
|
||||
<Styled.PollButton
|
||||
|
@ -134,7 +134,7 @@ class LiveResult extends PureComponent {
|
||||
</Styled.Left>
|
||||
<Styled.Center>
|
||||
<Styled.BarShade style={calculatedWidth} />
|
||||
<Styled.BarVal>{obj.numVotes || 0}</Styled.BarVal>
|
||||
<Styled.BarVal data-test="numberOfVotes">{obj.numVotes || 0}</Styled.BarVal>
|
||||
</Styled.Center>
|
||||
<Styled.Right>
|
||||
{pctFotmatted}
|
||||
@ -191,7 +191,7 @@ class LiveResult extends PureComponent {
|
||||
return (
|
||||
<div>
|
||||
<Styled.Stats>
|
||||
{currentPollQuestion ? <Styled.Title>{currentPollQuestion}</Styled.Title> : null}
|
||||
{currentPollQuestion ? <Styled.Title data-test="currentPollQuestion">{currentPollQuestion}</Styled.Title> : null}
|
||||
<Styled.Status>
|
||||
{waiting
|
||||
? (
|
||||
|
@ -215,7 +215,7 @@ const Warning = styled.div`
|
||||
|
||||
const CustomInputRow = styled.div`
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
flex-flow: nowrap;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
@ -349,6 +349,31 @@ const ResponseArea = styled.div`
|
||||
flex-flow: column wrap;
|
||||
`;
|
||||
|
||||
const CustomInputHeading = styled(SectionHeading)`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const CustomInputHeadingCol = styled(Col)`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const CustomInputToggleCol = styled(Col)`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const AnonymousHeading = styled(CustomInputHeading)``;
|
||||
|
||||
const AnonymousHeadingCol = styled(CustomInputHeadingCol)``;
|
||||
|
||||
const AnonymousToggleCol = styled(CustomInputToggleCol)``;
|
||||
|
||||
const AnonymousRow = styled(Row)`
|
||||
flex-flow: nowrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default {
|
||||
ToggleLabel,
|
||||
PollOptionInput,
|
||||
@ -376,4 +401,11 @@ export default {
|
||||
Question,
|
||||
OptionWrapper,
|
||||
ResponseArea,
|
||||
CustomInputHeading,
|
||||
CustomInputHeadingCol,
|
||||
CustomInputToggleCol,
|
||||
AnonymousHeading,
|
||||
AnonymousHeadingCol,
|
||||
AnonymousToggleCol,
|
||||
AnonymousRow,
|
||||
};
|
||||
|
@ -249,7 +249,7 @@ class Polling extends Component {
|
||||
key={pollAnswer.id}
|
||||
>
|
||||
<td>
|
||||
<Styled.PollingCheckbox>
|
||||
<Styled.PollingCheckbox data-test="optionsAnswers">
|
||||
<Checkbox
|
||||
disabled={!isMeteorConnected}
|
||||
id={`answerInput${pollAnswer.key}`}
|
||||
@ -280,6 +280,7 @@ class Polling extends Component {
|
||||
label={intl.formatMessage(intlMessages.submitLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
|
||||
onClick={() => this.handleSubmit(poll.pollId)}
|
||||
data-test="submitAnswersMultiple"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -78,6 +78,7 @@ class Presentation extends PureComponent {
|
||||
isFullscreen: false,
|
||||
tldrawAPI: null,
|
||||
isPanning: false,
|
||||
tldrawIsMounting: true,
|
||||
};
|
||||
|
||||
this.currentPresentationToastId = null;
|
||||
@ -99,6 +100,7 @@ class Presentation extends PureComponent {
|
||||
this.onResize = () => setTimeout(this.handleResize.bind(this), 0);
|
||||
this.renderCurrentPresentationToast = this.renderCurrentPresentationToast.bind(this);
|
||||
this.setPresentationRef = this.setPresentationRef.bind(this);
|
||||
this.setTldrawIsMounting = this.setTldrawIsMounting.bind(this);
|
||||
Session.set('componentPresentationWillUnmount', false);
|
||||
}
|
||||
|
||||
@ -895,6 +897,10 @@ class Presentation extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
setTldrawIsMounting(value) {
|
||||
this.setState({ tldrawIsMounting: value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
userIsPresenter,
|
||||
@ -922,6 +928,7 @@ class Presentation extends PureComponent {
|
||||
localPosition,
|
||||
fitToWidth,
|
||||
zoom,
|
||||
tldrawIsMounting,
|
||||
} = this.state;
|
||||
|
||||
let viewBoxDimensions;
|
||||
@ -1010,7 +1017,7 @@ class Presentation extends PureComponent {
|
||||
}}
|
||||
>
|
||||
<Styled.VisuallyHidden id="currentSlideText">{slideContent}</Styled.VisuallyHidden>
|
||||
{this.renderPresentationMenu()}
|
||||
{!tldrawIsMounting && currentSlide && this.renderPresentationMenu()}
|
||||
<WhiteboardContainer
|
||||
whiteboardId={currentSlide?.id}
|
||||
podId={podId}
|
||||
@ -1027,19 +1034,22 @@ class Presentation extends PureComponent {
|
||||
zoomChanger={this.zoomChanger}
|
||||
fitToWidth={fitToWidth}
|
||||
zoomValue={zoom}
|
||||
setTldrawIsMounting={this.setTldrawIsMounting}
|
||||
/>
|
||||
{isFullscreen && <PollingContainer />}
|
||||
</div>
|
||||
<Styled.PresentationToolbar
|
||||
ref={(ref) => { this.refPresentationToolbar = ref; }}
|
||||
style={
|
||||
{
|
||||
width: containerWidth,
|
||||
{!tldrawIsMounting && (
|
||||
<Styled.PresentationToolbar
|
||||
ref={(ref) => { this.refPresentationToolbar = ref; }}
|
||||
style={
|
||||
{
|
||||
width: containerWidth,
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
{this.renderPresentationToolbar(svgWidth)}
|
||||
</Styled.PresentationToolbar>
|
||||
>
|
||||
{this.renderPresentationToolbar(svgWidth)}
|
||||
</Styled.PresentationToolbar>
|
||||
)}
|
||||
{/*this.renderPresentationToolbar()*/}
|
||||
</Styled.SvgContainer>
|
||||
</Styled.Presentation>
|
||||
|
@ -57,6 +57,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.presentationUploder.upload.413',
|
||||
description: 'error that file exceed the size limit',
|
||||
},
|
||||
IVALID_MIME_TYPE: {
|
||||
id: 'app.presentationUploder.conversion.invalidMimeType',
|
||||
description: 'warns user that the file\'s mime type is not supported or it doesn\'t match the extension',
|
||||
},
|
||||
PAGE_COUNT_EXCEEDED: {
|
||||
id: 'app.presentationUploder.conversion.pageCountExceeded',
|
||||
description: 'warns the user that the conversion failed because of the page count',
|
||||
@ -147,6 +151,10 @@ function renderPresentationItemStatus(item, intl) {
|
||||
case 'PDF_HAS_BIG_PAGE':
|
||||
constraint['0'] = (item.conversion.bigPageSize / 1000 / 1000).toFixed(2);
|
||||
break;
|
||||
case 'IVALID_MIME_TYPE':
|
||||
constraint['0'] = item.conversion.fileExtension;
|
||||
constraint['1'] = item.conversion.fileMime;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -187,7 +195,7 @@ function renderToastItem(item, intl) {
|
||||
|
||||
return (
|
||||
<Styled.UploadRow
|
||||
key={item.temporaryPresentationId}
|
||||
key={item.id || item.temporaryPresentationId}
|
||||
onClick={() => {
|
||||
if (hasError || isProcessing) Session.set('showUploadPresentationView', true);
|
||||
}}
|
||||
|
@ -274,6 +274,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.presentationUploader.sending',
|
||||
description: 'sending label',
|
||||
},
|
||||
collecting: {
|
||||
id: 'app.presentationUploader.collecting',
|
||||
description: 'collecting label',
|
||||
},
|
||||
processing: {
|
||||
id: 'app.presentationUploader.processing',
|
||||
description: 'processing label',
|
||||
},
|
||||
sent: {
|
||||
id: 'app.presentationUploader.sent',
|
||||
description: 'sent label',
|
||||
@ -286,6 +294,8 @@ const intlMessages = defineMessages({
|
||||
|
||||
const EXPORT_STATUSES = {
|
||||
RUNNING: 'RUNNING',
|
||||
COLLECTING: 'COLLECTING',
|
||||
PROCESSING: 'PROCESSING',
|
||||
TIMEOUT: 'TIMEOUT',
|
||||
EXPORTED: 'EXPORTED',
|
||||
};
|
||||
@ -473,7 +483,7 @@ class PresentationUploader extends Component {
|
||||
isCurrent: false,
|
||||
conversion: { done: false, error: false },
|
||||
upload: { done: false, error: false, progress: 0 },
|
||||
exportation: { isRunning: false, error: false },
|
||||
exportation: { error: false },
|
||||
onProgress: (event) => {
|
||||
if (!event.lengthComputable) {
|
||||
this.deepMergeUpdateFileKey(id, 'upload', {
|
||||
@ -679,8 +689,9 @@ class PresentationUploader extends Component {
|
||||
|
||||
const observer = (exportation) => {
|
||||
this.deepMergeUpdateFileKey(item.id, 'exportation', exportation);
|
||||
|
||||
if (exportation.status === EXPORT_STATUSES.RUNNING) {
|
||||
if ([EXPORT_STATUSES.RUNNING,
|
||||
EXPORT_STATUSES.COLLECTING,
|
||||
EXPORT_STATUSES.PROCESSING].includes(exportation.status)) {
|
||||
this.setState((prevState) => {
|
||||
prevState.presExporting.add(item.id);
|
||||
return {
|
||||
@ -699,12 +710,12 @@ class PresentationUploader extends Component {
|
||||
closeOnClick: true,
|
||||
onClose: () => {
|
||||
this.exportToastId = null;
|
||||
|
||||
const presToShow = this.getPresentationsToShow();
|
||||
const isAnyRunning = presToShow.some(
|
||||
(p) => p.exportation.status === EXPORT_STATUSES.RUNNING
|
||||
|| p.exportation.status === EXPORT_STATUSES.COLLECTING
|
||||
|| p.exportation.status === EXPORT_STATUSES.PROCESSING,
|
||||
);
|
||||
|
||||
if (!isAnyRunning) {
|
||||
this.setState({ presExporting: new Set() });
|
||||
}
|
||||
@ -821,7 +832,7 @@ class PresentationUploader extends Component {
|
||||
const presToShow = this.getPresentationsToShow();
|
||||
|
||||
const isAllExported = presToShow.every(
|
||||
(p) => p.exportation.status === EXPORT_STATUSES.EXPORTED
|
||||
(p) => p.exportation.status === EXPORT_STATUSES.EXPORTED,
|
||||
);
|
||||
const shouldDismiss = isAllExported && this.exportToastId;
|
||||
|
||||
@ -831,12 +842,13 @@ class PresentationUploader extends Component {
|
||||
if (presExporting.size) {
|
||||
this.setState({ presExporting: new Set() });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const presToShowSorted = [
|
||||
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.RUNNING),
|
||||
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.COLLECTING),
|
||||
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.PROCESSING),
|
||||
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.TIMEOUT),
|
||||
...presToShow.filter((p) => p.exportation.status === EXPORT_STATUSES.EXPORTED),
|
||||
];
|
||||
@ -846,7 +858,7 @@ class PresentationUploader extends Component {
|
||||
: 'exportToastHeaderPlural';
|
||||
|
||||
return (
|
||||
<Styled.ToastWrapper>
|
||||
<Styled.ToastWrapper data-test="downloadPresentationToast">
|
||||
<Styled.UploadToastHeader>
|
||||
<Styled.UploadIcon iconName="download" />
|
||||
<Styled.UploadToastTitle>
|
||||
@ -866,19 +878,27 @@ class PresentationUploader extends Component {
|
||||
|
||||
renderToastExportItem(item) {
|
||||
const { status } = item.exportation;
|
||||
const loading = status === EXPORT_STATUSES.RUNNING;
|
||||
const loading = (status === EXPORT_STATUSES.RUNNING
|
||||
|| status === EXPORT_STATUSES.COLLECTING
|
||||
|| status === EXPORT_STATUSES.PROCESSING);
|
||||
const done = status === EXPORT_STATUSES.EXPORTED;
|
||||
let icon;
|
||||
|
||||
switch (status) {
|
||||
case EXPORT_STATUSES.RUNNING:
|
||||
icon = 'blank'
|
||||
icon = 'blank';
|
||||
break;
|
||||
case EXPORT_STATUSES.COLLECTING:
|
||||
icon = 'blank';
|
||||
break;
|
||||
case EXPORT_STATUSES.PROCESSING:
|
||||
icon = 'blank';
|
||||
break;
|
||||
case EXPORT_STATUSES.EXPORTED:
|
||||
icon = 'check'
|
||||
icon = 'check';
|
||||
break;
|
||||
case EXPORT_STATUSES.TIMEOUT:
|
||||
icon = 'warning'
|
||||
icon = 'warning';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -886,7 +906,7 @@ class PresentationUploader extends Component {
|
||||
|
||||
return (
|
||||
<Styled.UploadRow
|
||||
key={item.temporaryPresentationId}
|
||||
key={item.id || item.temporaryPresentationId}
|
||||
>
|
||||
<Styled.FileLine>
|
||||
<span>
|
||||
@ -919,6 +939,12 @@ class PresentationUploader extends Component {
|
||||
switch (item.exportation.status) {
|
||||
case EXPORT_STATUSES.RUNNING:
|
||||
return intl.formatMessage(intlMessages.sending);
|
||||
case EXPORT_STATUSES.COLLECTING:
|
||||
return intl.formatMessage(intlMessages.collecting,
|
||||
{ 0: item.exportation.pageNumber, 1: item.exportation.totalPages });
|
||||
case EXPORT_STATUSES.PROCESSING:
|
||||
return intl.formatMessage(intlMessages.processing,
|
||||
{ 0: item.exportation.pageNumber, 1: item.exportation.totalPages });
|
||||
case EXPORT_STATUSES.TIMEOUT:
|
||||
return intl.formatMessage(intlMessages.exportingTimeout);
|
||||
case EXPORT_STATUSES.EXPORTED:
|
||||
|
@ -69,7 +69,7 @@ const getPresentations = () => Presentations
|
||||
isRemovable: removable,
|
||||
conversion: conversion || { done: true, error: false },
|
||||
uploadTimestamp,
|
||||
exportation: exportation || { isRunning: false, error: false },
|
||||
exportation: exportation || { error: false },
|
||||
};
|
||||
});
|
||||
|
||||
@ -107,7 +107,8 @@ const observePresentationConversion = (
|
||||
|
||||
if (doc.temporaryPresentationId !== temporaryPresentationId && doc.id !== tokenId) return;
|
||||
|
||||
if (doc.conversion.status === 'FILE_TOO_LARGE' || doc.conversion.status === 'UNSUPPORTED_DOCUMENT' || doc.conversion.status === 'CONVERSION_TIMEOUT') {
|
||||
if (doc.conversion.status === 'FILE_TOO_LARGE' || doc.conversion.status === 'UNSUPPORTED_DOCUMENT'
|
||||
|| doc.conversion.status === 'CONVERSION_TIMEOUT' || doc.conversion.status === "IVALID_MIME_TYPE") {
|
||||
Presentations.update({id: tokenId}, {$set: {temporaryPresentationId, renderedInToast: false}})
|
||||
onConversion(doc.conversion);
|
||||
c.stop();
|
||||
@ -362,7 +363,7 @@ const exportPresentationToChat = (presentationId, observer) => {
|
||||
const cursor = Presentations.find({ id: presentationId });
|
||||
|
||||
const checkStatus = (exportation) => {
|
||||
const shouldStop = lastStatus.status === 'RUNNING' && exportation.status !== 'RUNNING';
|
||||
const shouldStop = lastStatus.status === 'PROCESSING' && exportation.status === 'EXPORTED';
|
||||
|
||||
if (shouldStop) {
|
||||
observer(exportation, true);
|
||||
|
@ -51,7 +51,6 @@ const getCurrentSlide = (podId) => {
|
||||
fields: {
|
||||
meetingId: 0,
|
||||
thumbUri: 0,
|
||||
swfUri: 0,
|
||||
txtUri: 0,
|
||||
},
|
||||
});
|
||||
|
@ -353,6 +353,7 @@ class ApplicationMenu extends BaseMenu {
|
||||
defaultChecked={settings.darkTheme}
|
||||
onChange={() => this.handleToggle('darkTheme')}
|
||||
showToggleLabel={showToggleLabel}
|
||||
data-test="darkModeToggleBtn"
|
||||
/>
|
||||
</Styled.FormElementRight>
|
||||
</Styled.Col>
|
||||
|
@ -117,7 +117,7 @@ const messages = defineMessages({
|
||||
description: 'Directory lookup',
|
||||
},
|
||||
yesLabel: {
|
||||
id: 'app.endMeeting.yesLabel',
|
||||
id: 'app.confirmationModal.yesLabel',
|
||||
description: 'confirm button label',
|
||||
},
|
||||
noLabel: {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user