add functionality to enable infinite whiteboard

This commit is contained in:
KDSBrowne 2024-06-14 13:58:32 +00:00
parent 5cc9604a54
commit 687ee36d29
30 changed files with 398 additions and 83 deletions

View File

@ -11,6 +11,7 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
with PresentationConversionCompletedSysPubMsgHdlr
with PdfConversionInvalidErrorSysPubMsgHdlr
with SetCurrentPagePubMsgHdlr
with SetPageInfiniteCanvasPubMsgHdlr
with SetPresenterInDefaultPodInternalMsgHdlr
with RemovePresentationPubMsgHdlr
with SetPresentationDownloadablePubMsgHdlr

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
import { RedisMessage } from '../types';
import {throwErrorIfInvalidInput, throwErrorIfNotPresenter} from "../imports/validation";
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): 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 };
}

View File

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

View File

@ -357,6 +357,13 @@ type Mutation {
): Boolean
}
type Mutation {
presentationSetPageInfiniteCanvas(
infiniteCanvas: Boolean!
pageId: String!
): Boolean
}
type Mutation {
presentationSetRenderedInToast(
presentationId: String!

View File

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

View File

@ -39,6 +39,7 @@ select_permissions:
- widthRatio
- xOffset
- yOffset
- infiniteCanvas
filter:
meetingId:
_eq: X-Hasura-PresenterInMeeting

View File

@ -37,6 +37,7 @@ select_permissions:
- widthRatio
- xOffset
- yOffset
- infiniteCanvas
filter:
meetingId:
_eq: X-Hasura-MeetingId

View File

@ -694,6 +694,7 @@ export interface Whiteboard {
maxStickyNoteLength: number
maxNumberOfAnnotations: number
annotations: Annotations
allowInfiniteCanvas: boolean
styles: Styles
toolbar: Toolbar
}

View File

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

View File

@ -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 (
<Presentation
{
...{
layoutContextDispatch,
numCameras,
...props,
userIsPresenter,
presentationBounds: presentation,
fullscreenContext,
fullscreenElementId,
isMobile: deviceType === DEVICE_TYPE.MOBILE,
isIphone,
currentSlide,
slidePosition,
downloadPresentationUri: `${APP_CONFIG.bbbWebBase}/${currentPresentationPage?.downloadFileUri}`,
multiUser: (multiUserData.hasAccess || multiUserData.active) && presentationIsOpen,
presentationIsDownloadable: currentPresentationPage?.downloadable,
mountPresentation: !!currentSlide,
currentPresentationId: currentPresentationPage?.presentationId,
totalPages: currentPresentationPage?.totalPages || 0,
notify,
zoomSlide,
publishedPoll: poll?.published || false,
restoreOnUpdate: getFromUserSettings(
'bbb_force_restore_presentation_on_new_events',
window.meetingClientSettings.public.presentation.restoreOnUpdate,
),
addWhiteboardGlobalAccess: getUsers,
removeWhiteboardGlobalAccess,
multiUserSize: multiUserData.size,
isViewersAnnotationsLocked,
setPresentationIsOpen: MediaService.setPresentationIsOpen,
isDefaultPresentation: currentPresentationPage?.isDefaultPresentation,
presentationName: currentPresentationPage?.presentationName,
presentationAreaSize,
currentUser,
hasPoll,
}
...{
layoutContextDispatch,
numCameras,
...props,
userIsPresenter,
presentationBounds: presentation,
fullscreenContext,
fullscreenElementId,
isMobile: deviceType === DEVICE_TYPE.MOBILE,
isIphone,
currentSlide,
slidePosition,
downloadPresentationUri: `${APP_CONFIG.bbbWebBase}/${currentPresentationPage?.downloadFileUri}`,
multiUser: (multiUserData.hasAccess || multiUserData.active) && presentationIsOpen,
presentationIsDownloadable: currentPresentationPage?.downloadable,
mountPresentation: !!currentSlide,
currentPresentationId: currentPresentationPage?.presentationId,
totalPages: currentPresentationPage?.totalPages || 0,
notify,
zoomSlide,
publishedPoll: poll?.published || false,
restoreOnUpdate: getFromUserSettings(
'bbb_force_restore_presentation_on_new_events',
window.meetingClientSettings.public.presentation.restoreOnUpdate,
),
addWhiteboardGlobalAccess: getUsers,
removeWhiteboardGlobalAccess,
multiUserSize: multiUserData.size,
isViewersAnnotationsLocked,
setPresentationIsOpen: MediaService.setPresentationIsOpen,
isDefaultPresentation: currentPresentationPage?.isDefaultPresentation,
presentationName: currentPresentationPage?.presentationName,
presentationAreaSize,
currentUser,
hasPoll,
currentPresentationPage,
}
}
/>
);

View File

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

View File

@ -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 (
<Styled.PresentationToolbarWrapper
id="presentationToolbarWrapper"
@ -433,6 +448,30 @@ class PresentationToolbar extends PureComponent {
/>
</Styled.PresentationSlideControls>
<Styled.PresentationZoomControls>
{(allowInfiniteCanvas) && (
<Styled.InfiniteCanvasButton
data-test={isInfiniteCanvas ? 'turnInfiniteCanvasOff' : 'turnInfiniteCanvasOn'}
role="button"
aria-label={
isInfiniteCanvas
? intl.formatMessage(intlMessages.infiniteCanvasOff)
: intl.formatMessage(intlMessages.infiniteCanvasOn)
}
color="light"
disabled={!isMeteorConnected}
customIcon={infiniteCanvasIcon(isInfiniteCanvas)}
size="md"
circle
onClick={() => { setPresentationPageInfiniteCanvas(!isInfiniteCanvas); }}
label={
isInfiniteCanvas
? intl.formatMessage(intlMessages.infiniteCanvasOff)
: intl.formatMessage(intlMessages.infiniteCanvasOn)
}
hideLabel
/>
)}
<Styled.WBAccessButton
data-test={multiUser ? 'turnMultiUsersWhiteboardOff' : 'turnMultiUsersWhiteboardOn'}
role="button"
@ -473,6 +512,7 @@ class PresentationToolbar extends PureComponent {
minBound={HUNDRED_PERCENT}
maxBound={MAX_PERCENT}
step={STEP}
isInfiniteCanvas={isInfiniteCanvas}
isMeteorConnected={isMeteorConnected}
/>
</TooltipContainer>

View File

@ -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 (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.6667 3H1.33333C1.14924 3 1 3.14924 1 3.33333V13.3333C1 13.5174
1.14924 13.6667 1.33333 13.6667H14.6667C14.8508 13.6667 15 13.5174 15 13.3333V3.33333C15
3.14924 14.8508 3 14.6667 3ZM1.33333 2C0.596954 2 0 2.59695 0 3.33333V13.3333C0 14.0697
0.596953 14.6667 1.33333 14.6667H14.6667C15.403 14.6667 16 14.0697 16 13.3333V3.33333C16
2.59695 15.403 2 14.6667 2H1.33333Z"
fill="#4E5A66"
/>
<path
d="M12.875 11.875L9.125 8.125M9.125 8.125L9.125 10.9375M9.125 8.125L11.9375 8.125"
stroke="#4E5A66"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3.125 5.125L6.875 8.875M6.875 8.875L6.875 6.0625M6.875 8.875L4.0625 8.875"
stroke="#4E5A66"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.6667 3H1.33333C1.14924 3 1 3.14924 1 3.33333V13.3333C1 13.5174
1.14924 13.6667 1.33333 13.6667H14.6667C14.8508 13.6667 15 13.5174 15 13.3333V3.33333C15
3.14924 14.8508 3 14.6667 3ZM1.33333 2C0.596954 2 0 2.59695 0 3.33333V13.3333C0 14.0697
0.596953 14.6667 1.33333 14.6667H14.6667C15.403 14.6667 16 14.0697 16 13.3333V3.33333C16
2.59695 15.403 2 14.6667 2H1.33333Z"
fill="#4E5A66"
/>
<path
d="M9.125 8.125L12.875 11.875M12.875 11.875L12.875 9.0625M12.875 11.875L10.0625 11.875"
stroke="#4E5A66"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6.875 8.875L3.125 5.125M3.125 5.125L3.125 7.9375M3.125 5.125L5.9375 5.125"
stroke="#4E5A66"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
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,
}}
/>
);

View File

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

View File

@ -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}
>
<Styled.DecreaseZoomButton
color="light"
@ -182,7 +184,7 @@ class ZoomTool extends PureComponent {
data-test="zoomOutBtn"
icon="substract"
onClick={() => { }}
disabled={(zoomValue <= minBound) || !isMeteorConnected}
disabled={(zoomValue <= minBound) || !isMeteorConnected || isInfiniteCanvas}
hideLabel
/>
<div id="zoomOutDescription" hidden>{intl.formatMessage(intlMessages.zoomOutDesc)}</div>
@ -213,6 +215,7 @@ class ZoomTool extends PureComponent {
exec={this.increment}
value={zoomValue}
maxBound={maxBound}
isInfiniteCanvas={isInfiniteCanvas}
>
<Styled.IncreaseZoomButton
color="light"
@ -225,7 +228,7 @@ class ZoomTool extends PureComponent {
data-test="zoomInBtn"
icon="add"
onClick={() => { }}
disabled={(zoomValue >= maxBound) || !isMeteorConnected}
disabled={(zoomValue >= maxBound) || !isMeteorConnected || isInfiniteCanvas}
hideLabel
/>
<div id="zoomInDescription" hidden>{intl.formatMessage(intlMessages.zoomInDesc)}</div>

View File

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

View File

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

View File

@ -53,6 +53,7 @@ export const CURRENT_PRESENTATION_PAGE_SUBSCRIPTION = gql`subscription CurrentPr
downloadable
presentationName
isDefaultPresentation
infiniteCanvas
}
}`;

View File

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

View File

@ -810,6 +810,7 @@ export const meetingClientSettingsInitialValues: MeetingClientSettings = {
pointerDiameter: 5,
maxStickyNoteLength: 1000,
maxNumberOfAnnotations: 300,
allowInfiniteCanvas: true,
annotations: {
status: {
start: 'DRAW_START',

View File

@ -997,6 +997,7 @@ public:
whiteboard:
annotationsQueueProcessInterval: 60
cursorInterval: 150
allowInfiniteCanvas: true
pointerDiameter: 5
maxStickyNoteLength: 1000
# limit number of annotations per slide

View File

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