feat(uploads): accessible image alt text (LC-537) #512

Merged
longjacksonle merged 3 commits from feat/LC-537-image-alt-text into main 2026-07-06 05:10:03 +02:00

What

LC-537: accessible image alt text. Images used the filename as their alt; now the author can attach real descriptive alt text, and it renders as the image's alt (falling back to the filename when unset).

Why

Filename-as-alt is close to useless for screen-reader users (IMG_4032.png). Letting the author describe the image is the standard accessibility affordance (Slack/Mastodon/Twitter all have it).

How

Three commits, each compiles + tests green on its own:

  1. Storage. New file_uploads.alt_text column (migration 0083, additive). Attachment carries it; alt() returns the author text or falls back to the filename (v1 behaviour), has_alt() reports whether one is set. db::uploads::set_upload_alt_text writes/clears it. Lib unit tests cover the fallback + blank-is-unset logic.

  2. Editor + live re-render. Images render with their effective alt, and the author gets an inline "Add / Edit alt text" disclosure under their own images (no-JS friendly: it degrades to a plain form POST). POST /api/files/{id}/alt is uploader-gated, caps at 1000 chars, rejects non-images and not-yet-posted uploads, then broadcasts a new AttachmentAltChanged event so every viewer's message re-renders through the existing render_edited_message path (same shape as VoiceTranscribed). en + es strings.

  3. Tests. Storage round-trip (set / read-back / clear-to-fallback) plus route authz: author sets alt and gets the re-rendered message back, non-author 403, anon redirected.

Notes

  • Author-only edit, mirroring message edit. The alt is rendered as an attribute and length-capped on write.
  • Migration is additive (one ADD COLUMN); no operator action.

Testing

just check, just test, just test-saas all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu

## What LC-537: accessible image alt text. Images used the filename as their `alt`; now the author can attach real descriptive alt text, and it renders as the image's `alt` (falling back to the filename when unset). ## Why Filename-as-alt is close to useless for screen-reader users (`IMG_4032.png`). Letting the author describe the image is the standard accessibility affordance (Slack/Mastodon/Twitter all have it). ## How Three commits, each compiles + tests green on its own: 1. **Storage.** New `file_uploads.alt_text` column (migration 0083, additive). `Attachment` carries it; `alt()` returns the author text or falls back to the filename (v1 behaviour), `has_alt()` reports whether one is set. `db::uploads::set_upload_alt_text` writes/clears it. Lib unit tests cover the fallback + blank-is-unset logic. 2. **Editor + live re-render.** Images render with their effective `alt`, and the author gets an inline "Add / Edit alt text" disclosure under their own images (no-JS friendly: it degrades to a plain form POST). `POST /api/files/{id}/alt` is uploader-gated, caps at 1000 chars, rejects non-images and not-yet-posted uploads, then broadcasts a new `AttachmentAltChanged` event so every viewer's message re-renders through the existing `render_edited_message` path (same shape as `VoiceTranscribed`). en + es strings. 3. **Tests.** Storage round-trip (set / read-back / clear-to-fallback) plus route authz: author sets alt and gets the re-rendered message back, non-author 403, anon redirected. ## Notes - Author-only edit, mirroring message edit. The alt is rendered as an attribute and length-capped on write. - Migration is additive (one `ADD COLUMN`); no operator action. ## Testing `just check`, `just test`, `just test-saas` all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
Add a NULLable `alt_text` column to file_uploads and thread it through the read path: `Attachment` carries it, `Attachment::alt()` returns the author's alt text (or falls back to the filename, v1 behaviour) and `has_alt()` reports whether one is set. New `db::uploads::set_upload_alt_text` writes/clears it; `get_upload` + `attachments_for_messages` select it. Lib unit tests cover the fallback + blank-is-unset logic (CI-run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
Render images with their effective alt (author text, else filename) and give the author an inline "Add / Edit alt text" disclosure under their own images (no-JS friendly: plain POST fallback). New POST /api/files/{id}/alt is uploader-gated, caps at 1000 chars, rejects non-images and not-yet-posted uploads, then broadcasts a new AttachmentAltChanged event so every viewer's message re-renders via the existing render_edited_message path (same shape as VoiceTranscribed). en + es strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
test(uploads): alt-text round-trip + author-gated editor route (LC-537)
All checks were successful
check-secrets / Nosey parker (push) Successful in 7s
check-secrets / Kingfisher (push) Successful in 11s
check-secrets / TruffleHog (push) Successful in 14s
check-secrets / TruffleHog (pull_request) Successful in 1m2s
check-secrets / Nosey parker (pull_request) Successful in 1m4s
check-secrets / Kingfisher (pull_request) Successful in 1m5s
Check / clippy + fmt + tests (pull_request) Successful in 5m23s
Create release / Create release from merged PR (pull_request) Has been skipped
ab1090b675
db round-trip (set, read back via attachments_for_message, clear -> filename fallback) plus route coverage: the author sets alt and gets the re-rendered message fragment back, a non-author is 403'd, and an anonymous request is redirected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
longjacksonle deleted branch feat/LC-537-image-alt-text 2026-07-06 05:10:04 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!512
No description provided.