Chat file attachments, stages 3-4: core wiring + chat UI
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled

Wire the send/receive paths and the chat UI on top of the file plane.
(Committed together because the UI renders the state the core wiring
produces.)

Core:
- CoreCommand::SendChatFile {text, attachment, data}: serve the bytes on
  the file plane (serve_attachment) then broadcast the descriptor via
  send_chat. CoreCommand::FetchAttachment {from, attachment}: detached
  fetch -> AttachmentReady/AttachmentFailed.
- On an inbound Chat with an Image attachment, auto-fetch + defensively
  re-validate (decodable + within pixel limits) before delivering;
  non-images wait for an explicit fetch (the Save/Download chip).
- UiEvent::ChatMessage carries the attachment; new AttachmentReady /
  AttachmentFailed events keyed by attachment id.

App:
- 📎 attach button + native picker; reads the file, enforces the size
  cap, classifies image vs file, mints a random id, optimistically
  echoes the message + caches our own bytes (so we see our own image
  inline), and sends SendChatFile.
- Renders inline image thumbnails (handle cached by id to avoid the
  per-redraw re-upload flicker), file chips with Save/Download, a
  loading placeholder for in-flight images, and an error line on
  failure. Image messages with no caption still render.
- SaveAttachment: saves immediately if bytes are in hand, else fetches
  then saves when ready (pending_saves) via a native save dialog;
  filename defaulted from the sanitized descriptor.
- Session-only: attachment bytes/handles cleared on leave, never
  persisted.

Binary + clippy clean, 349 lib tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 23:15:06 -04:00
co-authored by Claude Opus 4.8
parent f75760b14e
commit 60c1951567
3 changed files with 345 additions and 4 deletions
+15 -1
View File
@@ -53,6 +53,14 @@ pub enum CoreCommand {
SetRecordingMode(RecordingMode),
/// Broadcast a room text-chat message. No-op when not in a call.
SendChat(String),
/// Send a chat message carrying a file attachment. The app has already read +
/// capped the file and built the descriptor; core makes the bytes available
/// on the file plane and broadcasts the descriptor.
SendChatFile { text: String, attachment: crate::files::ChatAttachment, data: Vec<u8> },
/// Fetch a received attachment's bytes from its sender over the file plane
/// (used for on-demand file/chip downloads; images are auto-fetched on
/// receipt). Replies with `AttachmentReady`/`AttachmentFailed`.
FetchAttachment { from: EndpointId, attachment: crate::files::ChatAttachment },
/// Set the pixelpass binary location (config override, empty = use `$PATH`).
/// Sent at startup so screen-share can resolve the binary.
SetPixelpassPath(Option<String>),
@@ -109,7 +117,13 @@ pub enum UiEvent {
/// A room text-chat message arrived from a peer (never our own — local
/// messages are echoed by the UI on send). `from` is the sender's node id
/// string, used to key their avatar (W4).
ChatMessage { from: String, name: String, text: String },
ChatMessage { from: String, name: String, text: String, attachment: Option<crate::files::ChatAttachment> },
/// An attachment's bytes are now available (auto-fetched for images, or
/// fetched on demand for files). Keyed by attachment id so the UI can match
/// it to the chat entry.
AttachmentReady { id: crate::files::AttachmentId, data: Vec<u8> },
/// An attachment fetch failed (sender gone, too large, decode error, etc.).
AttachmentFailed { id: crate::files::AttachmentId, error: String },
/// Our own screen share started; the UI flips the Share button to "Stop".
ScreenShareStarted,
/// Our own screen share stopped (or failed to start).