refactor(attachments): collapse blob previews into one state, gate video on mime - #3657
Merged
Conversation
Drops SUPPORTED_VIDEO_SUFFIX to match canPreviewAudio: a <video> element neither parses HTML nor executes script, so the server-sniffed mime is the whole boundary. The allowlist only rejected playable containers (.mkv, .avi, extension-less uploads) without buying any safety.
Video was the only previewable type still rendering the generic file icon, so the thumbnail disagreed with the click behaviour.
Image, PDF and video each carried their own ref, close function, modal and unmount line. A previewKind() helper now drives both the guard and the assignment, so at most one preview is representable and there is no by-elimination branch left to route a new type into the PDF iframe. Audio stays on its inline per-row player, untouched.
The video and iframe branches sit on their own opaque media; the error branch had nothing, so white text and the download button floated over the dimmed attachment rows behind the modal.
kolaente
enabled auto-merge (rebase)
August 29, 2026 15:40
Preview DeploymentPreview deployments for this PR are available at:
The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the Run locally with Dockerdocker pull ghcr.io/go-vikunja/vikunja:pr-3657
docker run -p 3456:3456 ghcr.io/go-vikunja/vikunja:pr-3657Last updated for commit 2820bf7 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #3491, which merged before these could land.
#3491 shipped video attachment previews. A review pass on it surfaced four design questions that were deliberately deferred to the maintainer rather than decided mid-PR; this implements the answers.
Changes
1. Gate video previews on mime alone (
985cb0c74)Drops
SUPPORTED_VIDEO_SUFFIX. This matchescanPreviewAudio, which landed on main in the meantime with the reasoning already spelled out: a<video>element neither parses HTML nor executes script, so the server-sniffed mime is the whole boundary. The allowlist only rejected playable containers without buying any safety.Verified:
.mkv(server mimevideo/matroska) and a completely extension-less mp4 (video/mp4) now preview and decode, where they previously downloaded.text/plainnamedevil.mp4still downloads.2. Play icon for video rows (
57d2fe91a)Video was the only previewable type still rendering the generic
fileicon, so the thumbnail disagreed with the click behaviour. Mirrors theisAudio/volume-highbranch.3. Collapse the blob previews into one state (
2e9507e89)Image, PDF and video each carried their own ref, close function, modal and
onBeforeUnmountline — seven refs and three close functions between them. ApreviewKind()helper now drives both the guard and the assignment, so at most one preview is structurally representable.This also removes a latent hazard and fixes a real bug:
else), so a fourth preview type added to the guard would have routed its blob into the same-originpdf-preview-iframe. With onekindvalue there is no elimination branch left.closeVideoPreview()cleared only the video refs, so opening a video while an image lightbox was open left both mounted, with the video audible underneath. Now impossible by construction.Audio is untouched — it uses the separate inline per-row
AudioPreviewplayer.4. Panel behind the video error state (
2820bf734)The video and iframe branches sit on their own opaque media; the error branch had none, so white text and the download button floated over the dimmed attachment rows.
Testing
pnpm lint,pnpm lint:stylesandpnpm buildpass. 43 unit tests pass across 5 files, including main'sAudioPreviewsuite;previewKindhas its own coverage and thecanPreviewVideocases were updated for the mime-only gate. No newvue-tscerrors (the 4 in these files are pre-existing and only line-shifted).Driven end-to-end against a local build with Playwright across 13 flows: image lightbox (zoom/pan/keyboard) and PDF iframe unregressed; exactly one preview surface mounted at every animation frame across both switch directions; blob revoked in the same frame as the incoming click; ESC-mid-fetch and unmount-mid-fetch both end with the late blob created and revoked in the same millisecond and no modal resurrection; spinner confirmed video-only (147ms on a 64MB mp4, zero frames on an 18MB PDF); ProRes
.movstill shows the failure message and its Download button produces a byte-identical file; audio still bypasses the modal entirely.Notes
Reviewed and runtime-tested with Claude Code.