Merge tag 'v2.6.0-beta.2' into 26beta2-dev

This commit is contained in:
Ramón Souza 2022-12-08 10:11:13 -03:00
commit 3ec185ec4f
153 changed files with 4414 additions and 12202 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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"

View File

@ -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 =>

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -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);

View File

@ -21,4 +21,5 @@ package org.bigbluebutton.presentation;
public interface DocumentConversionService {
void processDocument(UploadedPresentation pres);
void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime, String fileExtension);
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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(),

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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))
}
}

View File

@ -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)

View File

@ -1,6 +1,7 @@
{
"log": {
"level": "info"
"level": "info",
"msgName": "PresAnnStatusMsg"
},
"shared": {
"presDir": "/var/bigbluebutton",

View 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);
};

View File

@ -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();
}

View File

@ -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}`);
}

View File

@ -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');

View File

@ -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();

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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();
});

View File

@ -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} />;

View File

@ -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();

View File

@ -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;

View File

@ -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';

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.6.0-beta.1
BIGBLUEBUTTON_RELEASE=2.6.0-beta.2

View File

@ -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

View File

@ -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);

View File

@ -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:

View File

@ -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,
});
}

View File

@ -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,

View File

@ -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}`);

View File

@ -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,

View File

@ -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()
}

View 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();
}

View File

@ -168,6 +168,7 @@ const AppContainer = (props) => {
shouldShowPresentation,
mountRandomUserModal,
isPresenter,
numCameras: cameraDockInput.numCameras,
}}
{...otherProps}
/>

View File

@ -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 = () => (

View File

@ -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;

View File

@ -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),

View File

@ -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({

View File

@ -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({

View File

@ -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} />

View File

@ -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={() => {

View File

@ -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}

View File

@ -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;

View File

@ -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}

View File

@ -1,7 +1,7 @@
.tippy-tooltip.bbbtip-theme{
color:#fff;
background-color:#333333;
padding: .25rem;
padding: .25rem .5rem;
border-radius: 4px;
}

View File

@ -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);
}

View File

@ -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>
);

View File

@ -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,
};

View File

@ -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)}
/>
);
}

View File

@ -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: {

View File

@ -202,6 +202,7 @@ const CustomLayout = (props) => {
}, INITIAL_INPUT_STATE),
});
}
Session.set('layoutReady', true);
throttledCalculatesLayout();
};

View File

@ -142,6 +142,7 @@ const PresentationFocusLayout = (props) => {
}, INITIAL_INPUT_STATE),
});
}
Session.set('layoutReady', true);
throttledCalculatesLayout();
};

View File

@ -138,6 +138,7 @@ const SmartLayout = (props) => {
}, INITIAL_INPUT_STATE),
});
}
Session.set('layoutReady', true);
throttledCalculatesLayout();
};

View File

@ -149,6 +149,7 @@ const VideoFocusLayout = (props) => {
),
});
}
Session.set('layoutReady', true);
throttledCalculatesLayout();
};

View 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;
}

View File

@ -19,6 +19,7 @@ const PadContent = ({
<Styled.Iframe
title="shared notes viewing mode"
srcDoc={contentWithStyle}
data-test="sharedNotesViewingMode"
/>
</Styled.Wrapper>
);

View File

@ -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>&nbsp;</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

View File

@ -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
? (

View File

@ -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,
};

View File

@ -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>

View File

@ -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>

View File

@ -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);
}}

View File

@ -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:

View File

@ -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);

View File

@ -51,7 +51,6 @@ const getCurrentSlide = (podId) => {
fields: {
meetingId: 0,
thumbUri: 0,
swfUri: 0,
txtUri: 0,
},
});

View File

@ -353,6 +353,7 @@ class ApplicationMenu extends BaseMenu {
defaultChecked={settings.darkTheme}
onChange={() => this.handleToggle('darkTheme')}
showToggleLabel={showToggleLabel}
data-test="darkModeToggleBtn"
/>
</Styled.FormElementRight>
</Styled.Col>

View File

@ -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