Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/components/notes/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@
padding-left: $padding-extra-small;
padding-right: $padding;
}

img {
max-width: 100%;
height: auto;
}
}
}
12 changes: 11 additions & 1 deletion src/components/tldraw_v2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import './index.scss';
import {
getTldrawData, getViewBox, createTldrawImageAsset,
createTldrawBackgroundShape, createTldrawCursorShape,
setupColorThemePaletteOverrides
getTldrawImageFilePath, setupColorThemePaletteOverrides
} from 'utils/tldraw';
import { buildFileURL } from 'utils/data';
import { isEmpty } from 'utils/data/validators';
Expand Down Expand Up @@ -88,6 +88,16 @@ const SlideData = (tldrawAPI) => {
const newShape = { ...shape };
newShape.parentId = tldrawAPI?.getCurrentPageId();
shapes[newShape.id] = newShape;

const imageFilePath = getTldrawImageFilePath(newShape);
if (imageFilePath) {
assets[newShape.props.assetId] = createTldrawImageAsset(
newShape.props.assetId,
buildFileURL(imageFilePath),
newShape.props.w,
newShape.props.h,
);
}
}
}

Expand Down
30 changes: 30 additions & 0 deletions src/setupTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// The whiteboard package ships an ESM bundle that jsdom cannot load, so the whole
// module is stubbed. Stubs below hold the exports the tests actually rely on; any
// other export throws on access, so a test that starts using a new symbol fails
// with its name instead of silently receiving undefined.
jest.mock('@bigbluebutton/tldraw', () => {
const stubs = {
// Flags the stub as an ES module so the interop layer hands it over as is:
// without it a namespace import copies the known keys into a plain object
// and the guard below never runs.
__esModule: true,
DefaultColorThemePalette: {
lightMode: { black: {}, yellow: {} },
darkMode: { black: {}, yellow: {} },
},
};

// Properties the module interop layer probes before any export is read.
const interop = ['__esModule', 'default', 'then'];

return new Proxy(stubs, {
get: (target, property) => {
if (property in target) return target[property];
if (typeof property === 'symbol' || interop.includes(property)) return undefined;

throw new Error(
`@bigbluebutton/tldraw is mocked and has no "${property}" export. Add it to src/setupTests.js.`
);
},
});
});
2 changes: 1 addition & 1 deletion src/utils/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const buildNotes = result => {
if (!result) return '';

// Extract the notes' body
const regex = /<body>\n.*\n<\/body>/g;
const regex = /<body>[\s\S]*<\/body>/g;
const match = result.match(regex);

let data = '';
Expand Down
19 changes: 19 additions & 0 deletions src/utils/tldraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ const createTldrawImageAsset = (assetId, imageUrl, scaledWidth, scaledHeight) =>
};
}

// Match the published counterpart of the upload path accepted by the whiteboard client.
// The file-uploads directory name is part of the recording format.
// Must match bbb-file-upload, bbb-shared-notes-server, the bbb-file-upload
// nginx template, the record-and-playback scripts and bbb-presentation-video.
const UPLOADED_IMAGE_SRC_PATTERN = /^\/presentation\/[A-Za-z0-9-]+\/(file-uploads\/[a-f0-9-]+\.(?:png|jpe?g|gif|webp))$/;
const TLDRAW_ASSET_ID_PATTERN = /^asset:[A-Za-z0-9_-]+$/;

const getTldrawImageFilePath = (shape) => {
const src = shape?.meta?.bbbImageSrc;
const assetId = shape?.props?.assetId;

if (shape?.type !== 'image' || !src || !assetId) return null;
if (typeof src !== 'string') return null;
if (!TLDRAW_ASSET_ID_PATTERN.test(assetId)) return null;

return src.match(UPLOADED_IMAGE_SRC_PATTERN)?.[1] || null;
};

/**
* Creates a background shape object for a Tldraw presentation page. The function generates a shape object
* with predefined properties suitable for a background image. It takes the asset ID of the image, the current page ID,
Expand Down Expand Up @@ -208,6 +226,7 @@ export {
getTldrawData,
getViewBox,
createTldrawImageAsset,
getTldrawImageFilePath,
createTldrawBackgroundShape,
createTldrawCursorShape,
isTldrawWhiteboard,
Expand Down
55 changes: 55 additions & 0 deletions src/utils/tldraw.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getTldrawImageFilePath } from './tldraw';

const validShape = {
type: 'image',
meta: {
bbbImageSrc: '/presentation/986a45d2a620695c3a0bdca939a8ad4ca6a9e2b9-1785814812863/file-uploads/44c71706-8566-4c79-a303-79f6862affb0.png',
},
props: {
assetId: 'asset:9bXh_4Vm-2',
},
};

it('gets the uploaded file path from a valid image shape', () => {
expect(getTldrawImageFilePath(validShape))
.toEqual('file-uploads/44c71706-8566-4c79-a303-79f6862affb0.png');
});

it('rejects a non-image shape', () => {
expect(getTldrawImageFilePath({ ...validShape, type: 'geo' })).toBeNull();
});

it('rejects an image shape without a source', () => {
expect(getTldrawImageFilePath({ ...validShape, meta: {} })).toBeNull();
});

it('rejects a source outside the uploaded image path', () => {
const shape = {
...validShape,
meta: { bbbImageSrc: 'https://example.com/image.png' },
};

expect(getTldrawImageFilePath(shape)).toBeNull();
});

it('rejects a malformed asset id', () => {
const shape = {
...validShape,
props: { assetId: 'asset:invalid/id' },
};

expect(getTldrawImageFilePath(shape)).toBeNull();
});

it('rejects an image shape without an asset id', () => {
expect(getTldrawImageFilePath({ ...validShape, props: {} })).toBeNull();
});

it('rejects a non string source', () => {
const shape = {
...validShape,
meta: { bbbImageSrc: 42 },
};

expect(getTldrawImageFilePath(shape)).toBeNull();
});