Improve camera UX: Direct camera access with camera icon

- Change paperclip icon to camera icon for clearer affordance
- Open camera directly on tap (no confirmation dialog)
- Add CameraPickerView wrapper for UIImagePickerController
- Remove PhotosPicker in favor of direct camera access
- Images still processed through ImageUtils with EXIF stripping
- Accessibility: Added 'Take photo' label
This commit is contained in:
jack
2025-10-18 13:45:04 +02:00
parent 9d738cff45
commit a84b8c05f1
2 changed files with 24136 additions and 23968 deletions
+24074 -23927
View File
File diff suppressed because it is too large Load Diff
+62 -41
View File
@@ -15,9 +15,6 @@ import AppKit
#endif #endif
import UniformTypeIdentifiers import UniformTypeIdentifiers
import BitLogger import BitLogger
#if canImport(PhotosUI)
import PhotosUI
#endif
// MARK: - Supporting Types // MARK: - Supporting Types
@@ -72,10 +69,8 @@ struct ContentView: View {
@State private var recordingTimer: Timer? @State private var recordingTimer: Timer?
@State private var recordingStartDate: Date? @State private var recordingStartDate: Date?
#if os(iOS) #if os(iOS)
@State private var showPhotoPicker = false @State private var showCameraPicker = false
@State private var selectedPhotoPickerItem: PhotosPickerItem?
#endif #endif
@State private var showAttachmentActions = false
@ScaledMetric(relativeTo: .body) private var headerHeight: CGFloat = 44 @ScaledMetric(relativeTo: .body) private var headerHeight: CGFloat = 44
@ScaledMetric(relativeTo: .subheadline) private var headerPeerIconSize: CGFloat = 11 @ScaledMetric(relativeTo: .subheadline) private var headerPeerIconSize: CGFloat = 11
@ScaledMetric(relativeTo: .subheadline) private var headerPeerCountFontSize: CGFloat = 12 @ScaledMetric(relativeTo: .subheadline) private var headerPeerCountFontSize: CGFloat = 12
@@ -209,10 +204,22 @@ struct ContentView: View {
} }
} }
#if os(iOS) #if os(iOS)
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoPickerItem, matching: .images) .sheet(isPresented: $showCameraPicker) {
.onChange(of: selectedPhotoPickerItem) { newItem in CameraPickerView { image in
guard let item = newItem else { return } showCameraPicker = false
Task { await handlePhotoSelection(item) } if let image = image {
Task {
do {
let processedURL = try ImageUtils.processImage(image)
await MainActor.run {
viewModel.sendImage(from: processedURL)
}
} catch {
SecureLogger.error("Camera image processing failed: \(error)", category: .session)
}
}
}
}
} }
#endif #endif
.sheet(isPresented: Binding( .sheet(isPresented: Binding(
@@ -223,15 +230,6 @@ struct ContentView: View {
ImagePreviewView(url: url) ImagePreviewView(url: url)
} }
} }
.confirmationDialog("Attach", isPresented: $showAttachmentActions, titleVisibility: .visible) {
#if os(iOS)
Button("Image") {
showAttachmentActions = false
DispatchQueue.main.async { showPhotoPicker = true }
}
#endif
Button("Cancel", role: .cancel) {}
}
.alert("Recording Error", isPresented: $showRecordingAlert, actions: { .alert("Recording Error", isPresented: $showRecordingAlert, actions: {
Button("OK", role: .cancel) {} Button("OK", role: .cancel) {}
}, message: { }, message: {
@@ -1805,13 +1803,18 @@ private extension ContentView {
} }
var attachmentButton: some View { var attachmentButton: some View {
Button(action: { showAttachmentActions = true }) { Button(action: {
Image(systemName: "paperclip.circle.fill") #if os(iOS)
showCameraPicker = true
#endif
}) {
Image(systemName: "camera.circle.fill")
.font(.bitchatSystem(size: 24)) .font(.bitchatSystem(size: 24))
.foregroundColor(composerAccentColor) .foregroundColor(composerAccentColor)
.frame(width: 36, height: 36) .frame(width: 36, height: 36)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel("Take photo")
} }
var sendOrMicButton: some View { var sendOrMicButton: some View {
@@ -1980,26 +1983,6 @@ private extension ContentView {
} }
} }
#if os(iOS)
func handlePhotoSelection(_ item: PhotosPickerItem) async {
defer { Task { @MainActor in selectedPhotoPickerItem = nil } }
do {
if let data = try await item.loadTransferable(type: Data.self) {
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("jpg")
try data.write(to: tempURL, options: Data.WritingOptions.atomic)
await MainActor.run {
viewModel.sendImage(from: tempURL) {
try? FileManager.default.removeItem(at: tempURL)
}
}
}
} catch {
SecureLogger.error("Photo picker load failed: \(error)", category: .session)
}
}
#endif
func applicationFilesDirectory() -> URL? { func applicationFilesDirectory() -> URL? {
// Cache the directory lookup to avoid repeated FileManager calls during view rendering // Cache the directory lookup to avoid repeated FileManager calls during view rendering
@@ -2143,3 +2126,41 @@ struct ImagePreviewView: View {
} }
#endif #endif
} }
#if os(iOS)
// MARK: - Camera Picker
struct CameraPickerView: UIViewControllerRepresentable {
let completion: (UIImage?) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = context.coordinator
picker.allowsEditing = false
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(completion: completion)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let completion: (UIImage?) -> Void
init(completion: @escaping (UIImage?) -> Void) {
self.completion = completion
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
let image = info[.originalImage] as? UIImage
completion(image)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
completion(nil)
}
}
}
#endif