Attachment Downloads
The messaging module supports downloading image attachments directly from the conversation feed. Downloads produce contact-aware filenames — prefixed with the contact’s name and phone number — so saved files are immediately identifiable.
How It Works
Section titled “How It Works”Two download entry points exist in the UI:
- Thumbnail button — a small download icon in the bottom-right corner of each image thumbnail. Clicking it triggers a download without opening the lightbox.
- Lightbox toolbar — a download button next to the close button in the full-screen image viewer.
Both append ?download=1 to the existing attachment URL, which tells the backend to respond with a Content-Disposition: attachment header instead of serving the file inline.
Download an attachment
Section titled “Download an attachment”GET /api/messages/attachments/:attachment_id?download=1The download query parameter accepts 1 or true. When omitted, the attachment serves inline (default behavior for lightbox viewing).
Response behavior by storage backend:
| Backend | download=1 | Default (inline) |
|---|---|---|
| R2 | Redirect to presigned URL with Content-Disposition: attachment | Redirect to presigned URL (inline) |
| LocalDisk | Direct bytes with Content-Disposition: attachment header | Direct bytes with Content-Type only |
Authorization — three checks run before serving any attachment:
ViewAttachmentcollection permission on the message resource- Organization isolation — the attachment’s
organization_idmust match the session - Contact access — if the attachment belongs to a message linked to a contact,
check_contact_accessverifies view permission
Contact-aware filenames
Section titled “Contact-aware filenames”When download=1 is set and the attachment belongs to a message with a linked contact, the server builds a filename prefix from the contact’s name and phone:
{FirstName}_{PhoneDigits}_{original_filename}Examples:
| Contact | Phone | Original file | Download filename |
|---|---|---|---|
| Jane Doe | +1 (555) 123-4567 | photo.jpg | Jane_15551234567_photo.jpg |
| Bob | — | image.png | Bob_image.png |
| — | — | photo.jpg | photo.jpg |
The prefix uses the contact’s preferred phone number, falling back to their first phone. When no contact or phone exists, the original filename passes through unchanged. If no filename exists at all, the server generates a fallback like attachment.jpg based on the content type.
Query Parameter
Section titled “Query Parameter”#[derive(Debug, Default, serde::Deserialize)]pub struct ServeAttachmentQuery { /// When `1` or `true`, serves as attachment download. download: Option<String>,}UI Components
Section titled “UI Components”Thumbnail download button
Section titled “Thumbnail download button”In MessageEntry, each image attachment renders inside a relative container. The download icon (lucide_dioxus::Download) sits absolutely positioned at the bottom-right with a semi-transparent background:
a { href: "{download_href}", // "/api/messages/attachments/{id}?download=1" class: "absolute bottom-1.5 right-1.5 p-1.5 rounded-md bg-background/90 ...", onclick: move |e| e.stop_propagation(), // prevents lightbox open Download { class: "w-4 h-4" }}The stop_propagation() call prevents the click from bubbling to the parent onclick handler that opens the lightbox.
Lightbox download button
Section titled “Lightbox download button”LightboxImage gained a file_name field and a download_url() method:
pub struct LightboxImage { pub url: String, pub alt: String, pub file_name: Option<String>,}
impl LightboxImage { pub fn download_url(&self) -> String { if self.url.contains('?') { format!("{}&download=1", self.url) } else { format!("{}?download=1", self.url) } }}The LightboxDownloadLink component renders the download button in the lightbox toolbar, using the current image’s download_url().
Storage Layer
Section titled “Storage Layer”For R2 downloads, the storage client exposes a dedicated method that bakes Content-Disposition into the presigned URL:
pub async fn presign_get_with_response_content_disposition( &self, key: &str, expires: Duration, content_disposition: &str,) -> Result<String, AppError>This ensures the browser receives the correct filename even when redirected to the R2 presigned URL.
Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
messaging/api/attachment/serve_attachment_api.rs | Endpoint with ?download=1 support and filename generation |
messaging/components/message_entry_component.rs | Thumbnail download button |
messaging/components/communication_feed_component.rs | Passes file_name to LightboxImage |
ui/lightbox_ui.rs | LightboxImage, download_url(), and lightbox download button |
bases/storage/r2.rs | presign_get_with_response_content_disposition |
bases/storage/mod.rs | Storage trait dispatch for download presigning |