Merge pull request #17648 from GuiLeme/issue-17531

feat: Restore old downloading of original presentation
This commit is contained in:
Anton Georgiev 2023-05-18 15:40:01 -04:00 committed by GitHub
commit 1d3bdb5802
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 686 additions and 159 deletions

View File

@ -8,10 +8,11 @@ import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core.util.RandomStringGenerator
import org.bigbluebutton.core.models.{ PresentationPod, PresentationPage, PresentationInPod }
import org.bigbluebutton.core.models.{ PresentationInPod, PresentationPage, PresentationPod }
import java.io.File
trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
this: PresentationPodHdlrs =>
object JobTypes {
@ -40,20 +41,20 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildNewPresAnnFileAvailable(fileURI: String, presId: String): NewPresAnnFileAvailableMsg = {
val header = BbbClientMsgHeader(NewPresAnnFileAvailableMsg.NAME, "not-used", "not-used")
val body = NewPresAnnFileAvailableMsgBody(fileURI, presId)
def buildNewPresFileAvailable(fileURI: String, presId: String, typeOfExport: String): NewPresFileAvailableMsg = {
val header = BbbClientMsgHeader(NewPresFileAvailableMsg.NAME, "not-used", "not-used")
val body = NewPresFileAvailableMsgBody(fileURI, presId, typeOfExport)
NewPresAnnFileAvailableMsg(header, body)
NewPresFileAvailableMsg(header, body)
}
def buildBroadcastNewPresAnnFileAvailable(newPresAnnFileAvailableMsg: NewPresAnnFileAvailableMsg, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
def buildBroadcastNewPresFileAvailable(newPresFileAvailableMsg: NewPresFileAvailableMsg, 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(NewPresAnnFileAvailableEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
val body = NewPresAnnFileAvailableEvtMsgBody(fileURI = newPresAnnFileAvailableMsg.body.fileURI, presId = newPresAnnFileAvailableMsg.body.presId)
val event = NewPresAnnFileAvailableEvtMsg(header, body)
val header = BbbClientMsgHeader(NewPresFileAvailableEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
val body = NewPresFileAvailableEvtMsgBody(fileURI = newPresFileAvailableMsg.body.fileURI, presId = newPresFileAvailableMsg.body.presId,
typeOfExport = newPresFileAvailableMsg.body.typeOfExport)
val event = NewPresFileAvailableEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
@ -112,7 +113,7 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
}
}
def handle(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def handle(m: MakePresentationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val meetingId = liveMeeting.props.meetingProp.intId
val userId = m.header.userId
@ -146,8 +147,9 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
val annotationCount: Int = storeAnnotationPages.map(_.annotations.size).sum
val isOriginalPresentationType = m.body.typeOfExport == "Original"
if (annotationCount > 0) {
if (!isOriginalPresentationType && annotationCount > 0) {
// Send Export Job to Redis
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
@ -155,15 +157,18 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
// Send Annotations to Redis
val annotations = StoredAnnotations(jobId, presId, storeAnnotationPages)
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
} else {
} else if (!isOriginalPresentationType && annotationCount == 0) {
log.error("There are no annotations for presentation with Id {}... Ignoring", presId)
} else if (isOriginalPresentationType) {
// Return existing uploaded file directly
val filename = currentPres.get.name
val convertedFileName = currentPres.get.filenameConverted
val filename = if (convertedFileName == "") currentPres.get.name else convertedFileName
val presFilenameExt = filename.split("\\.").last
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename)
val fileURI = List("bigbluebutton", "presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}").mkString(File.separator, File.separator, "")
val event = buildNewPresAnnFileAvailable(fileURI, presId)
val fileURI = List("bigbluebutton", "presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}&filename=${filename}").mkString(File.separator, File.separator, "")
val event = buildNewPresFileAvailable(fileURI, presId, m.body.typeOfExport)
handle(event, liveMeeting, bus)
}
@ -221,9 +226,9 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
}
}
def handle(m: NewPresAnnFileAvailableMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
log.info("Received NewPresAnnFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI)
bus.outGW.send(buildBroadcastNewPresAnnFileAvailable(m, liveMeeting))
def handle(m: NewPresFileAvailableMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
log.info("Received NewPresFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI)
bus.outGW.send(buildBroadcastNewPresFileAvailable(m, liveMeeting))
}
def handle(m: CaptureSharedNotesReqInternalMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {

View File

@ -3,6 +3,7 @@ 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.models.PresentationInPod
import org.bigbluebutton.core.running.LiveMeeting
trait PresentationConversionCompletedSysPubMsgHdlr {
@ -22,8 +23,7 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
pres <- pod.getPresentation(msg.body.presentation.id)
} yield {
val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres, temporaryPresentationId,
msg.body.presentation.isInitialPresentation)
msg.body.presentation.isInitialPresentation, msg.body.presentation.filenameConverted)
PresentationSender.broadcastPresentationConversionCompletedEvtMsg(
bus,
meetingId,
@ -31,7 +31,7 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
msg.header.userId,
msg.body.messageKey,
msg.body.code,
presVO
presVO,
)
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(
bus,
@ -43,8 +43,10 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
pres.name
)
val presWithConvertedName = PresentationInPod(pres.id, pres.name, pres.current, pres.pages,
pres.downloadable, pres.removable, msg.body.presentation.filenameConverted)
var pods = state.presentationPodManager.addPod(pod)
pods = pods.addPresentationToPod(pod.id, pres)
pods = pods.addPresentationToPod(pod.id, presWithConvertedName)
state.update(pods)
}

View File

@ -19,7 +19,7 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
with PresentationPageCountErrorPubMsgHdlr
with PresentationUploadedFileTooLargeErrorPubMsgHdlr
with PresentationUploadTokenReqMsgHdlr
with PresentationWithAnnotationsMsgHdlr
with MakePresentationDownloadReqMsgHdlr
with ResizeAndMovePagePubMsgHdlr
with SyncGetPresentationPodsMsgHdlr
with RemovePresentationPodPubMsgHdlr

View File

@ -58,7 +58,7 @@ object PresentationPodsApp {
}
PresentationVO(p.id, "", p.name, p.current,
pages.toVector, p.downloadable, p.removable, false)
pages.toVector, p.downloadable, p.removable, false, "")
}
PresentationPodVO(pod.id, pod.currentPresenter, presentationVOs.toVector)
@ -74,7 +74,7 @@ object PresentationPodsApp {
}
def translatePresentationToPresentationVO(pres: PresentationInPod, temporaryPresentationId: String,
isInitialPresentation: Boolean): PresentationVO = {
isInitialPresentation: Boolean, filenameConverted: String): PresentationVO = {
val pages = pres.pages.values.map { page =>
PageVO(
id = page.id,
@ -89,8 +89,8 @@ object PresentationPodsApp {
heightRatio = page.heightRatio
)
}
PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable,
isInitialPresentation)
PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable,
pres.removable, isInitialPresentation, filenameConverted)
}
def setCurrentPresentationInPod(state: MeetingState2x, podId: String, nextCurrentPresId: String): Option[PresentationPod] = {

View File

@ -11,7 +11,7 @@ object PresentationSender {
podId: String, userId: String,
presentationId: String,
downloadable: Boolean,
presFilename: String
presFilename: String,
): Unit = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
@ -30,7 +30,7 @@ object PresentationSender {
bus: MessageBus,
meetingId: String,
podId: String, userId: String, messageKey: String,
code: String, presentation: PresentationVO
code: String, presentation: PresentationVO,
): Unit = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,

View File

@ -62,6 +62,7 @@ case class PresentationInPod(
pages: scala.collection.immutable.Map[String, PresentationPage],
downloadable: Boolean,
removable: Boolean,
filenameConverted: String = "",
)
object PresentationPod {

View File

@ -314,10 +314,10 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[PdfConversionInvalidErrorSysPubMsg](envelope, jsonNode)
case AssignPresenterReqMsg.NAME =>
routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode)
case MakePresentationWithAnnotationDownloadReqMsg.NAME =>
routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode)
case NewPresAnnFileAvailableMsg.NAME =>
routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode)
case MakePresentationDownloadReqMsg.NAME =>
routeGenericMsg[MakePresentationDownloadReqMsg](envelope, jsonNode)
case NewPresFileAvailableMsg.NAME =>
routeGenericMsg[NewPresFileAvailableMsg](envelope, jsonNode)
case PresAnnStatusMsg.NAME =>
routeGenericMsg[PresAnnStatusMsg](envelope, jsonNode)

View File

@ -505,8 +505,8 @@ class MeetingActor(
// Presentation
case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus)
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: MakePresentationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: NewPresFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
case m: PresAnnStatusMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
case m: PadCapturePubMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)

View File

@ -117,8 +117,8 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
//case m: PresentationPageConvertedEventMsg => logMessage(msg)
// case m: StoreAnnotationsInRedisSysMsg => logMessage(msg)
// case m: StoreExportJobInRedisSysMsg => logMessage(msg)
case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg)
case m: NewPresAnnFileAvailableMsg => logMessage(msg)
case m: MakePresentationDownloadReqMsg => logMessage(msg)
case m: NewPresFileAvailableMsg => logMessage(msg)
case m: PresentationPageConversionStartedSysMsg => logMessage(msg)
case m: PresentationConversionEndedSysMsg => logMessage(msg)
case m: PresentationConversionRequestReceivedSysMsg => logMessage(msg)

View File

@ -2,7 +2,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,
isInitialPresentation: Boolean)
isInitialPresentation: Boolean, filenameConverted: String)
case class PageVO(id: String, num: Int, thumbUri: String = "",
txtUri: String, svgUri: String, current: Boolean = false, xOffset: Double = 0,

View File

@ -10,13 +10,13 @@ object PreuploadedPresentationsSysPubMsg { val NAME = "PreuploadedPresentationsS
case class PreuploadedPresentationsSysPubMsg(header: BbbClientMsgHeader, body: PreuploadedPresentationsSysPubMsgBody) extends StandardMsg
case class PreuploadedPresentationsSysPubMsgBody(presentations: Vector[PresentationVO])
object MakePresentationWithAnnotationDownloadReqMsg { val NAME = "MakePresentationWithAnnotationDownloadReqMsg" }
case class MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) extends StandardMsg
case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int])
object MakePresentationDownloadReqMsg { val NAME = "MakePresentationDownloadReqMsg" }
case class MakePresentationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationDownloadReqMsgBody) extends StandardMsg
case class MakePresentationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int], typeOfExport: String)
object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" }
case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg
case class NewPresAnnFileAvailableMsgBody(fileURI: String, presId: String)
object NewPresFileAvailableMsg { val NAME = "NewPresFileAvailableMsg" }
case class NewPresFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresFileAvailableMsgBody) extends StandardMsg
case class NewPresFileAvailableMsgBody(fileURI: String, presId: String, typeOfExport: String)
object PresAnnStatusMsg { val NAME = "PresAnnStatusMsg" }
case class PresAnnStatusMsg(header: BbbClientMsgHeader, body: PresAnnStatusMsgBody) extends StandardMsg
@ -37,9 +37,9 @@ object NewPresentationEvtMsg { val NAME = "NewPresentationEvtMsg" }
case class NewPresentationEvtMsg(header: BbbClientMsgHeader, body: NewPresentationEvtMsgBody) extends BbbCoreMsg
case class NewPresentationEvtMsgBody(presentation: PresentationVO)
object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" }
case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg
case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String)
object NewPresFileAvailableEvtMsg { val NAME = "NewPresFileAvailableEvtMsg" }
case class NewPresFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresFileAvailableEvtMsgBody) extends BbbCoreMsg
case class NewPresFileAvailableEvtMsgBody(fileURI: String, presId: String, typeOfExport: String)
object PresAnnStatusEvtMsg { val NAME = "PresAnnStatusEvtMsg" }
case class PresAnnStatusEvtMsg(header: BbbClientMsgHeader, body: PresAnnStatusEvtMsgBody) extends BbbCoreMsg

