Merge pull request #19783 from ramonlsouza/remove-unused-cursors

refactor: remove unused cursor code
This commit is contained in:
Ramón Souza 2024-03-18 11:03:30 -03:00 committed by GitHub
commit 379219085d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 96 additions and 610 deletions

View File

@ -0,0 +1,24 @@
import { RedisMessage } from '../types';
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
const eventName = `SendCursorPositionPubMsg`;
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 = {
whiteboardId: input.whiteboardId,
xPercent: input.xPercent,
yPercent: input.yPercent,
};
return { eventName, routing, header, body };
}

View File

@ -287,6 +287,14 @@ type Mutation {
): Boolean
}
type Mutation {
presentationPublishCursor(
whiteboardId: String!
xPercent: Float!
yPercent: Float!
): Boolean
}
type Mutation {
presentationRemove(
presentationId: String!

View File

@ -252,6 +252,12 @@ actions:
permissions:
- role: bbb_client
comment: presentationExport
- name: presentationPublishCursor
definition:
kind: synchronous
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
permissions:
- role: bbb_client
- name: presentationRemove
definition:
kind: synchronous

View File

@ -1 +0,0 @@
import './methods';

View File

@ -1,6 +0,0 @@
import { Meteor } from 'meteor/meteor';
import publishCursorUpdate from './methods/publishCursorUpdate';
Meteor.methods({
publishCursorUpdate,
});

View File

@ -1,10 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
export default function publishCursorUpdate(meetingId, requesterUserId, payload) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SendCursorPositionPubMsg';
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -90,6 +90,16 @@ export const PRES_ANNOTATION_SUBMIT = gql`
}
`;
export const PRESENTATION_PUBLISH_CURSOR = gql`
mutation PresentationPublishCursor($whiteboardId: String!, $xPercent: Float!, $yPercent: Float!) {
presentationPublishCursor(
whiteboardId: $whiteboardId,
xPercent: $xPercent,
yPercent: $yPercent,
)
}
`;
export default {
PRESENTATION_SET_ZOOM,
PRESENTATION_SET_WRITERS,
@ -100,4 +110,5 @@ export default {
PRESENTATION_REMOVE,
PRES_ANNOTATION_DELETE,
PRES_ANNOTATION_SUBMIT,
PRESENTATION_PUBLISH_CURSOR,
};

View File

@ -1,14 +1,20 @@
import React, { useEffect, useRef, useState } from 'react';
import React, {
useEffect,
useRef,
useState,
useMemo,
} from 'react';
import { useSubscription, useMutation } from '@apollo/client';
import {
AssetRecordType,
} from '@tldraw/tldraw';
import { throttle } from 'radash';
import {
CURRENT_PRESENTATION_PAGE_SUBSCRIPTION,
CURRENT_PAGE_ANNOTATIONS_STREAM,
CURRENT_PAGE_WRITERS_SUBSCRIPTION,
CURSOR_SUBSCRIPTION,
} from './queries';
import { CURSOR_SUBSCRIPTION } from './cursors/queries';
import {
initDefaultPages,
persistShape,
@ -17,7 +23,6 @@ import {
toggleToolsAnimations,
formatAnnotations,
} from './service';
import CursorService from './cursors/service';
import SettingsService from '/imports/ui/services/settings';
import Auth from '/imports/ui/services/auth';
import {
@ -35,6 +40,7 @@ import {
PRES_ANNOTATION_DELETE,
PRES_ANNOTATION_SUBMIT,
PRESENTATION_SET_PAGE,
PRESENTATION_PUBLISH_CURSOR,
} from '../presentation/mutations';
const WHITEBOARD_CONFIG = window.meetingClientSettings.public.whiteboard;
@ -76,6 +82,7 @@ const WhiteboardContainer = (props) => {
const [presentationSetPage] = useMutation(PRESENTATION_SET_PAGE);
const [presentationDeleteAnnotations] = useMutation(PRES_ANNOTATION_DELETE);
const [presentationSubmitAnnotations] = useMutation(PRES_ANNOTATION_SUBMIT);
const [presentationPublishCursor] = useMutation(PRESENTATION_PUBLISH_CURSOR);
const setPresentationPage = (pageId) => {
presentationSetPage({
@ -131,6 +138,23 @@ const WhiteboardContainer = (props) => {
persistShape(shape, whiteboardId, isModerator, submitAnnotations);
};
const publishCursorUpdate = (payload) => {
const { whiteboardId, xPercent, yPercent } = payload;
presentationPublishCursor({
variables: {
whiteboardId,
xPercent,
yPercent,
},
});
};
const throttledPublishCursorUpdate = useMemo(() => throttle(
{ interval: WHITEBOARD_CONFIG.cursorInterval },
publishCursorUpdate,
), []);
const isMultiUserActive = whiteboardWriters?.length > 0;
const { data: currentUser } = useCurrentUser((user) => ({
@ -282,7 +306,7 @@ const WhiteboardContainer = (props) => {
}}
{...props}
meetingId={Auth.meetingID}
publishCursorUpdate={CursorService.publishCursorUpdate}
publishCursorUpdate={throttledPublishCursorUpdate}
otherCursors={cursorArray}
hideViewersCursor={meeting?.data?.lockSettings?.hideViewersCursor}
/>

View File

@ -1,337 +0,0 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
import Cursor from './cursor/component';
import PositionLabel from './position-label/component';
const XS_OFFSET = 8;
const SMALL_OFFSET = 18;
const XL_OFFSET = 85;
const BOTTOM_CAM_HANDLE_HEIGHT = 10;
const PRES_TOOLBAR_HEIGHT = 35;
const baseName = window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename;
const makeCursorUrl = (filename) => `${baseName}/resources/images/whiteboard-cursor/${filename}`;
const TOOL_CURSORS = {
select: 'default',
erase: 'crosshair',
arrow: 'crosshair',
draw: `url('${makeCursorUrl('pencil.png')}') 2 22, default`,
rectangle: `url('${makeCursorUrl('square.png')}'), default`,
ellipse: `url('${makeCursorUrl('ellipse.png')}'), default`,
triangle: `url('${makeCursorUrl('triangle.png')}'), default`,
line: `url('${makeCursorUrl('line.png')}'), default`,
text: `url('${makeCursorUrl('text.png')}'), default`,
sticky: `url('${makeCursorUrl('square.png')}'), default`,
pan: 'grab',
grabbing: 'grabbing',
moving: 'move',
};
const Cursors = (props) => {
const cursorWrapper = React.useRef();
const [active, setActive] = React.useState(false);
const [pos, setPos] = React.useState({ x: 0, y: 0 });
const {
whiteboardId,
otherCursors,
currentUser,
currentPoint,
tldrawCamera,
publishCursorUpdate,
children,
hasWBAccess,
isMultiUserActive,
isPanning,
isMoving,
currentTool,
toggleToolsAnimations,
whiteboardToolbarAutoHide,
application,
whiteboardWriters,
} = props;
const [panGrabbing, setPanGrabbing] = React.useState(false);
const start = (event) => {
const targetElement = event?.target;
const className = targetElement instanceof SVGElement
? targetElement?.className?.baseVal
: targetElement?.className;
const hasTlPartial = className?.includes('tl-');
if (hasTlPartial) {
event?.preventDefault();
}
if (whiteboardToolbarAutoHide) toggleToolsAnimations('fade-out', 'fade-in', application?.animations ? '.3s' : '0s');
setActive(true);
};
const handleGrabbing = () => setPanGrabbing(true);
const handleReleaseGrab = () => setPanGrabbing(false);
const end = () => {
if (whiteboardId && (hasWBAccess || currentUser?.presenter)) {
publishCursorUpdate({
xPercent: -1.0,
yPercent: -1.0,
whiteboardId,
});
}
if (whiteboardToolbarAutoHide) toggleToolsAnimations('fade-in', 'fade-out', application?.animations ? '3s' : '0s');
setActive(false);
};
const moved = (event) => {
const { type, x, y } = event;
const nav = document.getElementById('Navbar');
const getSibling = (el) => {
if (el?.previousSibling && !el?.previousSibling?.hasAttribute('data-test')) {
return el?.previousSibling;
}
return null;
};
const panel = getSibling(nav);
const webcams = document.getElementById('cameraDock');
const subPanel = panel && getSibling(panel);
const camPosition = document.getElementById('layout')?.getAttribute('data-cam-position') || null;
const sl = document.getElementById('layout')?.getAttribute('data-layout');
const presentationContainer = document.querySelector('[data-test="presentationContainer"]');
const presentation = document.getElementById('currentSlideText')?.parentElement;
const banners = document.querySelectorAll('[data-test="notificationBannerBar"]');
let yOffset = 0;
let xOffset = 0;
const calcPresOffset = () => {
yOffset
+= (parseFloat(presentationContainer?.style?.height)
- (parseFloat(presentation?.style?.height)
+ (currentUser.presenter ? PRES_TOOLBAR_HEIGHT : 0))
) / 2;
xOffset
+= (parseFloat(presentationContainer?.style?.width)
- parseFloat(presentation?.style?.width)
) / 2;
};
// If the presentation container is the full screen element we don't
// need any offsets
const { webkitFullscreenElement, fullscreenElement } = document;
const fsEl = webkitFullscreenElement || fullscreenElement;
if (fsEl?.getAttribute('data-test') === 'presentationContainer') {
calcPresOffset();
return setPos({ x: x - xOffset, y: y - yOffset });
}
if (nav) yOffset += parseFloat(nav?.style?.height);
if (panel) xOffset += parseFloat(panel?.style?.width);
if (subPanel) xOffset += parseFloat(subPanel?.style?.width);
// offset native tldraw eraser animation container
const overlay = document.getElementsByClassName('tl-overlay')[0];
if (overlay) overlay.style.left = '0px';
if (type === 'touchmove') {
calcPresOffset();
if (!active) {
setActive(true);
}
const newX = event?.changedTouches[0]?.clientX - xOffset;
const newY = event?.changedTouches[0]?.clientY - yOffset;
return setPos({ x: newX, y: newY });
}
if (document?.documentElement?.dir === 'rtl') {
xOffset = 0;
if (presentationContainer && presentation) {
calcPresOffset();
}
if (sl.includes('custom')) {
if (webcams) {
if (camPosition === 'contentTop' || !camPosition) {
yOffset += (parseFloat(webcams?.style?.height || 0) + BOTTOM_CAM_HANDLE_HEIGHT);
}
if (camPosition === 'contentBottom') {
yOffset -= BOTTOM_CAM_HANDLE_HEIGHT;
}
if (camPosition === 'contentRight') {
xOffset += (parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET);
}
}
}
if (sl?.includes('smart')) {
if (panel || subPanel) {
const dockPos = webcams?.getAttribute('data-position');
if (dockPos === 'contentTop') {
yOffset += (parseFloat(webcams?.style?.height || 0) + SMALL_OFFSET);
}
}
}
if (webcams && sl?.includes('videoFocus')) {
xOffset += parseFloat(nav?.style?.width);
yOffset += (parseFloat(panel?.style?.height || 0) - XL_OFFSET);
}
} else {
if (sl.includes('custom')) {
if (webcams) {
if (camPosition === 'contentTop' || !camPosition) {
yOffset += (parseFloat(webcams?.style?.height) || 0) + XS_OFFSET;
}
if (camPosition === 'contentBottom') {
yOffset -= BOTTOM_CAM_HANDLE_HEIGHT;
}
if (camPosition === 'contentLeft') {
xOffset += (parseFloat(webcams?.style?.width) || 0) + SMALL_OFFSET;
}
}
}
if (sl.includes('smart')) {
if (panel || subPanel) {
const dockPos = webcams?.getAttribute('data-position');
if (dockPos === 'contentLeft') {
xOffset += (parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET);
}
if (dockPos === 'contentTop') {
yOffset += (parseFloat(webcams?.style?.height || 0) + SMALL_OFFSET);
}
}
if (!panel && !subPanel) {
if (webcams) {
xOffset = parseFloat(webcams?.style?.width || 0) + SMALL_OFFSET;
}
}
}
if (sl?.includes('videoFocus')) {
if (webcams) {
xOffset = parseFloat(subPanel?.style?.width);
yOffset = parseFloat(panel?.style?.height);
}
}
if (presentationContainer && presentation) {
calcPresOffset();
}
}
if (banners) {
banners.forEach((el) => {
yOffset += parseFloat(window.getComputedStyle(el).height);
});
}
return setPos({ x: event.x - xOffset, y: event.y - yOffset });
};
React.useEffect(() => {
const currentCursor = cursorWrapper?.current;
currentCursor?.addEventListener('mouseenter', start);
currentCursor?.addEventListener('touchstart', start);
currentCursor?.addEventListener('mouseleave', end);
currentCursor?.addEventListener('mousedown', handleGrabbing);
currentCursor?.addEventListener('mouseup', handleReleaseGrab);
currentCursor?.addEventListener('touchend', end);
currentCursor?.addEventListener('mousemove', moved);
currentCursor?.addEventListener('touchmove', moved);
return () => {
currentCursor?.removeEventListener('mouseenter', start);
currentCursor?.addEventListener('touchstart', start);
currentCursor?.removeEventListener('mouseleave', end);
currentCursor?.removeEventListener('mousedown', handleGrabbing);
currentCursor?.removeEventListener('mouseup', handleReleaseGrab);
currentCursor?.removeEventListener('touchend', end);
currentCursor?.removeEventListener('mousemove', moved);
currentCursor?.removeEventListener('touchmove', moved);
};
}, [cursorWrapper, whiteboardId, currentUser?.presenter, whiteboardToolbarAutoHide]);
let cursorType = hasWBAccess || currentUser?.presenter ? TOOL_CURSORS[currentTool] : 'default';
if (isPanning) {
if (panGrabbing) {
cursorType = TOOL_CURSORS.grabbing;
} else {
cursorType = TOOL_CURSORS.pan;
}
}
if (isMoving) cursorType = TOOL_CURSORS.moving;
return (
<span key={`cursor-wrapper-${whiteboardId}`} ref={cursorWrapper}>
<div style={{ height: '100%', cursor: cursorType }}>
{((active && hasWBAccess) || (active && currentUser?.presenter)) && (
<PositionLabel
pos={pos}
otherCursors={otherCursors}
currentUser={currentUser}
currentPoint={currentPoint}
tldrawCamera={tldrawCamera}
publishCursorUpdate={publishCursorUpdate}
whiteboardId={whiteboardId}
isMultiUserActive={isMultiUserActive}
/>
)}
{children}
</div>
{otherCursors
.filter((c) => c?.xPercent && c.xPercent !== -1.0 && c?.yPercent && c.yPercent !== -1.0)
.map((c) => {
if (c && currentUser?.userId !== c?.userId) {
if (c.user.presenter) {
return (
<Cursor
key={`${c?.userId}`}
name={c?.user.name}
color="#C70039"
x={c?.xPercent}
y={c?.yPercent}
tldrawCamera={tldrawCamera}
isMultiUserActive={isMultiUserActive}
owner
/>
);
}
return whiteboardWriters?.some((writer) => writer.userId === c?.userId)
&& (
<Cursor
key={`${c?.userId}`}
name={c?.user.name}
color="#AFE1AF"
x={c?.xPercent}
y={c?.yPercent}
tldrawCamera={tldrawCamera}
isMultiUserActive={isMultiUserActive}
owner
/>
);
}
return null;
})}
</span>
);
};
Cursors.propTypes = {
whiteboardId: PropTypes.string,
otherCursors: PropTypes.arrayOf(PropTypes.shape).isRequired,
currentUser: PropTypes.shape({
userId: PropTypes.string.isRequired,
presenter: PropTypes.bool.isRequired,
}).isRequired,
currentPoint: PropTypes.arrayOf(PropTypes.number),
tldrawCamera: PropTypes.shape({
point: PropTypes.arrayOf(PropTypes.number).isRequired,
zoom: PropTypes.number.isRequired,
}),
publishCursorUpdate: PropTypes.func.isRequired,
children: PropTypes.arrayOf(PropTypes.element).isRequired,
isMultiUserActive: PropTypes.bool.isRequired,
isPanning: PropTypes.bool.isRequired,
isMoving: PropTypes.bool.isRequired,
currentTool: PropTypes.string,
toggleToolsAnimations: PropTypes.func.isRequired,
};
Cursors.defaultProps = {
whiteboardId: undefined,
currentPoint: undefined,
tldrawCamera: undefined,
currentTool: null,
};
export default Cursors;

View File

@ -1,29 +0,0 @@
import React from 'react';
import { useSubscription } from '@apollo/client';
import SettingsService from '/imports/ui/services/settings';
import Cursors from './component';
import Service from './service';
import { CURSOR_SUBSCRIPTION } from './queries';
import { omit } from 'radash';
const CursorsContainer = (props) => {
const { data: cursorData } = useSubscription(CURSOR_SUBSCRIPTION);
const { pres_page_cursor: cursorArray } = (cursorData || []);
if (!cursorData) return null;
return (
<Cursors
{...{
application: SettingsService?.application,
publishCursorUpdate: Service.publishCursorUpdate,
otherCursors: cursorArray,
currentPoint: props.tldrawAPI?.currentPoint,
tldrawCamera: props.tldrawAPI?.getPageState().camera,
}}
{...omit(props, ['tldrawAPI'])}
/>
)
};
export default CursorsContainer;

View File

@ -1,93 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
const { pointerDiameter } = window.meetingClientSettings.public.whiteboard;
const Cursor = (props) => {
const {
name,
color,
x,
y,
currentPoint,
tldrawCamera,
isMultiUserActive,
owner = false,
} = props;
const z = !owner ? 2 : 1;
let _x = null;
let _y = null;
if (!currentPoint) {
_x = (x + tldrawCamera?.point[0]) * tldrawCamera?.zoom;
_y = (y + tldrawCamera?.point[1]) * tldrawCamera?.zoom;
}
const transitionStyle = owner ? { transition: 'left 0.3s ease-out, top 0.3s ease-out' } : {};
return (
<>
<div
style={{
zIndex: z,
position: 'absolute',
left: (_x || x) - pointerDiameter / 2,
top: (_y || y) - pointerDiameter / 2,
width: pointerDiameter,
height: pointerDiameter,
borderRadius: '50%',
background: `${color}`,
pointerEvents: 'none',
...transitionStyle,
}}
/>
{isMultiUserActive && (
<div
style={{
zIndex: z,
position: 'absolute',
pointerEvents: 'none',
left: (_x || x) + 3.75,
top: (_y || y) + 3,
paddingLeft: '.25rem',
paddingRight: '.25rem',
paddingBottom: '.1rem',
lineHeight: '1rem',
borderRadius: '2px',
color: '#FFF',
backgroundColor: color,
border: `1px solid ${color}`,
...transitionStyle,
}}
data-test="whiteboardCursorIndicator"
>
{name}
</div>
)}
</>
);
};
Cursor.propTypes = {
name: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
currentPoint: PropTypes.arrayOf(PropTypes.number),
tldrawCamera: PropTypes.shape({
point: PropTypes.arrayOf(PropTypes.number).isRequired,
zoom: PropTypes.number.isRequired,
}),
isMultiUserActive: PropTypes.bool.isRequired,
owner: PropTypes.bool,
};
Cursor.defaultProps = {
owner: false,
currentPoint: undefined,
tldrawCamera: undefined,
};
export default Cursor;

View File

@ -1,93 +0,0 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import logger from '/imports/startup/client/logger';
import Cursor from '../cursor/component';
const PositionLabel = (props) => {
const {
currentUser,
currentPoint,
tldrawCamera,
publishCursorUpdate,
whiteboardId,
pos,
isMultiUserActive,
} = props;
const { name, color, userId } = currentUser;
const { x, y } = pos;
const { zoom, point: tldrawPoint } = tldrawCamera;
React.useEffect(() => {
try {
const point = [x, y];
publishCursorUpdate({
xPercent:
point[0] / zoom - tldrawPoint[0],
yPercent:
point[1] / zoom - tldrawPoint[1],
whiteboardId,
});
} catch (error) {
logger.error({
logCode: 'cursor_update__error',
extraInfo: { error },
}, 'Whiteboard catch error on cursor update');
}
}, [x, y, zoom, tldrawPoint]);
// eslint-disable-next-line arrow-body-style
React.useEffect(() => {
return () => {
// Disable cursor on unmount
publishCursorUpdate({
xPercent: -1.0,
yPercent: -1.0,
whiteboardId,
});
};
}, []);
return (
<>
<div style={{ position: 'absolute', height: '100%', width: '100%' }}>
<Cursor
key={`${userId}-label`}
name={name}
color={color}
x={x}
y={y}
currentPoint={currentPoint}
tldrawCamera={tldrawCamera}
isMultiUserActive={isMultiUserActive}
/>
</div>
</>
);
};
PositionLabel.propTypes = {
currentUser: PropTypes.shape({
name: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
}).isRequired,
currentPoint: PropTypes.arrayOf(PropTypes.number).isRequired,
tldrawCamera: PropTypes.shape({
point: PropTypes.arrayOf(PropTypes.number).isRequired,
zoom: PropTypes.number.isRequired,
}).isRequired,
publishCursorUpdate: PropTypes.func.isRequired,
whiteboardId: PropTypes.string,
pos: PropTypes.shape({
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
}).isRequired,
isMultiUserActive: PropTypes.bool.isRequired,
};
PositionLabel.defaultProps = {
whiteboardId: undefined,
};
export default PositionLabel;

View File

@ -1,20 +0,0 @@
import { gql } from '@apollo/client';
export const CURSOR_SUBSCRIPTION = gql`subscription CursorSubscription {
pres_page_cursor {
isCurrentPage
lastUpdatedAt
pageId
presentationId
userId
xPercent
yPercent
user {
name
presenter
role
}
}
}`;
export default CURSOR_SUBSCRIPTION;

View File

@ -1,14 +0,0 @@
import Auth from '/imports/ui/services/auth';
import { throttle } from '/imports/utils/throttle';
import { makeCall } from '/imports/ui/services/api';
const { cursorInterval: CURSOR_INTERVAL } = window.meetingClientSettings.public.whiteboard;
const publishCursorUpdate = throttle(
(payload) => makeCall('publishCursorUpdate', Auth.meetingID, Auth.userID, payload),
CURSOR_INTERVAL,
);
export default {
publishCursorUpdate,
};

View File

@ -13,9 +13,9 @@ const useCursor = (publishCursorUpdate, whiteboardId) => {
useEffect(() => {
publishCursorUpdate({
whiteboardId,
xPercent: cursorPosition?.x,
yPercent: cursorPosition?.y,
whiteboardId,
});
}, [cursorPosition, publishCursorUpdate, whiteboardId]);

View File

@ -112,4 +112,21 @@ export const CURRENT_PAGE_WRITERS_QUERY = gql`query currentPageWritersQuery {
}
}`;
export const CURSOR_SUBSCRIPTION = gql`subscription CursorSubscription {
pres_page_cursor {
isCurrentPage
lastUpdatedAt
pageId
presentationId
userId
xPercent
yPercent
user {
name
presenter
role
}
}
}`;
export default CURRENT_PAGE_ANNOTATIONS_QUERY;

View File

@ -3,7 +3,6 @@ import '/imports/startup/server';
// 2x
import '/imports/api/meetings/server';
import '/imports/api/users/server';
import '/imports/api/cursor/server';
import '/imports/api/polls/server';
import '/imports/api/captions/server';
import '/imports/api/presentation-upload-token/server';