import SwiftUI import PhotosUI import UniformTypeIdentifiers import UIKit struct MessageInputView: View { @Binding var text: String let isSending: Bool let onSend: () -> Void var onImageSelected: ((Data) -> Void)? var onFileSelected: ((Data, String, String) -> Void)? // data, filename, mimeType var members: [ConversationMember] = [] @State private var isProcessing = false @State private var showFilePicker = false @State private var showPhotoPicker = false @State private var showMentionPopup = false @State private var mentionCandidates: [ConversationMember] = [] var body: some View { VStack(spacing: 0) { // Mention autocomplete popup if showMentionPopup && !mentionCandidates.isEmpty { ScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(mentionCandidates) { member in Button { completeMention(member: member) } label: { Text("@\(member.username)") .padding(.horizontal, 12) .padding(.vertical, 8) .frame(maxWidth: .infinity, alignment: .leading) } .buttonStyle(.plain) Divider() } } } .frame(maxHeight: 150) .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 8)) .padding(.horizontal) } HStack(spacing: 8) { // Attach button Menu { Button { showPhotoPicker = true } label: { Label("Photo", systemImage: "photo") } Button { showFilePicker = true } label: { Label("File", systemImage: "doc") } } label: { if isProcessing { ProgressView() .scaleEffect(0.8) } else { Image(systemName: "plus.circle.fill") .font(.title2) .foregroundStyle(.blue) } } .disabled(isProcessing || isSending) // Text field TextField("Message", text: $text, axis: .vertical) .textFieldStyle(.roundedBorder) .lineLimit(1...5) .onSubmit { if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { onSend() } } // Send button Button(action: onSend) { if isSending { ProgressView() .scaleEffect(0.8) } else { Image(systemName: "arrow.up.circle.fill") .font(.title2) .foregroundStyle(.blue) } } .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSending) } .padding(.horizontal) .padding(.vertical, 8) .background(.ultraThinMaterial) .sheet(isPresented: $showPhotoPicker) { ImagePickerView { data in isProcessing = true onImageSelected?(data) isProcessing = false } } .sheet(isPresented: $showFilePicker) { DocumentPickerView { url in guard url.startAccessingSecurityScopedResource() else { return } defer { url.stopAccessingSecurityScopedResource() } guard let data = try? Data(contentsOf: url) else { return } let filename = url.lastPathComponent let mimeType = UTType(filenameExtension: url.pathExtension)?.preferredMIMEType ?? "application/octet-stream" onFileSelected?(data, filename, mimeType) } } .onChange(of: text) { updateMentionCandidates() } } // end VStack } private func updateMentionCandidates() { // Look for @prefix at end of text guard let atRange = text.range(of: "@\\w*$", options: .regularExpression) else { showMentionPopup = false mentionCandidates = [] return } let prefix = String(text[atRange]).dropFirst().lowercased() // remove @ mentionCandidates = members.filter { member in prefix.isEmpty || member.username.lowercased().hasPrefix(prefix) } showMentionPopup = !mentionCandidates.isEmpty } private func completeMention(member: ConversationMember) { if let atRange = text.range(of: "@\\w*$", options: .regularExpression) { text.replaceSubrange(atRange, with: "@\(member.username) ") } showMentionPopup = false mentionCandidates = [] } } // MARK: - Image Picker (UIKit PHPicker wrapper) struct ImagePickerView: UIViewControllerRepresentable { let onImagePicked: (Data) -> Void func makeCoordinator() -> Coordinator { Coordinator(onImagePicked: onImagePicked) } func makeUIViewController(context: Context) -> PHPickerViewController { var config = PHPickerConfiguration() config.filter = .images config.selectionLimit = 1 let picker = PHPickerViewController(configuration: config) picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} class Coordinator: NSObject, PHPickerViewControllerDelegate { let onImagePicked: (Data) -> Void init(onImagePicked: @escaping (Data) -> Void) { self.onImagePicked = onImagePicked } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else { return } provider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in guard let uiImage = image as? UIImage, let data = uiImage.jpegData(compressionQuality: 0.9) else { return } DispatchQueue.main.async { self?.onImagePicked(data) } } } } } // MARK: - Document Picker (UIKit wrapper) struct DocumentPickerView: UIViewControllerRepresentable { let onPick: (URL) -> Void func makeCoordinator() -> Coordinator { Coordinator(onPick: onPick) } func makeUIViewController(context: Context) -> UIDocumentPickerViewController { let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.item]) picker.delegate = context.coordinator picker.allowsMultipleSelection = false return picker } func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} class Coordinator: NSObject, UIDocumentPickerDelegate { let onPick: (URL) -> Void init(onPick: @escaping (URL) -> Void) { self.onPick = onPick } func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } onPick(url) } } }