diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala index 527db49380..3d3fc1246a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala @@ -11,6 +11,7 @@ class PresentationPodHdlrs(implicit val context: ActorContext) with PresentationConversionCompletedSysPubMsgHdlr with PdfConversionInvalidErrorSysPubMsgHdlr with SetCurrentPagePubMsgHdlr + with SetPageInfiniteCanvasPubMsgHdlr with SetPresenterInDefaultPodInternalMsgHdlr with RemovePresentationPubMsgHdlr with SetPresentationDownloadablePubMsgHdlr diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/SetPageInfiniteCanvasPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/SetPageInfiniteCanvasPubMsgHdlr.scala new file mode 100644 index 0000000000..ea752c006c --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/SetPageInfiniteCanvasPubMsgHdlr.scala @@ -0,0 +1,33 @@ +package org.bigbluebutton.core.apps.presentationpod + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.domain.MeetingState2x +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.db.PresPageDAO + +trait SetPageInfiniteCanvasPubMsgHdlr extends RightsManagementTrait { + this: PresentationPodHdlrs => + + def handle( + msg: SetPageInfiniteCanvasPubMsg, state: MeetingState2x, + liveMeeting: LiveMeeting, bus: MessageBus + ): MeetingState2x = { + + if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "No permission to set infinite canvas." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + state + } else { + val pageId = msg.body.pageId + val infiniteCanvas = msg.body.infiniteCanvas + + PresPageDAO.updateInfiniteCanvas(pageId, infiniteCanvas) + + state + } + } +} + diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageDAO.scala index 9269eb9624..35c0b8b2a4 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPageDAO.scala @@ -23,7 +23,8 @@ case class PresPageDbModel( viewBoxHeight: Double, maxImageWidth: Int, maxImageHeight: Int, - uploadCompleted: Boolean + uploadCompleted: Boolean, + infiniteCanvas: Boolean, ) class PresPageDbTableDef(tag: Tag) extends Table[PresPageDbModel](tag, None, "pres_page") { @@ -45,8 +46,9 @@ class PresPageDbTableDef(tag: Tag) extends Table[PresPageDbModel](tag, None, "pr val maxImageWidth = column[Int]("maxImageWidth") val maxImageHeight = column[Int]("maxImageHeight") val uploadCompleted = column[Boolean]("uploadCompleted") + val infiniteCanvas = column[Boolean]("infiniteCanvas") // val presentation = foreignKey("presentation_fk", presentationId, Presentations)(_.presentationId, onDelete = ForeignKeyAction.Cascade) - def * = (pageId, presentationId, num, urlsJson, content, slideRevealed, current, xOffset, yOffset, widthRatio, heightRatio, width, height, viewBoxWidth, viewBoxHeight, maxImageWidth, maxImageHeight, uploadCompleted) <> (PresPageDbModel.tupled, PresPageDbModel.unapply) + def * = (pageId, presentationId, num, urlsJson, content, slideRevealed, current, xOffset, yOffset, widthRatio, heightRatio, width, height, viewBoxWidth, viewBoxHeight, maxImageWidth, maxImageHeight, uploadCompleted, infiniteCanvas) <> (PresPageDbModel.tupled, PresPageDbModel.unapply) } object PresPageDAO { @@ -76,7 +78,8 @@ object PresPageDAO { viewBoxHeight = 1, maxImageWidth = 1440, maxImageHeight = 1080, - uploadCompleted = page.converted + uploadCompleted = page.converted, + infiniteCanvas = page.infiniteCanvas ) ) ) @@ -109,4 +112,16 @@ object PresPageDAO { .update((width, height, xOffset, yOffset, widthRatio, heightRatio)) ) } + + def updateInfiniteCanvas(pageId: String, infiniteCanvas: Boolean) = { + DatabaseConnection.db.run( + TableQuery[PresPageDbTableDef] + .filter(_.pageId === pageId) + .map(p => p.infiniteCanvas) + .update(infiniteCanvas) + ).onComplete { + case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated infiniteCanvas on PresPage table") + case Failure(e) => DatabaseConnection.logger.debug(s"Error updating infiniteCanvas on PresPage: $e") + } + } } \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPresentationDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPresentationDAO.scala index 287a38515c..62911d4605 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPresentationDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PresPresentationDAO.scala @@ -153,34 +153,54 @@ object PresPresentationDAO { )) ) - DatabaseConnection.enqueue(DBIO.sequence( - for { - page <- presentation.pages - } yield { - TableQuery[PresPageDbTableDef].insertOrUpdate( - PresPageDbModel( - pageId = page._2.id, - presentationId = presentation.id, - num = page._2.num, - urlsJson = page._2.urls.toJson, - content = page._2.content, - slideRevealed = page._2.current, - current = page._2.current, - xOffset = page._2.xOffset, - yOffset = page._2.yOffset, - widthRatio = page._2.widthRatio, - heightRatio = page._2.heightRatio, - width = page._2.width, - height = page._2.height, - viewBoxWidth = 1, - viewBoxHeight = 1, - maxImageWidth = 1440, - maxImageHeight = 1080, - uploadCompleted = page._2.converted - ) + def updatePages(presentation: PresentationInPod) = { + DatabaseConnection.enqueue( + TableQuery[PresPresentationDbTableDef] + .filter(_.presentationId === presentation.id) + .map(p => (p.downloadFileExtension, p.uploadInProgress, p.uploadCompleted, p.totalPages)) + .update(( + presentation.downloadFileExtension match { + case "" => None + case downloadFileExtension => Some(downloadFileExtension) + }, + !presentation.uploadCompleted, + presentation.uploadCompleted, + presentation.numPages + )) + ) + + DatabaseConnection.db.run(DBIO.sequence( + for { + page <- presentation.pages + } yield { + TableQuery[PresPageDbTableDef].insertOrUpdate( + PresPageDbModel( + pageId = page._2.id, + presentationId = presentation.id, + num = page._2.num, + urlsJson = page._2.urls.toJson, + content = page._2.content, + slideRevealed = page._2.current, + current = page._2.current, + xOffset = page._2.xOffset, + yOffset = page._2.yOffset, + widthRatio = page._2.widthRatio, + heightRatio = page._2.heightRatio, + width = page._2.width, + height = page._2.height, + viewBoxWidth = 1, + viewBoxHeight = 1, + maxImageWidth = 1440, + maxImageHeight = 1080, + uploadCompleted = page._2.converted, + infiniteCanvas = page._2.infiniteCanvas ) - } - ).transactionally) + ) + } + ).transactionally).onComplete { + case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated on PresentationPage table!") + case Failure(e) => DatabaseConnection.logger.debug(s"Error updating PresentationPage: $e") + } //Set current if (presentation.current) { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala index 6d9d5a54bd..cd4be30b6e 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala @@ -30,7 +30,8 @@ case class PresentationPage( heightRatio: Double = 100D, width: Double = 1440D, height: Double = 1080D, - converted: Boolean = false + converted: Boolean = false, + infiniteCanvas: Boolean = false, ) object PresentationInPod { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index cc1bd7d5b4..250b3b8b8b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -287,6 +287,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[SetCurrentPresentationPubMsg](envelope, jsonNode) case SetCurrentPagePubMsg.NAME => routeGenericMsg[SetCurrentPagePubMsg](envelope, jsonNode) + case SetPageInfiniteCanvasPubMsg.NAME => + routeGenericMsg[SetPageInfiniteCanvasPubMsg](envelope, jsonNode) case ResizeAndMovePagePubMsg.NAME => routeGenericMsg[ResizeAndMovePagePubMsg](envelope, jsonNode) case SlideResizedPubMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 7ba1a20d41..d401afcf5c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -36,7 +36,6 @@ import org.apache.pekko.actor.Props import org.apache.pekko.actor.OneForOneStrategy import org.bigbluebutton.ClientSettings.{ getConfigPropertyValueByPathAsBooleanOrElse, getConfigPropertyValueByPathAsIntOrElse, getConfigPropertyValueByPathAsStringOrElse } import org.bigbluebutton.common2.msgs - import scala.concurrent.duration._ import org.bigbluebutton.core.apps.layout.LayoutApp2x import org.bigbluebutton.core.apps.meeting.{ SyncGetMeetingInfoRespMsgHdlr, ValidateConnAuthTokenSysMsgHdlr } @@ -633,6 +632,7 @@ class MeetingActor( case m: SetCurrentPagePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) updateUserLastActivity(m.header.userId) + case m: SetPageInfiniteCanvasPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: RemovePresentationPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala index 123b8b8152..6759301049 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala @@ -96,6 +96,8 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender) msgSender.send(fromAkkaAppsPresRedisChannel, json) case SetCurrentPageEvtMsg.NAME => msgSender.send(fromAkkaAppsPresRedisChannel, json) + case SetPageInfiniteCanvasEvtMsg.NAME => + msgSender.send(fromAkkaAppsPresRedisChannel, json) case ResizeAndMovePageEvtMsg.NAME => msgSender.send(fromAkkaAppsPresRedisChannel, json) case RemovePresentationEvtMsg.NAME => diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala index c2b72804d1..38d2d5ea0c 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala @@ -23,6 +23,10 @@ object SetCurrentPagePubMsg { val NAME = "SetCurrentPagePubMsg" } case class SetCurrentPagePubMsg(header: BbbClientMsgHeader, body: SetCurrentPagePubMsgBody) extends StandardMsg case class SetCurrentPagePubMsgBody(podId: String, presentationId: String, pageId: String) +object SetPageInfiniteCanvasPubMsg { val NAME = "SetPageInfiniteCanvasPubMsg" } +case class SetPageInfiniteCanvasPubMsg(header: BbbClientMsgHeader, body: SetPageInfiniteCanvasPubMsgBody) extends StandardMsg +case class SetPageInfiniteCanvasPubMsgBody(pageId: String, infiniteCanvas: Boolean) + object RemovePresentationPubMsg { val NAME = "RemovePresentationPubMsg" } case class RemovePresentationPubMsg(header: BbbClientMsgHeader, body: RemovePresentationPubMsgBody) extends StandardMsg case class RemovePresentationPubMsgBody(podId: String, presentationId: String) @@ -325,6 +329,10 @@ object SetCurrentPageEvtMsg { val NAME = "SetCurrentPageEvtMsg" } case class SetCurrentPageEvtMsg(header: BbbClientMsgHeader, body: SetCurrentPageEvtMsgBody) extends BbbCoreMsg case class SetCurrentPageEvtMsgBody(podId: String, presentationId: String, pageId: String) +object SetPageInfiniteCanvasEvtMsg { val NAME = "SetPageInfiniteCanvasEvtMsg" } +case class SetPageInfiniteCanvasEvtMsg(header: BbbClientMsgHeader, body: SetPageInfiniteCanvasEvtMsgBody) extends BbbCoreMsg +case class SetPageInfiniteCanvasEvtMsgBody(pageId: String, infiniteCanvas: Boolean) + object SetPresenterInPodRespMsg { val NAME = "SetPresenterInPodRespMsg" } case class SetPresenterInPodRespMsg(header: BbbClientMsgHeader, body: SetPresenterInPodRespMsgBody) extends StandardMsg case class SetPresenterInPodRespMsgBody(podId: String, nextPresenterId: String) diff --git a/bbb-graphql-actions/src/actions/presentationSetPageInfiniteCanvas.ts b/bbb-graphql-actions/src/actions/presentationSetPageInfiniteCanvas.ts new file mode 100644 index 0000000000..92d6285445 --- /dev/null +++ b/bbb-graphql-actions/src/actions/presentationSetPageInfiniteCanvas.ts @@ -0,0 +1,32 @@ +import { RedisMessage } from '../types'; +import {throwErrorIfInvalidInput, throwErrorIfNotPresenter} from "../imports/validation"; + +export default function buildRedisMessage(sessionVariables: Record, input: Record): RedisMessage { + throwErrorIfNotPresenter(sessionVariables); + throwErrorIfInvalidInput(input, + [ + {name: 'infiniteCanvas', type: 'boolean', required: true}, + {name: 'pageId', type: 'string', required: true}, + ] + ) + + const eventName = `SetPageInfiniteCanvasPubMsg`; + + const routing = { + meetingId: sessionVariables['x-hasura-meetingid'] as string, + userId: sessionVariables['x-hasura-userid'] as string + }; + + const header = { + name: eventName, + meetingId: routing.meetingId, + userId: routing.userId + }; + + const body = { + pageId: input.pageId, + infiniteCanvas: input.infiniteCanvas, + }; + + return { eventName, routing, header, body }; +} diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index f153ec420d..e54ee81655 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1207,7 +1207,8 @@ CREATE TABLE "pres_page" ( "viewBoxHeight" NUMERIC, "maxImageWidth" integer, "maxImageHeight" integer, - "uploadCompleted" boolean + "uploadCompleted" boolean, + "infiniteCanvas" boolean ); CREATE INDEX "idx_pres_page_presentationId" ON "pres_page"("presentationId"); CREATE INDEX "idx_pres_page_presentationId_curr" ON "pres_page"("presentationId") where "current" is true; @@ -1265,7 +1266,8 @@ SELECT pres_presentation."meetingId", (pres_page."height" * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledHeight", (pres_page."width" * pres_page."widthRatio" / 100 * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledViewBoxWidth", (pres_page."height" * pres_page."heightRatio" / 100 * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledViewBoxHeight", - pres_page."uploadCompleted" + pres_page."uploadCompleted", + pres_page."infiniteCanvas" FROM pres_page JOIN pres_presentation ON pres_presentation."presentationId" = pres_page."presentationId"; @@ -1297,7 +1299,8 @@ SELECT pres_presentation."meetingId", (pres_page."width" * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledWidth", (pres_page."height" * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledHeight", (pres_page."width" * pres_page."widthRatio" / 100 * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledViewBoxWidth", - (pres_page."height" * pres_page."heightRatio" / 100 * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledViewBoxHeight" + (pres_page."height" * pres_page."heightRatio" / 100 * LEAST(pres_page."maxImageWidth" / pres_page."width", pres_page."maxImageHeight" / pres_page."height")) AS "scaledViewBoxHeight", + pres_page."infiniteCanvas" FROM pres_presentation JOIN pres_page ON pres_presentation."presentationId" = pres_page."presentationId" AND pres_page."current" IS TRUE and pres_presentation."current" IS TRUE; diff --git a/bbb-graphql-server/metadata/actions.graphql b/bbb-graphql-server/metadata/actions.graphql index 72bc546190..01534ab0ad 100644 --- a/bbb-graphql-server/metadata/actions.graphql +++ b/bbb-graphql-server/metadata/actions.graphql @@ -357,6 +357,13 @@ type Mutation { ): Boolean } +type Mutation { + presentationSetPageInfiniteCanvas( + infiniteCanvas: Boolean! + pageId: String! + ): Boolean +} + type Mutation { presentationSetRenderedInToast( presentationId: String! diff --git a/bbb-graphql-server/metadata/actions.yaml b/bbb-graphql-server/metadata/actions.yaml index e7f0e8365a..157f86c456 100644 --- a/bbb-graphql-server/metadata/actions.yaml +++ b/bbb-graphql-server/metadata/actions.yaml @@ -306,6 +306,12 @@ actions: handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' permissions: - role: bbb_client + - name: presentationSetPageInfiniteCanvas + definition: + kind: synchronous + handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' + permissions: + - role: bbb_client - name: presentationSetRenderedInToast definition: kind: synchronous diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page.yaml index ddbf350ab5..23796bd63f 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page.yaml @@ -39,6 +39,7 @@ select_permissions: - widthRatio - xOffset - yOffset + - infiniteCanvas filter: meetingId: _eq: X-Hasura-PresenterInMeeting diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_curr.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_curr.yaml index 3cab393855..99bd8a66f9 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_curr.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_pres_page_curr.yaml @@ -37,6 +37,7 @@ select_permissions: - widthRatio - xOffset - yOffset + - infiniteCanvas filter: meetingId: _eq: X-Hasura-MeetingId diff --git a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts index 48b4be985c..3f5e643bfe 100644 --- a/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/Types/meetingClientSettings.ts @@ -694,6 +694,7 @@ export interface Whiteboard { maxStickyNoteLength: number maxNumberOfAnnotations: number annotations: Annotations + allowInfiniteCanvas: boolean styles: Styles toolbar: Toolbar } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index 74b97d11c4..7cb1d9dc02 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -595,6 +595,7 @@ class Presentation extends PureComponent { totalPages, userIsPresenter, hasPoll, + currentPresentationPage, } = this.props; const { zoom, isPanning } = this.state; @@ -619,6 +620,7 @@ class Presentation extends PureComponent { layoutContextDispatch, presentationIsOpen, userIsPresenter, + currentPresentationPage, }} setIsPanning={this.setIsPanning} isPanning={isPanning} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/container.jsx index 70dbabeed6..0a895a38df 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/container.jsx @@ -125,6 +125,7 @@ const PresentationContainer = (props) => { num: currentPresentationPage?.num, presentationId: currentPresentationPage?.presentationId, svgUri: slideSvgUrl, + infiniteCanvas: currentPresentationPage.infiniteCanvas, } : null; let slidePosition; @@ -201,42 +202,43 @@ const PresentationContainer = (props) => { return ( ); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index fb79ee17a2..0521d361a2 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -100,6 +100,15 @@ export const PRESENTATION_PUBLISH_CURSOR = gql` } `; +export const PRESENTATION_SET_PAGE_INFINITE_CANVAS = gql` + mutation PresentationSetPageInfiniteCanvas($pageId: String!, $infiniteCanvas: Boolean!) { + presentationSetPageInfiniteCanvas( + pageId: $pageId, + infiniteCanvas: $infiniteCanvas + ) + } +`; + export default { PRESENTATION_SET_ZOOM, PRESENTATION_SET_WRITERS, @@ -111,4 +120,5 @@ export default { PRES_ANNOTATION_DELETE, PRES_ANNOTATION_SUBMIT, PRESENTATION_PUBLISH_CURSOR, + PRESENTATION_SET_PAGE_INFINITE_CANVAS, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 06333f46b8..e1e6a9ccd8 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -89,6 +89,14 @@ const intlMessages = defineMessages({ id: 'app.whiteboard.toolbar.multiUserOff', description: 'Whiteboard toolbar turn multi-user off menu', }, + infiniteCanvasOn: { + id: 'app.whiteboard.toolbar.infiniteCanvasOn', + description: 'Whiteboard toolbar turn infinite canvas on', + }, + infiniteCanvasOff: { + id: 'app.whiteboard.toolbar.infiniteCanvasOff', + description: 'Whiteboard toolbar turn infinite canvas off', + }, pan: { id: 'app.whiteboard.toolbar.tools.hand', description: 'presentation toolbar pan label', @@ -120,7 +128,9 @@ class PresentationToolbar extends PureComponent { } componentDidUpdate(prevProps) { - const { zoom, setIsPanning, fitToWidth, fitToWidthHandler, currentSlideNum } = this.props; + const { + zoom, setIsPanning, fitToWidth, fitToWidthHandler, currentSlideNum, + } = this.props; const { wasFTWActive } = this.state; if (zoom <= HUNDRED_PERCENT && zoom !== prevProps.zoom && !fitToWidth) setIsPanning(); @@ -129,7 +139,7 @@ class PresentationToolbar extends PureComponent { setTimeout(() => { fitToWidthHandler(); this.setWasActive(false); - }, 150) + }, 150); } } @@ -137,10 +147,6 @@ class PresentationToolbar extends PureComponent { document.removeEventListener('keydown', this.switchSlide); } - setWasActive(wasFTWActive) { - this.setState({ wasFTWActive }); - } - handleFTWSlideChange() { const { fitToWidth, fitToWidthHandler } = this.props; if (fitToWidth) { @@ -171,6 +177,10 @@ class PresentationToolbar extends PureComponent { return addWhiteboardGlobalAccess(whiteboardId); } + setWasActive(wasFTWActive) { + this.setState({ wasFTWActive }); + } + fullscreenToggleHandler() { const { fullscreenElementId, @@ -343,6 +353,9 @@ class PresentationToolbar extends PureComponent { slidePosition, multiUserSize, multiUser, + setPresentationPageInfiniteCanvas, + allowInfiniteCanvas, + infiniteCanvasIcon, } = this.props; const { isMobile } = deviceInfo; @@ -360,6 +373,8 @@ class PresentationToolbar extends PureComponent { : `${intl.formatMessage(intlMessages.nextSlideLabel)} (${currentSlideNum >= 1 ? currentSlideNum + 1 : '' })`; + const isInfiniteCanvas = currentSlide?.infiniteCanvas; + return ( + {(allowInfiniteCanvas) && ( + { setPresentationPageInfiniteCanvas(!isInfiniteCanvas); }} + label={ + isInfiniteCanvas + ? intl.formatMessage(intlMessages.infiniteCanvasOff) + : intl.formatMessage(intlMessages.infiniteCanvasOn) + } + hideLabel + /> + )} + diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index adcc2d98db..1bc8c7a731 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -5,14 +5,85 @@ import FullscreenService from '/imports/ui/components/common/fullscreen-button/s import { useIsPollingEnabled } from '/imports/ui/services/features'; import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context'; import { POLL_CANCEL, POLL_CREATE } from '/imports/ui/components/poll/mutations'; -import { PRESENTATION_SET_PAGE } from '../mutations'; +import { PRESENTATION_SET_PAGE, PRESENTATION_SET_PAGE_INFINITE_CANVAS } from '../mutations'; import PresentationToolbar from './component'; import Session from '/imports/ui/services/storage/in-memory'; +const infiniteCanvasIcon = (isInfiniteCanvas) => { + if (isInfiniteCanvas) { + return ( + + + + + + ); + } + return ( + + + + + + ); +}; + const PresentationToolbarContainer = (props) => { const pluginsContext = useContext(PluginsContext); const { pluginsExtensibleAreasAggregatedState } = pluginsContext; + const WHITEBOARD_CONFIG = window.meetingClientSettings.public.whiteboard; + const { userIsPresenter, layoutSwapped, @@ -20,6 +91,8 @@ const PresentationToolbarContainer = (props) => { presentationId, numberOfSlides, hasPoll, + currentSlide, + currentPresentationPage, } = props; const handleToggleFullScreen = (ref) => FullscreenService.toggleFullScreen(ref); @@ -27,6 +100,7 @@ const PresentationToolbarContainer = (props) => { const [stopPoll] = useMutation(POLL_CANCEL); const [createPoll] = useMutation(POLL_CREATE); const [presentationSetPage] = useMutation(PRESENTATION_SET_PAGE); + const [presentationSetPageInfiniteCanvas] = useMutation(PRESENTATION_SET_PAGE_INFINITE_CANVAS); const endCurrentPoll = () => { if (hasPoll) stopPoll(); @@ -41,6 +115,16 @@ const PresentationToolbarContainer = (props) => { }); }; + const setPresentationPageInfiniteCanvas = (infiniteCanvas) => { + const pageId = `${presentationId}/${currentSlideNum}`; + presentationSetPageInfiniteCanvas({ + variables: { + pageId, + infiniteCanvas, + }, + }); + }; + const skipToSlide = (slideNum) => { const slideId = `${presentationId}/${slideNum}`; setPresentationPage(slideId); @@ -93,6 +177,7 @@ const PresentationToolbarContainer = (props) => { amIPresenter={userIsPresenter} endCurrentPoll={endCurrentPoll} isPollingEnabled={isPollingEnabled} + allowInfiniteCanvas={WHITEBOARD_CONFIG?.allowInfiniteCanvas} // TODO: Remove this isMeteorConnected {...{ @@ -102,6 +187,10 @@ const PresentationToolbarContainer = (props) => { previousSlide, nextSlide, skipToSlide, + setPresentationPageInfiniteCanvas, + currentSlide, + currentPresentationPage, + infiniteCanvasIcon, }} /> ); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js index 6a58c8e5d2..47aabd96ee 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js @@ -281,6 +281,34 @@ const WBAccessButton = styled(Button)` } `; +const InfiniteCanvasButton = styled(Button)` + border: none !important; + + svg { + [dir="rtl"] & { + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); + } + } + + position: relative; + color: ${toolbarButtonColor}; + background-color: ${colorOffWhite}; + border-radius: 0; + box-shadow: none !important; + border: 0; + margin-left: 2px; + margin-right: 2px; + + &:focus { + background-color: ${colorOffWhite}; + border: 0; + } +`; + export default { PresentationToolbarWrapper, QuickPollButton, @@ -294,4 +322,5 @@ export default { MultiUserTool, WBAccessButton, MUTPlaceholder, + InfiniteCanvasButton, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx index 4247946264..c19d33cb1e 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx @@ -147,6 +147,7 @@ class ZoomTool extends PureComponent { intl, isMeteorConnected, step, + isInfiniteCanvas, } = this.props; const { stateZoomValue } = this.state; @@ -170,6 +171,7 @@ class ZoomTool extends PureComponent { exec={this.decrement} value={zoomValue} minBound={minBound} + isInfiniteCanvas={isInfiniteCanvas} > { }} - disabled={(zoomValue <= minBound) || !isMeteorConnected} + disabled={(zoomValue <= minBound) || !isMeteorConnected || isInfiniteCanvas} hideLabel /> @@ -213,6 +215,7 @@ class ZoomTool extends PureComponent { exec={this.increment} value={zoomValue} maxBound={maxBound} + isInfiniteCanvas={isInfiniteCanvas} > { }} - disabled={(zoomValue >= maxBound) || !isMeteorConnected} + disabled={(zoomValue >= maxBound) || !isMeteorConnected || isInfiniteCanvas} hideLabel /> diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component.jsx index ae1026f370..43a436ab66 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/holdButton/component.jsx @@ -26,9 +26,10 @@ class HoldDownButton extends PureComponent { minBound, maxBound, value, + isInfiniteCanvas } = this.props; const bounds = (value === maxBound) || (value === minBound); - if (bounds) return; + if (bounds || isInfiniteCanvas) return; exec(); } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index e7755fa29d..414264ec52 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -117,6 +117,7 @@ const Whiteboard = React.memo(function Whiteboard(props) { locale, darkTheme, selectedLayout, + isInfiniteCanvas, } = props; clearTldrawCache(); @@ -648,7 +649,7 @@ const Whiteboard = React.memo(function Whiteboard(props) { // Adjust camera position to ensure it stays within bounds const panned = next?.id?.includes("camera") && (prev.x !== next.x || prev.y !== next.y); - if (panned) { + if (panned && !currentPresentationPageRef.current?.infiniteCanvas) { // Horizontal bounds check if (next.x > 0) { next.x = 0; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts b/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts index 4fbe2218f2..763f38f5d0 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/queries.ts @@ -53,6 +53,7 @@ export const CURRENT_PRESENTATION_PAGE_SUBSCRIPTION = gql`subscription CurrentPr downloadable presentationName isDefaultPresentation + infiniteCanvas } }`; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js index df4d8b10a2..bcb7f4127c 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/styles.js @@ -51,7 +51,6 @@ const TldrawV2GlobalStyle = createGlobalStyle` position: relative; } - // Add the following lines to override height and width attributes for .tl-overlays__item .tl-overlays__item { height: auto !important; width: auto !important; @@ -103,6 +102,7 @@ const TldrawV2GlobalStyle = createGlobalStyle` } `} + .tlui-helper-buttons, [data-testid="main.page-menu"], [data-testid="main.menu"], [data-testid="tools.more.laser"], diff --git a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts index c0d9b06a3a..f9198e9213 100644 --- a/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts +++ b/bigbluebutton-html5/imports/ui/core/initial-values/meetingClientSettings.ts @@ -810,6 +810,7 @@ export const meetingClientSettingsInitialValues: MeetingClientSettings = { pointerDiameter: 5, maxStickyNoteLength: 1000, maxNumberOfAnnotations: 300, + allowInfiniteCanvas: true, annotations: { status: { start: 'DRAW_START', diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 87e4b4fa5b..dffafe41e0 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -997,6 +997,7 @@ public: whiteboard: annotationsQueueProcessInterval: 60 cursorInterval: 150 + allowInfiniteCanvas: true pointerDiameter: 5 maxStickyNoteLength: 1000 # limit number of annotations per slide diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 61e0f901a5..04d37d2579 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -1180,6 +1180,8 @@ "app.whiteboard.toolbar.clearConfirmation": "Are you sure you want to clear all annotations?", "app.whiteboard.toolbar.multiUserOn": "Turn multi-user whiteboard on", "app.whiteboard.toolbar.multiUserOff": "Turn multi-user whiteboard off", + "app.whiteboard.toolbar.infiniteCanvasOn": "Turn infinite canvas on", + "app.whiteboard.toolbar.infiniteCanvasOff": "Turn infinite canvas off", "app.whiteboard.toolbar.palmRejectionOn": "Turn palm rejection on", "app.whiteboard.toolbar.palmRejectionOff": "Turn palm rejection off", "app.whiteboard.toolbar.fontSize": "Font size list",