Skip to content

feat: sync image files#23

Open
martian56 wants to merge 6 commits intomainfrom
feat/sync-image-files
Open

feat: sync image files#23
martian56 wants to merge 6 commits intomainfrom
feat/sync-image-files

Conversation

@martian56
Copy link
Copy Markdown
Member

This pull request implements robust support for collaborative image/file synchronization in the board editor, ensuring that image blobs (files) are reliably shared and loaded across all peers. It introduces new logic for managing files in the Yjs-based collaboration layer, updates the frontend to handle file state alongside elements, and adds comprehensive tests to prevent regression bugs related to file and element syncing.

Collaboration and Synchronization Enhancements:

  • Added support for synchronizing files (images/blobs) via Yjs: introduced FileJson type, applyFilesToYMap, and readFilesFromYMap functions, and updated the collaboration hook (useBoardCollab) to handle file syncing, observation, and callbacks. [1] [2] [3] [4] [5]
  • Updated the board page to register files with Excalidraw whenever new files are received or loaded, ensuring that image elements always have their corresponding blobs and avoiding blank images or disabled interactions. [1] [2] [3] [4] [5] [6] [7]

API and Data Model Updates:

  • Extended the board snapshot and shared board page data models to include a files field, ensuring that image data is preserved and loaded even for legacy boards. [1] [2] [3]

Testing and Regression Prevention:

  • Added comprehensive tests for file synchronization and for suppressing "echo" updates when element changes are only superficial (e.g., only versionNonce, version, or updated fields change), preventing mid-drag snap-back bugs and unnecessary updates. [1] [2]

Internal Refactoring:

  • Refactored the change handling logic in the board page to use a combined syncLocalChanges function, ensuring atomic updates of elements and files and preventing race conditions where an image element might reference a file not yet present. [1] [2] [3]

These changes collectively ensure a smoother, more reliable collaborative editing experience, especially when working with images and other file-based elements.

@martian56 martian56 added this to the Deadline milestone Apr 30, 2026
@martian56 martian56 self-assigned this Apr 30, 2026
@martian56 martian56 added the enhancement New feature or request label Apr 30, 2026
@devlaner-coolify-app
Copy link
Copy Markdown

devlaner-coolify-app Bot commented Apr 30, 2026

The preview deployment for Loomy UI is in progress. 🟡

Open Build Logs | Open Application Logs

Last updated at: 2026-04-30 22:31:52 CET

Copy link
Copy Markdown
Member

@nazarli-shabnam nazarli-shabnam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the board editor’s Yjs collaboration layer to synchronize Excalidraw file blobs (images) alongside elements, and wires the frontend to register incoming files so image elements load reliably for all peers (including from legacy snapshots).

Changes:

  • Added Yjs bridge support for files (FileJson, applyFilesToYMap, readFilesFromYMap) and improved element “echo” suppression to avoid mid-drag snap-back.
  • Updated board pages and collab hook to sync, observe, and seed files into Excalidraw via addFiles, plus a combined syncLocalChanges writer.
  • Added/expanded Vitest coverage for file syncing and the echo-suppression regression cases.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/frontend/src/pages/board/BoardPage.tsx Registers remote/seeded files with Excalidraw and persists files into the saved snapshot while syncing files+elements together.
apps/frontend/src/pages/board/SharedBoardPage.tsx Extends shared-board initial data to include files so shared view can render images.
apps/frontend/src/lib/collab/yjs-bridge.ts Introduces file Y.Map helpers and changes element equality to suppress drift-only echoes.
apps/frontend/src/lib/collab/yjs-bridge.test.ts Adds regression tests for echo suppression and file map syncing behavior.
apps/frontend/src/hooks/useBoardContent.ts Extends persisted snapshot type to include files.
apps/frontend/src/hooks/useBoardCollab.ts Adds file observation/callbacks, file sync helpers, and combined local sync transaction.
apps/frontend/package.json Bumps frontend version to 0.4.0.
apps/frontend/package-lock.json Updates lockfile version fields to match package.json version bump.
Files not reviewed (1)
  • apps/frontend/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +194 to +204
// Register every known file first. If an image element references
// a fileId whose blob hasn't reached this client yet, Excalidraw
// marks the element as not-fully-loaded and disables interaction —
// the peer who didn't add the image can't move it. addFiles is
// idempotent, so calling it on every remote elements update is
// safe and only meaningful when a new file actually landed.
const files = readAllFilesRef.current?.() ?? [];
if (files.length > 0) {
excalidrawAPI.addFiles(
files as unknown as Parameters<ExcalidrawAPI["addFiles"]>[0],
);
Comment on lines 354 to 358
const content: ExcalidrawSnapshot = {
elements: [...latestElementsRef.current],
appState: latestAppStateRef.current as ExcalidrawSnapshot["appState"],
files: latestFilesRef.current,
yjs_update: encodeYjsState(),
Comment on lines +162 to +165
doc.transact(() => {
if (files) applyFilesToYMap(doc, yfiles, files);
applyElementsToYMap(doc, ymap, elements);
}, LOCAL_ORIGIN);
Comment on lines 80 to +96
function elementsEqual(a: ElementJson, b: ElementJson): boolean {
// Fast path — matching versionNonce always implies identical content,
// so we can skip the structural compare on the hot drag path.
if (
typeof a.versionNonce === "number" &&
typeof b.versionNonce === "number"
typeof b.versionNonce === "number" &&
a.versionNonce === b.versionNonce
) {
return a.versionNonce === b.versionNonce;
return true;
}
return JSON.stringify(a) === JSON.stringify(b);
// Slow path — Excalidraw sometimes bumps an element's version /
// versionNonce without changing visible content (e.g. when applying
// an inbound updateScene). If we treated those as different, we'd
// echo a "no-op" change back to the peer, and that echo lands on
// their canvas mid-drag and snaps them back to the pre-drag spot.
return canonicalize(a) === canonicalize(b);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants