feat(front): show loading and error states for video attachment previews (#24454)
Closes #24453 ## Problem `mp4` is a previewable type, so opening a video attachment mounts the viewer and hands the URL to `@cyntler/react-doc-viewer`, which creates the `<video>`. From there the user gets no feedback at all: - While the video downloads, the modal shows the empty viewer container. On a large recording over a slow connection that lasts a long time and reads as a broken preview rather than a loading one. - If the media never loads — expired signed URL, unsupported codec, network error — that empty box is the **final** state. No message, and no way to reach the file, even though this same component already offers a download for types it cannot preview. ## Change `useMediaLoadStatus` tracks the media element's load state and the viewer uses it for two new states: - **loading** → the `Loader` is shown over the viewer container. - **error** → the existing "Preview Not Available" panel with its download button, instead of an empty box. Non-video previews are untouched: the hook is disabled for them, keeps returning `loading`, and nothing reads it. The `Suspense` fallback that covers the lazy-loaded viewer chunk in `AttachmentList` also gets the `Loader`, where it was a bare line of text. ## Why the listeners are on the container The `<video>` belongs to the viewer library, so `onLoadedData` / `onError` cannot be passed as props. The hook attaches them to the container in the **capture** phase instead: media events do not bubble, but they still travel down through the ancestors, and the container is mounted before the library creates the element, so no event is missed. That is the one non-obvious part of the change, so it is what the test pins down — it dispatches a non-bubbling `loadeddata` on a child `<video>` and asserts the hook still reports `ready`. ## Tests `useMediaLoadStatus.test.tsx` (new, 4 cases): starts in `loading`; reports `ready` on a non-bubbling `loadeddata`; reports `error` on `error`; stays `loading` when disabled, so non-media previews keep their current behaviour. `jest src/modules/activities/files` passes 5 suites / 15 tests. `oxlint --type-aware` and `oxfmt --check` are clean on the four files, and `tsgo --noEmit` reports no errors in them. The behaviour was also verified by hand against a real 28-minute video attachment on a self-hosted instance: the overlay appears while the video downloads and is gone once `readyState` reaches 4. ## Note on scope I kept this to the loading and error states. The viewer also letterboxes videos — the container is a fixed tall box, so a landscape video sits at the top with dead space under it — but that is a layout change with visual trade-offs, so it seemed better not to bundle it here. Happy to open it separately if you want it. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/24454?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
E
Emmanuel Hernández Bazán committed
1935c33b6414b3308c3fe577a4902b430d083ccd
Parent: bb44697
Committed by GitHub <noreply@github.com>
on 8/24/2026, 10:09:00 AM