Skip to content

feat(desktop): say "attachment expired" instead of showing a broken file - #31

Merged
mattbalza merged 2 commits into
mainfrom
fix/media-expired-state
Aug 7, 2026
Merged

feat(desktop): say "attachment expired" instead of showing a broken file#31
mattbalza merged 2 commits into
mainfrom
fix/media-expired-state

Conversation

@mattbalza

Copy link
Copy Markdown
Owner

What

A message can outlive its own attachment. Until now that read as a bug in the app rather than an old message:

  • Images rendered a broken frame, still wrapped in a lightbox trigger that opened onto nothing.
  • Files kept a download button that failed every single time it was pressed, with a toast that was gone before the next press.

Both now render an "Attachment expired" card that keeps the filename — so the message still records what was shared — and offers no action, because there is nothing left to fetch.

How each path learns it

They fail differently, so they ask differently:

surface how it fails how it asks
ImageBlock <img> error event, which carries no status one-byte range GET at the media URL, classify the response
FileCard native download_file rejection parse the status out of the message

The file path deliberately does not probe. href there is the unrewritten relay URL — RELAY_MEDIA_RE only matches image/video extensions, so a PDF is never proxied and a webview fetch to it is bounced by Cloudflare. That 403 would classify as unavailable and the card would never learn the truth. The rejection string already carries it: relay_error_message builds every relay error as relay returned {status}[: message], and that prefix is the stable half.

Only 404 and 410 are expiry

Everything else is a fault that may clear. Telling someone their file is gone because the network blinked is the one wrong answer available here, so 403, 429, 5xx and a thrown fetch all stay a plain failure with the normal toast. A message with no status in it at all — a refused connection, a cancelled save dialog, a hash mismatch — returns null and changes nothing.

Incidentally fixed

FileCard was doing err instanceof Error ? err.message : "Download failed". Tauri rejects with the command's raw String, never an Error, so every real download error was being discarded and replaced with the generic text. errorText() now handles both shapes.

Test plan

  • desktop/src/shared/lib/mediaAvailability.test.mjs — 7 tests: 404/410 → expired, 200/206/304 → available, 403/429/5xx → unavailable, status parsed out of the relay wording, statusless messages → null, the probe's shape (GET + range: bytes=0-0 + no-store), and a throwing fetch → unavailable.
  • npx tsc --noEmit -p tsconfig.json clean.
  • npx biome check clean.

An old message can outlive its own attachment. Until now that read as a
bug in the app: a broken image frame with no explanation, or a download
button that failed every single time it was pressed, with a toast that
was gone before the next press.

The two paths learn it differently, because they fail differently. An
<img> error event carries no status at all, so the image path asks the
media URL for one byte and classifies the answer. Downloads never touch
the webview — they go through the native command so they traverse the
tunnel — so the file path reads the status out of the rejection string,
which relay_error_message always builds as "relay returned {status}".

404 and 410 are the only two statuses that mean the bytes are not coming
back; everything else is a fault that may clear. Telling someone their
file is gone because the network blinked is the one wrong answer here,
so a 403, a 500 or a thrown fetch stays a plain failure.
…dule

markdown.tsx is 1968 lines against a 1000-line ceiling, so the ratchet
holds it where it is: the expired-attachment branch cannot land while the
file also grows. `getImageLightboxFocusableElements` is pure DOM with no
markdown in it and every one of its siblings already lives in
./markdown/imageLightbox — this is where it belonged already.
@mattbalza
mattbalza merged commit 6d87a16 into main Aug 7, 2026
23 checks passed
@mattbalza
mattbalza deleted the fix/media-expired-state branch August 7, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant