bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx

394 lines
11 KiB
React
Raw Normal View History

2024-03-12 23:03:43 +08:00
import React, {
useEffect,
useRef,
useState,
useMemo,
useCallback,
2024-03-12 23:03:43 +08:00
} from 'react';
import PropTypes from 'prop-types';
import { useMutation, useQuery } from '@apollo/client';
2024-03-09 08:39:57 +08:00
import {
AssetRecordType,
2024-05-23 05:49:28 +08:00
} from '@bigbluebutton/tldraw';
2024-03-12 23:03:43 +08:00
import { throttle } from 'radash';
2023-09-28 04:42:47 +08:00
import {
CURRENT_PRESENTATION_PAGE_SUBSCRIPTION,
2023-09-28 10:15:33 +08:00
CURRENT_PAGE_ANNOTATIONS_STREAM,
CURRENT_PAGE_ANNOTATIONS_QUERY,
2023-10-12 04:17:10 +08:00
CURRENT_PAGE_WRITERS_SUBSCRIPTION,
2023-09-28 04:42:47 +08:00
} from './queries';
import {
initDefaultPages,
persistShape,
notifyNotAllowedChange,
notifyShapeNumberExceeded,
toggleToolsAnimations,
formatAnnotations,
} from './service';
2024-07-27 05:12:45 +08:00
import { getSettingsSingletonInstance } from '/imports/ui/services/settings';
2023-09-28 04:42:47 +08:00
import Auth from '/imports/ui/services/auth';
import {
layoutSelect,
layoutDispatch,
} from '/imports/ui/components/layout/context';
import FullscreenService from '/imports/ui/components/common/fullscreen-button/service';
import deviceInfo from '/imports/utils/deviceInfo';
import Whiteboard from './component';
2023-11-23 04:01:18 +08:00
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
2024-01-24 19:37:51 +08:00
import {
PRESENTATION_SET_ZOOM,
PRES_ANNOTATION_DELETE,
PRES_ANNOTATION_SUBMIT,
2024-02-27 01:06:18 +08:00
PRESENTATION_SET_PAGE,
2024-03-12 23:03:43 +08:00
PRESENTATION_PUBLISH_CURSOR,
2024-01-24 19:37:51 +08:00
} from '../presentation/mutations';
import { useMergedCursorData } from './hooks.ts';
import useDeduplicatedSubscription from '../../core/hooks/useDeduplicatedSubscription';
import MediaService from '/imports/ui/components/media/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
const FORCE_RESTORE_PRESENTATION_ON_NEW_EVENTS = 'bbb_force_restore_presentation_on_new_events';
2023-09-28 04:42:47 +08:00
const WhiteboardContainer = (props) => {
const {
intl,
zoomChanger,
2023-09-28 04:42:47 +08:00
} = props;
const WHITEBOARD_CONFIG = window.meetingClientSettings.public.whiteboard;
const layoutContextDispatch = layoutDispatch();
const [annotations, setAnnotations] = useState([]);
2024-02-21 10:18:36 +08:00
const [shapes, setShapes] = useState({});
2024-07-01 22:19:42 +08:00
const [currentPresentationPage, setCurrentPresentationPage] = useState(null);
const meeting = useMeeting((m) => ({
lockSettings: m?.lockSettings,
}));
const { data: currentUser } = useCurrentUser((user) => ({
presenter: user.presenter,
isModerator: user.isModerator,
userId: user.userId,
}));
const isPresenter = currentUser?.presenter;
const isModerator = currentUser?.isModerator;
const { data: presentationPageData } = useDeduplicatedSubscription(
CURRENT_PRESENTATION_PAGE_SUBSCRIPTION,
);
2023-09-28 04:42:47 +08:00
const { pres_page_curr: presentationPageArray } = (presentationPageData || {});
2024-07-01 22:19:42 +08:00
const newPresentationPage = presentationPageArray && presentationPageArray[0];
useEffect(() => {
if (newPresentationPage) {
setCurrentPresentationPage(newPresentationPage);
}
}, [newPresentationPage]);
2024-03-09 08:39:57 +08:00
const curPageNum = currentPresentationPage?.num;
const curPageId = currentPresentationPage?.pageId;
2024-07-06 04:09:19 +08:00
const isInfiniteWhiteboard = currentPresentationPage?.infiniteWhiteboard;
2024-03-09 08:39:57 +08:00
const curPageIdRef = useRef();
React.useEffect(() => {
curPageIdRef.current = curPageId;
}, [curPageId]);
2023-09-28 04:42:47 +08:00
const presentationId = currentPresentationPage?.presentationId;
const { data: whiteboardWritersData } = useDeduplicatedSubscription(
CURRENT_PAGE_WRITERS_SUBSCRIPTION,
{
variables: { pageId: curPageId },
skip: !curPageId,
},
);
2023-10-12 04:17:10 +08:00
const whiteboardWriters = whiteboardWritersData?.pres_page_writers || [];
const hasWBAccess = whiteboardWriters?.some((writer) => writer.userId === Auth.userID);
const [presentationSetZoom] = useMutation(PRESENTATION_SET_ZOOM);
2024-02-27 01:06:18 +08:00
const [presentationSetPage] = useMutation(PRESENTATION_SET_PAGE);
2024-01-23 22:31:39 +08:00
const [presentationDeleteAnnotations] = useMutation(PRES_ANNOTATION_DELETE);
2024-01-24 19:37:51 +08:00
const [presentationSubmitAnnotations] = useMutation(PRES_ANNOTATION_SUBMIT);
2024-03-12 23:03:43 +08:00
const [presentationPublishCursor] = useMutation(PRESENTATION_PUBLISH_CURSOR);
2024-01-23 22:31:39 +08:00
2024-02-27 01:06:18 +08:00
const setPresentationPage = (pageId) => {
presentationSetPage({
variables: {
presentationId,
pageId,
},
});
};
const skipToSlide = (slideNum) => {
const slideId = `${presentationId}/${slideNum}`;
setPresentationPage(slideId);
};
2024-01-23 22:31:39 +08:00
const removeShapes = (shapeIds) => {
presentationDeleteAnnotations({
variables: {
2024-03-09 08:39:57 +08:00
pageId: curPageIdRef.current,
2024-01-23 22:31:39 +08:00
annotationsIds: shapeIds,
},
});
};
const zoomSlide = (
widthRatio, heightRatio, xOffset, yOffset, currPage = currentPresentationPage,
) => {
const { pageId, num } = currPage;
presentationSetZoom({
variables: {
presentationId,
pageId,
pageNum: num,
xOffset,
yOffset,
widthRatio,
heightRatio,
},
});
};
2024-01-24 19:37:51 +08:00
const submitAnnotations = async (newAnnotations) => {
2024-01-24 20:18:42 +08:00
const isAnnotationSent = await presentationSubmitAnnotations({
2024-01-24 19:37:51 +08:00
variables: {
2024-03-09 08:39:57 +08:00
pageId: curPageIdRef.current,
2024-01-24 19:37:51 +08:00
annotations: newAnnotations,
},
});
2024-01-24 20:18:42 +08:00
return isAnnotationSent?.data?.presAnnotationSubmit;
2024-01-24 19:37:51 +08:00
};
const persistShapeWrapper = (shape, whiteboardId, amIModerator) => {
persistShape(shape, whiteboardId, amIModerator, submitAnnotations);
2024-01-24 19:37:51 +08:00
};
const publishCursorUpdate = useCallback((payload) => {
2024-03-12 23:03:43 +08:00
const { whiteboardId, xPercent, yPercent } = payload;
if (!whiteboardId || !xPercent || !yPercent || !(hasWBAccess || isPresenter)) return;
2024-04-23 02:08:50 +08:00
2024-03-12 23:03:43 +08:00
presentationPublishCursor({
variables: {
whiteboardId,
xPercent,
yPercent,
},
});
}, [hasWBAccess, isPresenter]);
2024-03-12 23:03:43 +08:00
const throttledPublishCursorUpdate = useMemo(() => throttle(
{ interval: WHITEBOARD_CONFIG.cursorInterval },
publishCursorUpdate,
), [publishCursorUpdate]);
2024-03-12 23:03:43 +08:00
2023-10-13 00:30:39 +08:00
const isMultiUserActive = whiteboardWriters?.length > 0;
const cursorArray = useMergedCursorData();
const { data: annotationStreamData } = useDeduplicatedSubscription(
2023-09-28 10:15:33 +08:00
CURRENT_PAGE_ANNOTATIONS_STREAM,
{
variables: { lastUpdatedAt: new Date(0).toISOString() },
2023-09-28 10:26:30 +08:00
},
2023-09-28 10:15:33 +08:00
);
const { data: initialPageAnnotations, refetch: refetchInitialPageAnnotations } = useQuery(
CURRENT_PAGE_ANNOTATIONS_QUERY,
{
skip: !curPageId,
2024-05-17 03:47:05 +08:00
},
);
React.useEffect(() => {
if (curPageIdRef.current) {
refetchInitialPageAnnotations();
}
}, [curPageIdRef.current]);
const processAnnotations = (data) => {
const newAnnotations = [];
const annotationsToBeRemoved = [];
2024-05-17 03:47:05 +08:00
data.forEach((item) => {
if (item.annotationInfo === '') {
annotationsToBeRemoved.push(item.annotationId);
} else {
newAnnotations.push(item);
}
});
2024-05-17 03:47:05 +08:00
const currentAnnotations = annotations.filter(
(annotation) => !annotationsToBeRemoved.includes(annotation.annotationId),
);
2024-05-17 03:47:05 +08:00
setAnnotations([...currentAnnotations, ...newAnnotations]);
if (newAnnotations.length) {
const restoreOnUpdate = getFromUserSettings(
FORCE_RESTORE_PRESENTATION_ON_NEW_EVENTS,
window.meetingClientSettings.public.presentation.restoreOnUpdate,
);
if (restoreOnUpdate) {
MediaService.setPresentationIsOpen(layoutContextDispatch, true);
}
}
2024-05-17 03:47:05 +08:00
};
React.useEffect(() => {
if (initialPageAnnotations && initialPageAnnotations.pres_annotation_curr) {
processAnnotations(initialPageAnnotations.pres_annotation_curr);
}
}, [initialPageAnnotations]);
useEffect(() => {
const { pres_annotation_curr_stream: annotationStream } = annotationStreamData || {};
if (annotationStream) {
processAnnotations(annotationStream);
2023-09-28 10:15:33 +08:00
}
}, [annotationStreamData]);
2024-03-09 08:39:57 +08:00
const bgShape = [];
2023-09-28 04:42:47 +08:00
2024-02-21 10:18:36 +08:00
React.useEffect(() => {
const updatedShapes = formatAnnotations(
2024-03-09 08:39:57 +08:00
annotations.filter((annotation) => annotation.pageId === curPageIdRef.current),
2024-02-21 10:18:36 +08:00
intl,
2024-03-09 08:39:57 +08:00
curPageNum,
2024-02-21 10:18:36 +08:00
currentPresentationPage,
);
setShapes(updatedShapes);
2024-03-09 08:39:57 +08:00
}, [annotations, intl, curPageNum, currentPresentationPage]);
2023-09-28 04:42:47 +08:00
const { isIphone } = deviceInfo;
2024-03-09 08:39:57 +08:00
const assetId = AssetRecordType.createId(curPageNum);
const assets = [{
id: assetId,
2024-03-09 08:39:57 +08:00
typeName: 'asset',
2023-09-28 04:42:47 +08:00
type: 'image',
meta: {},
props: {
w: currentPresentationPage?.scaledWidth,
h: currentPresentationPage?.scaledHeight,
src: currentPresentationPage?.svgUrl,
2024-03-09 08:39:57 +08:00
name: '',
isAnimated: false,
mimeType: null,
2024-03-09 08:39:57 +08:00
},
}];
2023-09-28 04:42:47 +08:00
const Settings = getSettingsSingletonInstance();
const { isRTL } = Settings.application;
2023-09-28 04:42:47 +08:00
const width = layoutSelect((i) => i?.output?.presentation?.width);
const height = layoutSelect((i) => i?.output?.presentation?.height);
const sidebarNavigationWidth = layoutSelect(
2023-09-28 10:26:30 +08:00
(i) => i?.output?.sidebarNavigation?.width,
2023-09-28 04:42:47 +08:00
);
const { maxStickyNoteLength, maxNumberOfAnnotations } = WHITEBOARD_CONFIG;
const fontFamily = WHITEBOARD_CONFIG.styles.text.family;
2024-03-09 08:39:57 +08:00
const {
colorStyle, dashStyle, fillStyle, fontStyle, sizeStyle,
} = WHITEBOARD_CONFIG.styles;
2023-09-28 10:26:30 +08:00
const handleToggleFullScreen = (ref) => FullscreenService.toggleFullScreen(ref);
2023-09-28 04:42:47 +08:00
// use -0.5 offset to avoid white borders rounding erros
bgShape.push({
x: -0.5,
y: -0.5,
rotation: 0,
2023-09-28 04:42:47 +08:00
isLocked: true,
opacity: 1,
meta: {},
2024-03-09 08:39:57 +08:00
id: `shape:BG-${curPageNum}`,
type: 'image',
props: {
w: currentPresentationPage?.scaledWidth + 1.5 || 1,
h: currentPresentationPage?.scaledHeight + 1.5 || 1,
2024-03-09 08:39:57 +08:00
assetId,
playing: true,
2024-03-09 08:39:57 +08:00
url: '',
crop: null,
2023-09-28 04:42:47 +08:00
},
2024-03-09 08:39:57 +08:00
parentId: `page:${curPageNum}`,
index: 'a0',
typeName: 'shape',
});
2023-09-28 04:42:47 +08:00
2024-05-17 03:47:05 +08:00
if (!currentPresentationPage) return null;
2023-09-28 10:26:30 +08:00
return (
<Whiteboard
2024-07-01 22:19:42 +08:00
key={presentationId}
2023-09-28 10:26:30 +08:00
{...{
isPresenter,
isModerator,
currentUser,
isRTL,
width,
height,
maxStickyNoteLength,
maxNumberOfAnnotations,
fontFamily,
colorStyle,
dashStyle,
fillStyle,
fontStyle,
sizeStyle,
2023-09-28 10:26:30 +08:00
handleToggleFullScreen,
sidebarNavigationWidth,
layoutContextDispatch,
initDefaultPages,
2024-01-24 19:37:51 +08:00
persistShapeWrapper,
2023-09-28 10:26:30 +08:00
isMultiUserActive,
shapes,
bgShape,
2023-09-28 10:26:30 +08:00
assets,
removeShapes,
zoomSlide,
2023-09-28 10:26:30 +08:00
notifyNotAllowedChange,
notifyShapeNumberExceeded,
2024-07-27 05:12:45 +08:00
whiteboardToolbarAutoHide: Settings?.application?.whiteboardToolbarAutoHide,
animations: Settings?.application?.animations,
2023-09-28 10:26:30 +08:00
toggleToolsAnimations,
isIphone,
currentPresentationPage,
2023-10-18 00:35:48 +08:00
numberOfPages: currentPresentationPage?.totalPages,
2023-09-28 10:26:30 +08:00
presentationId,
2023-10-12 04:17:10 +08:00
hasWBAccess,
2023-10-13 00:30:39 +08:00
whiteboardWriters,
zoomChanger,
2024-02-27 01:06:18 +08:00
skipToSlide,
2024-07-27 05:12:45 +08:00
locale: Settings?.application?.locale,
darkTheme: Settings?.application?.darkTheme,
selectedLayout: Settings?.application?.selectedLayout,
2024-07-06 04:09:19 +08:00
isInfiniteWhiteboard,
curPageNum,
2023-09-28 10:26:30 +08:00
}}
{...props}
meetingId={Auth.meetingID}
2024-03-12 23:03:43 +08:00
publishCursorUpdate={throttledPublishCursorUpdate}
otherCursors={cursorArray}
hideViewersCursor={meeting?.data?.lockSettings?.hideViewersCursor}
2023-09-28 10:26:30 +08:00
/>
);
};
2023-09-28 04:42:47 +08:00
WhiteboardContainer.propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
zoomChanger: PropTypes.func.isRequired,
};
2024-03-09 08:39:57 +08:00
export default WhiteboardContainer;