| index c7b6249..152b233 100644 |
| --- a/ios/LChat/Views/NativeChatScreen.swift |
| +++ b/ios/LChat/Views/NativeChatScreen.swift |
| @@ -1057,6 +1057,13 @@ struct NativeChatScreen: View { |
| // web composer hides its attach button the same way). |
| if defaultCommand == nil { |
| Menu { |
| + // Simulators (and some iPads) have no camera — the row |
| + // just doesn't exist there rather than failing on tap. |
| + if UIImagePickerController.isSourceTypeAvailable(.camera) { |
| + Button { showCamera = true } label: { |
| + Label("Camera", systemImage: "camera") |
| + } |
| + } |
| Button { showPhotoPicker = true } label: { |
| Label("Photo or Video", systemImage: "photo") |
| } |
| @@ -1124,6 +1131,13 @@ struct NativeChatScreen: View { |
| .fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.item]) { result in |
| if case .success(let url) = result { stageFile(url) } |
| } |
| + // A fresh capture stages exactly like a pasted image — the upload |
| + // path's processImage downscales and strips EXIF/location before |
| + // encryption, same as every other photo source. |
| + .fullScreenCover(isPresented: $showCamera) { |
| + CameraCapture { data in stageImageData(data, name: "photo.jpg") } |
| + .ignoresSafeArea() |
| + } |
| // Drop-to-attach (iPad split view, iPhone app-to-app drags): images, |
| // movies and files stage exactly like the pickers' choices. The system |
| // drag badge is the whole affordance — no overlay (matching the Mac). |
| @@ -1163,6 +1177,7 @@ struct NativeChatScreen: View { |
| } |
| |
| @State private var showPhotoPicker = false |
| + @State private var showCamera = false |
| |
| private var canSend: Bool { |
| let hasText = !draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty |
| @@ -1652,6 +1667,44 @@ struct NativeChatScreen: View { |
| } |
| } |
| |
| +// The system camera (UIImagePickerController — SwiftUI has no native capture |
| +// view). Stills only, on purpose: movie capture records audio, which would |
| +// demand a microphone-permission prompt for a flow nobody asked for. The |
| +// shot comes back as JPEG bytes and rides the ordinary image-upload path. |
| +private struct CameraCapture: UIViewControllerRepresentable { |
| + let onCapture: (Data) -> Void |
| + @Environment(\.dismiss) private var dismiss |
| + |
| + func makeUIViewController(context: Context) -> UIImagePickerController { |
| + let picker = UIImagePickerController() |
| + picker.sourceType = .camera |
| + picker.delegate = context.coordinator |
| + return picker |
| + } |
| + |
| + func updateUIViewController(_ picker: UIImagePickerController, context: Context) {} |
| + |
| + func makeCoordinator() -> Coordinator { Coordinator(self) } |
| + |
| + final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { |
| + let parent: CameraCapture |
| + init(_ parent: CameraCapture) { self.parent = parent } |
| + |
| + func imagePickerController(_ picker: UIImagePickerController, |
| + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { |
| + if let image = info[.originalImage] as? UIImage, |
| + let data = image.jpegData(compressionQuality: 0.9) { |
| + parent.onCapture(data) |
| + } |
| + parent.dismiss() |
| + } |
| + |
| + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { |
| + parent.dismiss() |
| + } |
| + } |
| +} |
| + |
| // Full-screen encrypted-video playback — an AVPlayerViewController wrapped so |
| // its letterbox is TRANSPARENT (PlayerSurface): the backdrop is our own layer, |
| // so the dismiss swipe translates only the video surface (scaling down as it |