ios_client
This commit is contained in:
215
ios_client 0.8.5/Kecalek/Views/Chat/MessageInputView.swift
Normal file
215
ios_client 0.8.5/Kecalek/Views/Chat/MessageInputView.swift
Normal file
@@ -0,0 +1,215 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user