Repository navigation
Conversation
Adds the chat-media downscale target, the JPEG quality ladder that walks down until a photo fits the policy's size cap, and the mime selection that follows the policy's own order. GetUploadPolicy is wrapped with a chatMessage access context. The shared chat_media.json vectors are synced and gated in CI.
Adds Content.media with MediaAttachment and a media quote kind, and stores media messages locally. Database.schemaVersion is bumped because the store now writes media content.
Photos are staged as composer chips, uploaded through the downscale and quality ladder, and sent one message each with retry. The transcript draws photo bubbles, quotes and previews describe them, and tapping one opens a full-screen viewer. An inline camera captures straight into the composer. Only the downsampled copy is cached, not the original.
The attach menu grows out of +, and the menu, camera card and photo card are one surface that morphs between states. The surface draws over the live keyboard by joining the keyboard's window, falling back to an input view swap and then to dismissing the keyboard. The camera and picker start while the menu is up, so a card has content when it opens. A tap anywhere outside the open menu only closes it. Staged photos sit inside the message field and fade at its edges. The keyboard overlay decisions are recorded in .claude/plans/2026-10-02-attach-photo-card.md.
Downloaded photos go to a dedicated Kingfisher disk cache keyed by blob id, so they no longer fall back to the blur hash after five minutes. In an encrypted DM a photo is sealed with the shared-core chat cipher, stored with endToEndEncryptedFor set to the chat, and sent as EncryptedContent. Incoming encrypted photos are decrypted before they are drawn, and one that can't be opened shows as unavailable. Blob rejections now log the server's reason code instead of "unknown".
Outgoing photo bubbles show a thin progress bar in the corner: the upload's byte fraction, then a sliding segment while the server processes the blob, which has no progress signal. A send no longer fails when the app is backgrounded mid-send. The READY poll retries through a dropped stream, its deadline only counts time the app is awake, and the send holds a background task assertion. A retry after the bytes are stored polls again instead of uploading again. Pending photos are written to disk at send, so a relaunch restores them: one whose bytes were stored resumes, one that never reached the server comes back failed with retry, and one that already sent is dropped.
The composer is a single row: +, the message field and send, in glass tinted 3% white with a lit rim and 28pt corners. A round $ sits left of the field and splits from the field's glass on the draft's first character, rejoining it when the draft is cleared. Send pops in on a draft and is gone without one. An empty field takes focus from a tap anywhere right of +. Text that wraps or holds a newline stacks the field: the text takes the full width and + and send drop to a row below. It stays stacked until the draft is cleared, so editing near the wrap point does not flip the layout. The caret and text spring into place together rather than the caret trailing on UIKit's own animation. The bar now reports its height with `onGeometryChange`. The `GeometryReader` report delivered only the height an animated stack started from, so the bar's clip stayed one row tall and cut off the top line. When the composer shrinks, the clip eases down on the stack spring while the transcript moves in one frame, as #908 does. The + menu holds camera and photos. It opens from the $'s margin at 220pt, its rows sharpening out of a blur as it grows from +, and springs back into + without a colour jump. The composer stays visible under the camera and photo cards, and a capture or pick collapses into its chip with the photo drawn throughout.
#908 lowered BubbleBackgroundView.liftShadowOpacity from 0.65 to 0.135 and left this expectation at 0.65.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds photo messages to chat: capture or pick photos from a + menu drawn over the keyboard, send each as its own message, and draw them in the transcript. The composer also moves to a one-row glass layout.
Sending
GetUploadPolicyis called with achatMessageaccess context, and the sharedchat_media.jsonvectors are synced and gated in CI.Content.mediaand a media quote kind are modelled and stored locally.Database.schemaVersionis bumped.EncryptedContent. Incoming ones are decrypted before they are drawn; one that can't be opened shows as unavailable.Composer and attach menu
onGeometryChange. TheGeometryReaderreport missed the end height of an animated stack, so the clip cut off the top line.ChatBubbleViewTestsnow expects the 0.135 lift shadow opacity that #908 set; it was failing on main.Design notes are in
.claude/plans/2026-10-02-attach-photo-card.mdand.claude/plans/2026-10-03-pending-photo-persistence.md.