03-Jul-2026 — Email attachments, inbox previews and pasted images #306
build 733 — commit 0442d65
What's new
The inbox now handles the files people send you like a proper email client would. When a walker or member replies with a photo, a PDF (a booking form or a risk assessment, say) or a spreadsheet, it arrives as a labelled attachment you can open on the spot. Images, PDFs, CSV files (shown as a table) and plain text all open in a preview window right inside the inbox, so you can read what was sent without downloading it or switching to another program. The preview window can be dragged aside and resized to suit what you are looking at, and when you do download a file it keeps its original name.
You can now send attachments as well. The email composer has an Attachments section, so a committee email can carry files with it. If you attach a type the mail system cannot send, it tells you there and then (and suggests zipping it) rather than letting the send fail later. Pasting an image straight into the editor uploads and drops it in for you, and pasting text offers an "Unwrap on paste" switch that turns hard-wrapped lines back into flowing paragraphs.
The walks pages also got a round of display work: consistent action buttons on walk cards and the edit page, configurable map framing, tidier leader display, and Quick Search that finds a walk by its Walk ID.

Previewing a CSV attachment as a table, straight from the inbox.
At a glance
- Inbox messages show attachments as pills with filename and size; collapsed messages show a paperclip hint.
- Each attachment offers Preview and Download; downloads save under the original filename.
- The preview dialog is draggable by its header, resizable from its corner, and renders images, PDFs, CSVs as a table (first 200 rows) and text files.
- The email composer's new Attachments section lets outgoing emails carry files. When you email a whole list only the first attachment goes out; to include several, email selected members or external recipients instead.
- File types the mail platform cannot send are refused when attached, with a suggestion to zip them, instead of failing at send time.
- Pasting an image into the email editor uploads and inserts it automatically, resizing anything over 300KB.
- An "Unwrap on paste" toolbar switch (green when on) collapses line breaks within paragraphs of pasted markdown so body text flows; headings, lists, blank lines and code blocks are kept, and it also applies when the composer lifts a pasted document's heading into the subject.
- Member login audit: sortable columns update the URL, login success shows as a tick or cross, the table gains the standard styling and the delete-audit action is removed.
- Walk cards and the walk edit page use one consistent action-button family with icons; the card action is a proper dropdown.
- Walk maps zoom out around the start point by a configurable amount, set on the walk settings page with a live example-map preview that also updates as you zoom it.
- The walk settings General tab is a two-column layout; Regular Walk Day and walk-leader self-service sit alongside the map controls, and the old Publishing Defaults tab is merged into Meetup.
- Walk pages no longer show an empty "Leader" heading when a walk has no leader details to display.
- Quick Search on the walks page finds a walk by its Walk ID; the walk History tab shows the synced version for Walks-Manager-sourced walks even in Local mode.
Technical changes
EmailAttachment model carried on composer state, drafts, BatchTransactionalSendRequest and SendSmtpEmailRequest; mapped to Brevo's attachment array as absolute URLs; inbox reply writeback records sent attachments so the thread's sent copy shows the same pills.
Attachments upload to a new email-attachments root folder via the existing S3 file-upload endpoint; campaign sends map the first attachment to Brevo's single attachmentUrl.
Attach-time validation against BREVO_SUPPORTED_ATTACHMENT_EXTENSIONS; Brevo 400s anything else, discovered via a GPX file.
TipTap handlePaste intercepts image files, resizes anything over 300KB via FileUtilsService helpers and inserts the uploaded S3 URL at the cursor.
Shared unwrapSoftLineBreaks/hasSoftWrappedParagraph helpers (functions/unwrap-line-breaks.ts) accumulate soft-wrapped and continuation lines while preserving standalone blocks and code fences; the editor exposes unwrapIfEnabled and the composer applies it when consuming a pasted title/forwarded body so the toggle covers that path too.
First use of @angular/cdk drag-drop for the preview dialog, with a native CSS resize grip and a mousedown-based backdrop close so resize drags cannot dismiss it.
CSV preview parses with csv-parse (already a dependency) capped at 200 rows with a truncation note, falling back to text on parse failure.
UiActionsService is the single route query-param API: queryParameter(), updateQueryParameter(s)() with merge and replaceUrl semantics, plus kebab-case value alias helpers; keys always come from StoredValue (31 entries added with values matching the previous literals, so no existing URLs change).
SectionToggle and ContentBlockEditor queryParamKey inputs typed StoredValue, making raw string keys a compile error.
Member login audit sorts via ?sort=login-time&sort-order=descending with a two-way alias map at the URL boundary; Gmail OAuth callback and campaign deep-link keys keep their server-written names; around ten components with entangled queryParams subscriptions were converted keys-only and noted as follow-up.
MapEditComponent gains an initialZoomOffset input and a mapZoomOutLevels walks-config setting (default 2), clamped to the map's real max zoom so OS Leisure tiles zoom out correctly; it emits zoomOutLevelsChange so the config preview reflects manual zooming.
Card and walk-edit action buttons become real buttons/dropdowns in the shared pager-btn style with FontAwesome icons, replacing input[type=submit].
hasVisibleLeaderContactDetails on WalkDisplayService gates the leader heading and card row; quick-search adds fields.migratedFromId; the History tab's synced-version block keys off the walk's own inputSource rather than the group population mode.
AGENTS.md records the commit conventions this session established: compound types and scopes joined with +, the (ref #NNN) subject suffix, the What's new / At a glance / Technical changes body structure the release-notes generator reads, and the rule that commit messages and GitHub issues never name external contacts.