View File

@ -121,6 +121,8 @@ public final class Util {
File downloadMarker = Util.getPresFileDownloadMarker(presFileDir, presId);
if (downloadable && downloadMarker != null && ! downloadMarker.exists()) {
downloadMarker.createNewFile();
} else if (!downloadable && downloadMarker != null && downloadMarker.exists()) {
downloadMarker.delete();
}
}
}

View File

@ -21,6 +21,7 @@ package org.bigbluebutton.presentation;
import java.io.File;
import java.util.ArrayList;
import org.apache.commons.io.FilenameUtils;
public final class UploadedPresentation {
private final String podId;
@ -34,6 +35,7 @@ public final class UploadedPresentation {
private String fileType = "unknown";
private int numberOfPages = 0;
private String conversionStatus;
private String filenameConverted;
private final String baseUrl;
private boolean isDownloadable = false;
private boolean isRemovable = true;
@ -212,4 +214,27 @@ public final class UploadedPresentation {
public boolean getIsInitialPresentation() {
return isInitialPresentation;
}
public String getFilenameConverted() {
if (filenameConverted != null) {
return filenameConverted;
} else {
return "";
}
}
public void generateFilenameConverted(String newExtension) {
String nameWithoutExtension = FilenameUtils.removeExtension(name);
this.filenameConverted = nameWithoutExtension.concat("." + newExtension);
}
public void deleteOriginalFile() {
String pathToFileWithoutExtension = FilenameUtils.removeExtension(uploadedFile.getPath());
String newExtension = FilenameUtils.getExtension(uploadedFile.getPath());
String originalExtension = FilenameUtils.getExtension(name);
if (!originalExtension.equals("pdf") && newExtension.equals("pdf")) {
File originalFile = new File(pathToFileWithoutExtension + "." + originalExtension);
originalFile.delete();
}
}
}

View File

@ -3,10 +3,7 @@ package org.bigbluebutton.presentation.imp;
import com.google.gson.Gson;
import org.bigbluebutton.api.Util;
import org.bigbluebutton.presentation.*;
import org.bigbluebutton.presentation.messages.DocPageConversionStarted;
import org.bigbluebutton.presentation.messages.DocPageCountExceeded;
import org.bigbluebutton.presentation.messages.DocPageCountFailed;
import org.bigbluebutton.presentation.messages.PresentationConvertMessage;
import org.bigbluebutton.presentation.messages.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -76,6 +73,7 @@ public class PresentationFileProcessor {
private void processUploadedPresentation(UploadedPresentation pres) {
if (SupportedFileTypes.isPdfFile(pres.getFileType())) {
pres.generateFilenameConverted("pdf");
determineNumberOfPages(pres);
sendDocPageConversionStartedProgress(pres);
PresentationConvertMessage msg = new PresentationConvertMessage(pres);

View File

@ -108,12 +108,14 @@ public class SlidesGenerationProgressNotifier {
log.error("GeneratedSlidesInfoHelper was not set. Could not notify interested listeners.");
return;
}
// Completed conversion -> delete original file
pres.deleteOriginalFile();
DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(),
pres.getId(), pres.getTemporaryPresentationId(), pres.getId(),
pres.getName(), "notUsedYet", "notUsedYet",
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.CONVERSION_COMPLETED_KEY,
pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent(), pres.getIsInitialPresentation());
pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent(), pres.getIsInitialPresentation(), pres.getFilenameConverted());
messagingService.sendDocConversionMsg(progress);
}

View File

@ -16,11 +16,14 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
public final String presBaseUrl;
public final Boolean current;
public final Boolean isInitialPresentation;
public final String filenameConverted;
public DocPageCompletedProgress(String podId, String meetingId, String presId, String temporaryPresentationId, String presInstance,
String filename, String uploaderId, String authzToken,
Boolean downloadable, Boolean removable, String key,
Integer numPages, String presBaseUrl, Boolean current, Boolean isInitialPresentation) {
Integer numPages, String presBaseUrl, Boolean current,
Boolean isInitialPresentation, String filenameConverted) {
this.podId = podId;
this.meetingId = meetingId;
this.presId = presId;
@ -36,5 +39,6 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
this.presBaseUrl = presBaseUrl;
this.current = current;
this.isInitialPresentation = isInitialPresentation;
this.filenameConverted = filenameConverted;
}
}

View File

@ -148,8 +148,9 @@ object MsgBuilder {
val pages = generatePresentationPages(msg.presId, msg.numPages.intValue(), msg.presBaseUrl)
val presentation = PresentationVO(msg.presId, msg.temporaryPresentationId, msg.filename,
current = msg.current.booleanValue(), pages.values.toVector, msg.downloadable.booleanValue(), msg.removable.booleanValue(),
isInitialPresentation = msg.isInitialPresentation)
current = msg.current.booleanValue(), pages.values.toVector, msg.downloadable.booleanValue(),
msg.removable.booleanValue(),
isInitialPresentation = msg.isInitialPresentation, msg.filenameConverted)
val body = PresentationConversionCompletedSysPubMsgBody(podId = msg.podId, messageKey = msg.key,
code = msg.key, presentation)

View File

@ -25,7 +25,7 @@
"notifier": {
"pod_id": "DEFAULT_PRESENTATION_POD",
"is_downloadable": "false",
"msgName": "NewPresAnnFileAvailableMsg"
"msgName": "NewPresFileAvailableMsg"
},
"bbbWebAPI": "http://127.0.0.1:8090",
"bbbWebPublicAPI": "/bigbluebutton/",

View File

@ -53,7 +53,7 @@ class PresAnnStatusMsg {
}
};
class NewPresAnnFileAvailableMsg {
class NewPresFileAvailableMsg {
constructor(exportJob, link) {
this.message = {
envelope: {
@ -72,6 +72,7 @@ class NewPresAnnFileAvailableMsg {
body: {
fileURI: link,
presId: exportJob.presId,
typeOfExport: "Annotated",
},
},
};
@ -84,5 +85,5 @@ class NewPresAnnFileAvailableMsg {
module.exports = {
PresAnnStatusMsg,
NewPresAnnFileAvailableMsg,
NewPresFileAvailableMsg,
};

View File

@ -5,7 +5,7 @@ const FormData = require('form-data');
const redis = require('redis');
const axios = require('axios').default;
const path = require('path');
const {NewPresAnnFileAvailableMsg} = require('../lib/utils/message-builder');
const {NewPresFileAvailableMsg} = require('../lib/utils/message-builder');
const {workerData} = require('worker_threads');
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
@ -32,7 +32,7 @@ async function notifyMeetingActor() {
exportJob.parentMeetingId, exportJob.parentMeetingId,
exportJob.presId, 'pdf', jobId, filename);
const notification = new NewPresAnnFileAvailableMsg(exportJob, link);
const notification = new NewPresFileAvailableMsg(exportJob, link);
logger.info(`Annotated PDF available at ${link}`);
await client.publish(config.redis.channels.publish, notification.build());

View File

@ -18,5 +18,5 @@ RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded)
RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove);
RedisPubSub.on('SetCurrentPresentationEvtMsg', handlePresentationCurrentSet);
RedisPubSub.on('SetPresentationDownloadableEvtMsg', handlePresentationDownloadableSet);
RedisPubSub.on('NewPresAnnFileAvailableEvtMsg', handlePresentationExport);
RedisPubSub.on('NewPresFileAvailableEvtMsg', handlePresentationExport);
RedisPubSub.on('PresAnnStatusEvtMsg', handlePresentationExportToastUpdate);

View File

@ -1,16 +1,22 @@
import { check } from 'meteor/check';
import sendExportedPresentationChatMsg from '/imports/api/presentations/server/handlers/sendExportedPresentationChatMsg';
import setPresentationExporting from '/imports/api/presentations/server/modifiers/setPresentationExporting';
import setOriginalUriDownload from '/imports/api/presentations/server/modifiers/setOriginalUriDownload';
export default async function handlePresentationExport({ body }, meetingId) {
check(body, Object);
check(meetingId, String);
const { fileURI, presId } = body;
const { fileURI, presId, typeOfExport } = body;
check(fileURI, String);
check(presId, String);
check(typeOfExport, String);
await sendExportedPresentationChatMsg(meetingId, presId, fileURI);
if (typeOfExport === 'Original') {
await setOriginalUriDownload(meetingId, presId, fileURI);
} else {
await sendExportedPresentationChatMsg(meetingId, presId, fileURI, typeOfExport);
}
await setPresentationExporting(meetingId, presId, { status: 'EXPORTED' });
}

View File

@ -3,11 +3,13 @@ import Presentations from '/imports/api/presentations';
const DEFAULT_FILENAME = 'annotated_slides.pdf';
export default async function sendExportedPresentationChatMsg(meetingId, presentationId, fileURI) {
export default async function sendExportedPresentationChatMsg(meetingId,
presentationId, fileURI, typeOfExport) {
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid;
const CHAT_EXPORTED_PRESENTATION_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_exported_presentation;
const CHAT_EXPORTED_PRESENTATION_MESSAGE = CHAT_CONFIG
.system_messages_keys.chat_exported_presentation;
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
const pres = await Presentations.findOneAsync({ meetingId, id: presentationId });
@ -16,6 +18,7 @@ export default async function sendExportedPresentationChatMsg(meetingId, present
type: 'presentation',
fileURI,
filename: pres?.name || DEFAULT_FILENAME,
typeOfExport,
};
const payload = {

View File

@ -3,12 +3,12 @@ import removePresentation from './methods/removePresentation';
import setPresentationRenderedInToast from './methods/setPresentationRenderedInToast';
import setPresentation from './methods/setPresentation';
import setPresentationDownloadable from './methods/setPresentationDownloadable';
import exportPresentationToChat from './methods/exportPresentationToChat';
import exportPresentation from './methods/exportPresentation';
Meteor.methods({
removePresentation,
setPresentation,
setPresentationDownloadable,
exportPresentationToChat,
exportPresentation,
setPresentationRenderedInToast,
});

View File

@ -7,10 +7,10 @@ import Presentations from '/imports/api/presentations';
const EXPORTING_THRESHOLD_PER_SLIDE = 2500;
export default async function exportPresentationToChat(presentationId) {
export default async function exportPresentation(presentationId, type) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg';
const EVENT_NAME = 'MakePresentationDownloadReqMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
@ -22,6 +22,7 @@ export default async function exportPresentationToChat(presentationId) {
const payload = {
presId: presentationId,
allPages: true,
typeOfExport: type,
pages: [],
};
@ -30,7 +31,8 @@ export default async function exportPresentationToChat(presentationId) {
const selector = { meetingId, id: presentationId };
const cursor = Presentations.find(selector);
const numPages = await cursor.fetchAsync()[0]?.pages?.length ?? 1;
const pres = await Presentations.findOneAsync(selector);
const numPages = pres?.pages?.length ?? 1;
const threshold = EXPORTING_THRESHOLD_PER_SLIDE * numPages;
const observer = cursor.observe({
@ -54,6 +56,6 @@ export default async function exportPresentationToChat(presentationId) {
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method exportPresentationToChat ${err.stack}`);
Logger.error(`Exception while invoking method exportPresentation ${err.stack}`);
}
}

View File

@ -52,6 +52,7 @@ export default async function addPresentation(meetingId, podId, presentation) {
downloadable: Boolean,
removable: Boolean,
isInitialPresentation: Boolean,
filenameConverted: String,
});
const selector = {
@ -71,7 +72,7 @@ export default async function addPresentation(meetingId, podId, presentation) {
};
try {
const { insertedId } = await Presentations.upsertAsync(selector, modifier);
await Presentations.upsertAsync(selector, modifier);
await addSlides(meetingId, podId, presentation.id, presentation.pages);

View File

@ -0,0 +1,30 @@
import { check } from 'meteor/check';
import Presentations from '/imports/api/presentations';
import Logger from '/imports/startup/server/logger';
export default async function setOriginalUriDownload(meetingId, presentationId, fileURI) {
check(meetingId, String);
check(presentationId, String);
check(fileURI, String);
const selector = {
meetingId,
id: presentationId,
};
const modifier = {
$set: {
originalFileURI: fileURI,
},
};
try {
const { numberAffected } = await Presentations.upsertAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Set URI for file ${presentationId} in meeting ${meetingId} URI=${fileURI}`);
}
} catch (err) {
Logger.error(`Could not set URI for file ${presentationId} in meeting ${meetingId} ${err}`);
}
}

View File

@ -51,6 +51,14 @@ const intlMessages = defineMessages({
id: 'app.presentationUploader.export.notAccessibleWarning',
description: 'used for indicating that a link may be not accessible',
},
original: {
id: 'app.presentationUploader.export.originalLabel',
description: 'Label to identify original presentation exported',
},
annotated: {
id: 'app.presentationUploader.export.withAnnotationsLabel',
description: 'Label to identify annotated presentation exported',
},
});
const setUserSentMessage = (bool) => {
@ -328,12 +336,13 @@ const removePackagedClassAttribute = (classnames, attribute) => {
});
};
const getExportedPresentationString = (fileURI, filename, intl) => {
const warningIcon = `<i class="icon-bbb-warning"></i>`;
const getExportedPresentationString = (fileURI, filename, intl, typeOfExport) => {
const intlTypeOfExport = typeOfExport === 'Original' ? intlMessages.original : intlMessages.annotated;
const warningIcon = '<i class="icon-bbb-warning"></i>';
const label = `<span>${intl.formatMessage(intlMessages.download)}</span>`;
const notAccessibleWarning = `<span title="${intl.formatMessage(intlMessages.notAccessibleWarning)}">${warningIcon}</span>`;
const link = `<a aria-label="${intl.formatMessage(intlMessages.notAccessibleWarning)}" href=${fileURI} type="application/pdf" rel="noopener, noreferrer" download>${label}&nbsp;${notAccessibleWarning}</a>`;
const name = `<span>${filename}</span>`;
const name = `<span>${filename} (${intl.formatMessage(intlTypeOfExport)})</span>`;
return `${name}</br>${link}`;
};

View File

@ -332,7 +332,7 @@ class TimeWindowChatItem extends PureComponent {
<Styled.PresentationChatItem
type="presentation"
key={messages[0].id}
text={getExportedPresentationString(extra.fileURI, extra.filename, intl)}
text={getExportedPresentationString(extra.fileURI, extra.filename, intl, extra.typeOfExport)}
time={messages[0].time}
chatAreaId={chatAreaId}
lastReadMessageTime={lastReadMessageTime}

View File

@ -8,6 +8,7 @@ import { toast } from 'react-toastify';
import { Session } from 'meteor/session';
import PresentationToolbarContainer from './presentation-toolbar/container';
import PresentationMenu from './presentation-menu/container';
import DownloadPresentationButton from './download-presentation-button/component';
import Styled from './styles';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import Icon from '/imports/ui/components/common/icon/component';
@ -629,6 +630,23 @@ class Presentation extends PureComponent {
);
}
renderPresentationDownload() {
const { presentationIsDownloadable, downloadPresentationUri } = this.props;
if (!presentationIsDownloadable || !downloadPresentationUri) return null;
const handleDownloadPresentation = () => {
window.open(downloadPresentationUri);
};
return (
<DownloadPresentationButton
handleDownloadPresentation={handleDownloadPresentation}
dark
/>
);
}
renderPresentationMenu() {
const {
intl,
@ -756,6 +774,7 @@ class Presentation extends PureComponent {
}}
id="presentationInnerWrapper"
>
{this.renderPresentationDownload()}
<Styled.VisuallyHidden id="currentSlideText">{slideContent}</Styled.VisuallyHidden>
{!tldrawIsMounting && currentSlide && this.renderPresentationMenu()}
<WhiteboardContainer

View File

@ -0,0 +1,46 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import Styled from './styles';
const intlMessages = defineMessages({
downloadPresentationButton: {
id: 'app.downloadPresentationButton.label',
description: 'Download presentation label',
},
});
const propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
handleDownloadPresentation: PropTypes.func.isRequired,
dark: PropTypes.bool,
};
const defaultProps = {
dark: false,
};
const DownloadPresentationButton = ({
intl,
handleDownloadPresentation,
dark,
}) => (
<Styled.ButtonWrapper theme={dark ? 'dark' : 'light'}>
<Styled.DownloadButton
data-test="presentationDownload"
color="default"
icon="template_download"
size="sm"
onClick={handleDownloadPresentation}
label={intl.formatMessage(intlMessages.downloadPresentationButton)}
hideLabel
/>
</Styled.ButtonWrapper>
);
DownloadPresentationButton.propTypes = propTypes;
DownloadPresentationButton.defaultProps = defaultProps;
export default injectIntl(DownloadPresentationButton);

View File

@ -0,0 +1,74 @@
import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import {
colorWhite,
colorBlack,
colorTransparent,
} from '/imports/ui/stylesheets/styled-components/palette';
const DownloadButton = styled(Button)`
&,
&:active,
&:hover,
&:focus {
background-color: ${colorTransparent} !important;
border: none !important;
i {
border: none !important;
background-color: ${colorTransparent} !important;
}
}
padding: 5px;
&:hover {
border: 0;
}
i {
font-size: 1rem;
}
`;
const ButtonWrapper = styled.div`
position: absolute;
right: auto;
left: 0;
background-color: ${colorTransparent};
cursor: pointer;
border: 0;
z-index: 2;
margin: 2px;
bottom: 0;
[dir="rtl"] & {
right: 0;
left : auto;
}
[class*="presentationZoomControls"] & {
position: relative !important;
}
${({ theme }) => theme === 'dark' && `
background-color: rgba(0,0,0,.3) !important;
& > button i {
color: ${colorWhite} !important;
}
`}
${({ theme }) => theme === 'light' && `
background-color: ${colorTransparent} !important;
& > button i {
color: ${colorBlack} !important;
}
`}
`;
export default {
DownloadButton,
ButtonWrapper,
};

View File

@ -12,6 +12,7 @@ import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import { registerTitleView, unregisterTitleView } from '/imports/utils/dom-utils';
import Styled from './styles';
import PresentationDownloadDropdown from './presentation-download-dropdown/component';
import Settings from '/imports/ui/services/settings';
import Radio from '/imports/ui/components/common/radio/component';
import { unique } from 'radash';
@ -46,7 +47,8 @@ const propTypes = {
presentationUploadExternalUrl: PropTypes.string.isRequired,
}).isRequired,
isPresenter: PropTypes.bool.isRequired,
exportPresentationToChat: PropTypes.func.isRequired,
exportPresentation: PropTypes.func.isRequired,
hasAnnotations: PropTypes.func.isRequired,
};
const defaultProps = {
@ -308,6 +310,10 @@ const intlMessages = defineMessages({
id: 'app.presentationUploader.export.linkAvailable',
description: 'download presentation link available on public chat',
},
downloadButtonAvailable: {
id: 'app.presentationUploader.export.downloadButtonAvailable',
description: 'download presentation link available on public chat',
},
});
const EXPORT_STATUSES = {
@ -341,7 +347,7 @@ class PresentationUploader extends Component {
this.handleDismiss = this.handleDismiss.bind(this);
this.handleRemove = this.handleRemove.bind(this);
this.handleCurrentChange = this.handleCurrentChange.bind(this);
this.handleSendToChat = this.handleSendToChat.bind(this);
this.handleDownloadingOfPresentation = this.handleDownloadingOfPresentation.bind(this);
// renders
this.renderDropzone = this.renderDropzone.bind(this);
this.renderExternalUpload = this.renderExternalUpload.bind(this);
@ -355,6 +361,7 @@ class PresentationUploader extends Component {
this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this);
this.updateFileKey = this.updateFileKey.bind(this);
this.getPresentationsToShow = this.getPresentationsToShow.bind(this);
this.handleToggleDownloadable = this.handleToggleDownloadable.bind(this);
}
componentDidUpdate(prevProps) {
@ -405,6 +412,11 @@ class PresentationUploader extends Component {
modPresentation.isCurrent = currentPropPres.isCurrent;
}
if (currentPropPres?.isDownloadable !== prevPropPres?.isDownloadable) {
presentation.isDownloadable = currentPropPres.isDownloadable;
shouldUpdateState = true;
}
modPresentation.conversion = currentPropPres.conversion;
modPresentation.isRemovable = currentPropPres.isRemovable;
@ -647,18 +659,26 @@ class PresentationUploader extends Component {
return null;
}
handleToggleDownloadable(item) {
const { dispatchTogglePresentationDownloadable } = this.props;
const oldDownloadableState = item.isDownloadable;
dispatchTogglePresentationDownloadable(item, !oldDownloadableState);
}
handleDismiss() {
const { presentations } = this.state;
const { presentations: propPresentations } = this.props;
const ids = new Set(propPresentations.map((d) => d.id));
const filteredPresentations = presentations.filter((d) => {
return !ids.has(d.id) && (d.upload.done || d.upload.progress !== 0)});
const isThereStateCurrentPres = filteredPresentations.some(p => p.isCurrent);
const filteredPresentations = presentations.filter((d) => !ids.has(d.id)
&& (d.upload.done || d.upload.progress !== 0));
const isThereStateCurrentPres = filteredPresentations.some((p) => p.isCurrent);
const merged = [
...filteredPresentations,
...propPresentations.filter(p => {
...propPresentations.filter((p) => {
if (isThereStateCurrentPres) {
p.isCurrent = false;
}
@ -671,9 +691,9 @@ class PresentationUploader extends Component {
);
}
handleSendToChat(item) {
handleDownloadingOfPresentation(item, type) {
const {
exportPresentationToChat,
exportPresentation,
intl,
} = this.props;
@ -681,14 +701,20 @@ class PresentationUploader extends Component {
this.deepMergeUpdateFileKey(item.id, 'exportation', exportation);
if (exportation.status === EXPORT_STATUSES.EXPORTED && stopped) {
notify(intl.formatMessage(intlMessages.linkAvailable, { 0: item.filename }), 'success');
if (type === 'Original') {
if (!item.isDownloadable) {
notify(intl.formatMessage(intlMessages.downloadButtonAvailable, { 0: item.filename }), 'success');
}
} else {
notify(intl.formatMessage(intlMessages.linkAvailable, { 0: item.filename }), 'success');
}
}
if ([
EXPORT_STATUSES.RUNNING,
EXPORT_STATUSES.COLLECTING,
EXPORT_STATUSES.PROCESSING,
].includes(exportation.status)) {
].includes(exportation.status) && type !== 'Original') {
this.setState((prevState) => {
prevState.presExporting.add(item.id);
return {
@ -723,9 +749,7 @@ class PresentationUploader extends Component {
}
};
exportPresentationToChat(item.id, observer);
Session.set('showUploadPresentationView', false);
exportPresentation(item.id, observer, type);
}
getPresentationsToShow() {
@ -972,6 +996,7 @@ class PresentationUploader extends Component {
selectedToBeNextCurrent,
allowDownloadable,
renderPresentationItemStatus,
hasAnnotations,
} = this.props;
const isActualCurrent = selectedToBeNextCurrent
@ -988,14 +1013,14 @@ class PresentationUploader extends Component {
const { animations } = Settings.application;
const { isRemovable, exportation: { status } } = item;
const { isRemovable, exportation: { status }, isDownloadable } = item;
const isExporting = status === 'RUNNING';
const shouldDisableExportButton = isExporting
const shouldDisableExportButton = (isExporting
|| !item.conversion.done
|| hasError
|| disableActions;
|| disableActions) && !item.conversion?.done;
const formattedDownloadLabel = isExporting
? intl.formatMessage(intlMessages.exporting)
@ -1003,6 +1028,7 @@ class PresentationUploader extends Component {
const formattedDownloadAriaLabel = `${formattedDownloadLabel} ${item.filename}`;
const hasAnyAnnotation = hasAnnotations(item.id);
return (
<Styled.PresentationItem
key={item.id}
@ -1040,18 +1066,22 @@ class PresentationUploader extends Component {
<Styled.TableItemStatus colSpan={hasError ? 2 : 0}>
{renderPresentationItemStatus(item, intl)}
</Styled.TableItemStatus>
{hasError ? null : (
{
hasError ? null : (
<Styled.TableItemActions notDownloadable={!allowDownloadable}>
{allowDownloadable ? (
<Styled.DownloadButton
<PresentationDownloadDropdown
hasAnnotations={hasAnyAnnotation}
disabled={shouldDisableExportButton}
label={intl.formatMessage(intlMessages.export)}
data-test="exportPresentationToPublicChat"
data-test="exportPresentation"
aria-label={formattedDownloadAriaLabel}
size="sm"
color="primary"
onClick={() => this.handleSendToChat(item)}
animations={animations}
isDownloadable={isDownloadable}
handleToggleDownloadable={this.handleToggleDownloadable}
item={item}
closeModal={() => Session.set('showUploadPresentationView', false)}
handleDownloadingOfPresentation={(type) => this
.handleDownloadingOfPresentation(item, type)}
/>
) : null}
{isRemovable ? (

View File

@ -9,6 +9,7 @@ import PresentationUploader from './component';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import Auth from '/imports/ui/services/auth';
import { isDownloadPresentationWithAnnotationsEnabled, isPresentationEnabled } from '/imports/ui/services/features';
import { hasAnnotations } from '/imports/ui/components/whiteboard/service';
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
@ -32,7 +33,7 @@ export default withTracker(() => {
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchTogglePresentationDownloadable,
exportPresentationToChat,
exportPresentation,
} = Service;
const isOpen = isPresentationEnabled() && (Session.get('showUploadPresentationView') || false);
@ -51,10 +52,11 @@ export default withTracker(() => {
dispatchDisableDownloadable,
dispatchEnableDownloadable,
dispatchTogglePresentationDownloadable,
exportPresentationToChat,
exportPresentation,
isOpen,
selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null,
externalUploadData: Service.getExternalUploadData(),
handleFiledrop: Service.handleFiledrop,
hasAnnotations,
};
})(PresentationUploaderContainer);

View File

@ -0,0 +1,173 @@
import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import BBBMenu from '/imports/ui/components/common/menu/component';
import { uniqueId } from '/imports/utils/string-utils';
import Trigger from '/imports/ui/components/common/control-header/right/component';
import PresentationDownloadDropdownWrapper from './presentation-download-dropdown-wrapper/component';
const intlMessages = defineMessages({
enableOriginalPresentationDownload: {
id: 'app.presentationUploader.enableOriginalPresentationDownload',
description: 'Send original presentation to chat',
},
disableOriginalPresentationDownload: {
id: 'app.presentationUploader.disableOriginalPresentationDownload',
description: 'Send original presentation to chat',
},
sendAnnotatedDocument: {
id: 'app.presentationUploader.exportAnnotatedPresentation',
description: 'Send presentation to chat with annotations label',
},
copySuccess: {
id: 'app.chat.copySuccess',
description: 'aria success alert',
},
copyErr: {
id: 'app.chat.copyErr',
description: 'aria error alert',
},
options: {
id: 'app.presentationUploader.dropdownExportOptions',
description: 'Chat Options',
},
});
const propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
handleDownloadingOfPresentation: PropTypes.func.isRequired,
handleToggleDownloadable: PropTypes.func.isRequired,
isDownloadable: PropTypes.bool.isRequired,
item: PropTypes.shape({
id: PropTypes.string.isRequired,
filename: PropTypes.string.isRequired,
isCurrent: PropTypes.bool.isRequired,
temporaryPresentationId: PropTypes.string.isRequired,
isDownloadable: PropTypes.bool.isRequired,
isRemovable: PropTypes.bool.isRequired,
conversion: PropTypes.shape,
upload: PropTypes.shape,
exportation: PropTypes.shape,
uploadTimestamp: PropTypes.number.isRequired,
}).isRequired,
closeModal: PropTypes.func.isRequired,
hasAnnotations: PropTypes.bool.isRequired,
isRTL: PropTypes.bool.isRequired,
disabled: PropTypes.bool.isRequired,
};
class PresentationDownloadDropdown extends PureComponent {
constructor(props) {
super(props);
this.actionsKey = [
uniqueId('action-item-'),
uniqueId('action-item-'),
uniqueId('action-item-'),
];
}
getAvailableActions() {
const {
intl,
handleDownloadingOfPresentation,
handleToggleDownloadable,
isDownloadable,
item,
closeModal,
hasAnnotations,
} = this.props;
this.menuItems = [];
const toggleDownloadOriginalPresentation = (enableDownload) => {
handleToggleDownloadable(item);
if (enableDownload) {
handleDownloadingOfPresentation('Original');
}
closeModal();
};
if (!isDownloadable) {
this.menuItems.push(
{
key: this.actionsKey[0],
dataTest: 'enableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.enableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(true),
},
);
} else {
this.menuItems.push(
{
key: this.actionsKey[0],
dataTest: 'disableOriginalPresentationDownload',
label: intl.formatMessage(intlMessages.disableOriginalPresentationDownload),
onClick: () => toggleDownloadOriginalPresentation(false),
},
);
}
if (hasAnnotations) {
this.menuItems.push(
{
key: this.actionsKey[1],
id: 'sendAnnotatedDocument',
dataTest: 'sendAnnotatedDocument',
label: intl.formatMessage(intlMessages.sendAnnotatedDocument),
onClick: () => {
closeModal();
handleDownloadingOfPresentation('Annotated');
},
},
);
}
return this.menuItems;
}
render() {
const {
intl,
isRTL,
disabled,
} = this.props;
return (
<PresentationDownloadDropdownWrapper
disabled={disabled}
>
<BBBMenu
trigger={
(
<Trigger
data-test="presentationOptionsDownload"
icon="more"
label={intl.formatMessage(intlMessages.options)}
aria-label={intl.formatMessage(intlMessages.options)}
onClick={() => null}
/>
)
}
opts={{
id: 'presentation-download-dropdown',
keepMounted: true,
transitionDuration: 0,
elevation: 2,
getContentAnchorEl: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
}}
actions={this.getAvailableActions()}
/>
</PresentationDownloadDropdownWrapper>
);
}
}
PresentationDownloadDropdown.propTypes = propTypes;
export default injectIntl(PresentationDownloadDropdown);

View File

@ -0,0 +1,110 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
const propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
children: PropTypes.shape.isRequired,
disabled: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
onDoubleClick: PropTypes.func.isRequired,
onMouseDown: PropTypes.func.isRequired,
onMouseUp: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onKeyDown: PropTypes.func.isRequired,
onKeyUp: PropTypes.func.isRequired,
};
class PresentationDownloadDropdownWrapper extends PureComponent {
constructor(props) {
super(props);
// Bind Mouse Event Handlers
this.internalClickHandler = this.internalClickHandler.bind(this);
this.internalDoubleClickHandler = this.internalDoubleClickHandler.bind(this);
this.internalMouseDownHandler = this.internalMouseDownHandler.bind(this);
this.internalMouseUpHandler = this.internalMouseUpHandler.bind(this);
// Bind Keyboard Event Handlers
this.internalKeyPressHandler = this.internalKeyPressHandler.bind(this);
this.internalKeyDownHandler = this.internalKeyDownHandler.bind(this);
this.internalKeyUpHandler = this.internalKeyUpHandler.bind(this);
}
validateDisabled(eventHandler, ...args) {
const { disabled } = this.props;
if (!disabled && typeof eventHandler === 'function') {
return eventHandler(...args);
}
return null;
}
// Define Mouse Event Handlers
internalClickHandler(...args) {
const { onClick } = this.props;
return this.validateDisabled(onClick, ...args);
}
internalDoubleClickHandler(...args) {
const { onDoubleClick } = this.props;
return this.validateDisabled(onDoubleClick, ...args);
}
internalMouseDownHandler(...args) {
const { onMouseDown } = this.props;
return this.validateDisabled(onMouseDown, ...args);
}
internalMouseUpHandler(...args) {
const { onMouseUp } = this.props;
return this.validateDisabled(onMouseUp, ...args);
}
// Define Keyboard Event Handlers
internalKeyPressHandler(...args) {
const { onKeyPress } = this.props;
return this.validateDisabled(onKeyPress, ...args);
}
internalKeyDownHandler(...args) {
const { onKeyDown } = this.props;
return this.validateDisabled(onKeyDown, ...args);
}
internalKeyUpHandler(...args) {
const { onKeyUp } = this.props;
return this.validateDisabled(onKeyUp, ...args);
}
render() {
const {
disabled,
children,
} = this.props;
return (
<Styled.DropdownMenuWrapper
disabled={disabled}
aria-disabled={disabled}
// Render Mouse event handlers
onClick={this.internalClickHandler}
onDoubleClick={this.internalDoubleClickHandler}
onMouseDown={this.internalMouseDownHandler}
onMouseUp={this.internalMouseUpHandler}
// Render Keyboard event handlers
onKeyPress={this.internalKeyPressHandler}
onKeyDown={this.internalKeyDownHandler}
onKeyUp={this.internalKeyUpHandler}
>
{children}
</Styled.DropdownMenuWrapper>
);
}
}
PresentationDownloadDropdownWrapper.propTypes = propTypes;
export default PresentationDownloadDropdownWrapper;

View File

@ -0,0 +1,16 @@
import styled from 'styled-components';
const DropdownMenuWrapper = styled.div`
display: inline-block;
&[aria-disabled="true"] {
cursor: not-allowed;
opacity: .5;
box-shadow: none;
pointer-events: none;
}
`;
export default {
DropdownMenuWrapper,
};

View File

@ -378,7 +378,7 @@ const getExternalUploadData = () => {
};
};
const exportPresentationToChat = (presentationId, observer) => {
const exportPresentation = (presentationId, observer, type) => {
let lastStatus = {};
Tracker.autorun((c) => {
@ -407,7 +407,7 @@ const exportPresentationToChat = (presentationId, observer) => {
});
});
makeCall('exportPresentationToChat', presentationId);
makeCall('exportPresentation', presentationId, type);
};
function handleFiledrop(files, files2, that, intl, intlMessages) {
@ -487,7 +487,7 @@ export default {
setPresentation,
requestPresentationUploadToken,
getExternalUploadData,
exportPresentationToChat,
exportPresentation,
uploadAndConvertPresentation,
handleFiledrop,
};

View File

@ -530,50 +530,6 @@ const TableItemActions = styled.td`
`}
`;
const DownloadButton = styled(Button)`
background: transparent;
background-color: transparent;
border: 2px solid ${colorBlueLight} !important;
border-radius: 4px;
color: ${colorBlueLight};
cursor: pointer;
display: inline-block;
font-size: 80%;
&:hover {
background-color: ${colorOffWhite} !important;
color: ${colorBlueLight} !important;
filter: none !important;
}
&:focus {
background-color: transparent !important;
color: ${colorBlueLight} !important;
}
&:hover:focus {
background-color: ${colorOffWhite} !important;
color: ${colorBlueLight} !important;
}
&:active:focus {
background-color: ${colorOffWhite} !important;
color: ${colorBlueLight} !important;
filter: brightness(85%) !important;
}
${({ animations }) => animations && `
transition: all .25s;
`}
&[aria-disabled="true"] {
cursor: not-allowed;
opacity: .5;
box-shadow: none;
pointer-events: none;
}
`;
const ExtraHint = styled.div`
margin-top: 1rem;
font-weight: bold;
@ -702,7 +658,6 @@ export default {
StatusInfoSpan,
PresentationItem,
TableItemActions,
DownloadButton,
ExtraHint,
ExternalUpload,
ExternalUploadTitle,

View File

@ -17,13 +17,7 @@ const downloadPresentationUri = (podId) => {
return null;
}
const presentationFileName = `${currentPresentation.id}.${currentPresentation.name.split('.').pop()}`;
const APP = Meteor.settings.public.app;
const uri = `${APP.bbbWebBase}/presentation/download/`
+ `${currentPresentation.meetingId}/${currentPresentation.id}`
+ `?presFilename=${encodeURIComponent(presentationFileName)}`;
const { originalFileURI: uri } = currentPresentation;
return uri;
};

View File

@ -212,6 +212,13 @@ const getCurrentWhiteboardId = () => {
return currentSlide && currentSlide.id;
};
const hasAnnotations = (presentationId) => {
const ann = Annotations.findOne(
{ whiteboardId: { $regex: `^${presentationId}` } },
);
return ann !== undefined;
};
const isMultiUserActive = (whiteboardId) => {
const multiUser = getMultiUser(whiteboardId);
@ -404,5 +411,6 @@ export {
changeCurrentSlide,
notifyNotAllowedChange,
notifyShapeNumberExceeded,
hasAnnotations,
toggleToolsAnimations,
};

View File

@ -250,8 +250,15 @@
"app.presentationUploader.sent": "Sent",
"app.presentationUploader.exportingTimeout": "The export is taking too long...",
"app.presentationUploader.export": "Send to chat",
"app.presentationUploader.exportAnnotatedPresentation": "Send out a download link for the presentation with annotations included",
"app.presentationUploader.enableOriginalPresentationDownload": "Enable download of the original presentation",
"app.presentationUploader.disableOriginalPresentationDownload": "Disable download of the original presentation",
"app.presentationUploader.dropdownExportOptions": "Export options",
"app.presentationUploader.export.linkAvailable": "Link for downloading {0} available on the public chat.",
"app.presentationUploader.export.downloadButtonAvailable": "Download button for presentation {0} is available.",
"app.presentationUploader.export.notAccessibleWarning": "may not be accessibility compliant",
"app.presentationUploader.export.originalLabel": "Original",
"app.presentationUploader.export.withAnnotationsLabel": "With Annotations",
"app.presentationUploader.currentPresentationLabel": "Current presentation",
"app.presentationUploder.extraHint": "IMPORTANT: each file may not exceed {0} MB and {1} pages.",
"app.presentationUploder.uploadLabel": "Upload",

View File

@ -338,6 +338,7 @@ class PresentationController {
def presId = params.presId
def presFilename = params.presFilename
def meetingId = params.meetingId
def filename = params.filename
log.debug "Controller: Download request for $presFilename"
@ -348,7 +349,7 @@ class PresentationController {
log.debug "Controller: Sending pdf reply for $presFilename"
def bytes = pres.readBytes()
def responseName = pres.getName();
def responseName = filename;
def mimeType = grailsMimeUtility.getMimeTypeForURI(responseName)
def mimeName = mimeType != null ? mimeType.name : 'application/octet-stream'