feat(mobile): attach images from the phone's photo library - #552
Merged
Merged
Conversation
Phones and tablets get a "+" at the left of the composer row that opens the system picker (PHPicker on iOS; the photo picker on Android 13+, the document picker below). Selected images are fitted on the device — long edge capped at 2048 px, non-wire formats and HEIC converted — before they go to the machine. Sending is gated by how the device reaches the machine: LAN and local are unrestricted; over a punched internet path an image above the device's "Attachment limit over the internet" setting (2 MiB default) asks first; over a relay it is refused, since the relay is shared and the main stream would stall behind it. Also fixed on the way: the client outgoing queue capped a line at 8 MiB while the wire allows 16 MiB and an attachment can encode to ~13 MiB, so images over about 6 MiB were rejected as "queue full" over any remote connection. The line cap now has one owner in tcode-protocol. The host also validates saved attachments: below its attachments root, no traversal, a plain extension, and at most the per-image limit.
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.
What changes
Mobile photo attachments. A
+button at the left of the phone composer row opens a bottom sheet with Photo library, which calls the platform's own picker:PHPickerViewControlleron iOS,MediaStore.ACTION_PICK_IMAGESon Android 13+ andACTION_OPEN_DOCUMENTbelow. Neither needs a permission. The picker returns bytes through a newClientHost::pick_images(crates/client/src/host.rs), wired inNativeClientHostwith the same request/callback pattern as QR scanning. Desktops keep paste and drop; the button only renders when the host reports a picker.Fitting on the device (
fit_for_upload,crates/ui/src/composer/components/images.rs): images longer than 2048 px on either side are resized, non-wire formats become PNG, JPEG stays JPEG, GIF is never re-encoded. Bytes that already qualify pass through untouched. iOS delivers HEIC as JPEG via.compatiblerepresentation.Link-aware transfer policy (
crates/ui/src/attachments.rs,transfer_verdict):The limit is a device preference (
ClientPreferences::remote_attachment_limit_mib, default 2 MiB) with a row in Settings → General.Fixes found on the way:
crates/client/src/outgoing.rscapped one line at 8 MiB while the transport allows 16 MiB; a 10 MiB attachment encodes to ~13 MiB, so images over ~6 MiB failed withqueue_fullover any remote connection. The cap now has one owner,tcode_protocol::MAX_LINE_BYTES, used by both the traverse wire and the client queue.SaveAttachment: it now rejects adiroutside its attachments root or containing.., an extension that is not plain alphanumeric (the generated file name embeds it), and bytes over the per-image limit (crates/runtime/src/pipe.rs,AppState::accepts_attachment).Abstractions
TransferLink/TransferVerdicthide the path-kind → policy mapping;WorkspaceStore::attachment_linkis the one placeConnectionStateis read for it.PreparedImagereplaces a tuple;owns_image_loadreplaces a three-way condition duplicated at two completion points.is_safe_extensionandattachments_rootlive in core / services next to the limits and layout they belong to.transcode_image_to_pngis folded intofit_for_upload(its only caller).Tests
pipe_p4b_tests: extends the SaveAttachment round trip with the four rejected shapes (outside root,.., traversal inext, oversized).attachments::tests: the verdict table above.images::tests: fitting bounds the long edge, keeps qualifying bytes, converts BMP, leaves GIF alone, rejects non-images.core::attachments: extension admission.enandzh-CN.Checks run
cargo fmt --all --check,cargo clippy --workspace --all-targets --locked -- -D warnings,cargo nextest run --workspace --locked(950 passed),cargo-machete: clean.cargo check -p tcode-ios --target aarch64-apple-ios-sim,cargo ndk -t arm64-v8a check -p tcode-android,cargo check -p tcode-web --target wasm32-unknown-unknown, all with-D warnings: clean.swiftc -typecheckagainst the iOS 27 simulator SDK; Java:gradlew :app:compileReleaseJavaWithJavac: clean.Not verified
+layout at phone width) are established only by the type checks above; the+does not appear in--example phonebecause that shell has no picker-capable host.