fchaty/FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift
2026-07-26 21:43:39 +02:00

125 lines
4.1 KiB
Swift

import AppKit
import SwiftUI
@MainActor
struct ChatView: View {
@ObservedObject private var session: AppSession
@StateObject private var viewModel: ChatViewModel
init(session: AppSession) {
self.session = session
_viewModel = StateObject(wrappedValue: ChatViewModel(session: session))
}
init() {
self.init(session: AppSession.shared)
}
var body: some View {
VStack(spacing: 0) {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 12) {
ForEach(session.messages) { message in
MessageBubble(
message: message,
isSent: message.from == KeychainStore.installationID
)
.id(message.id)
}
}
.padding()
}
.onAppear {
scrollToLatest(using: proxy)
Task { await session.markVisibleMessagesRead() }
}
.onChange(of: session.messages.last?.id) { _, _ in
scrollToLatest(using: proxy)
Task { await session.markVisibleMessagesRead() }
}
}
Divider()
VStack(alignment: .leading, spacing: 8) {
if let errorMessage = viewModel.errorMessage {
Label(errorMessage, systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.red)
}
HStack(alignment: .center, spacing: 8) {
Button {
chooseFile()
} label: {
Image(systemName: "paperclip")
}
.help("Choose a file")
.disabled(viewModel.isSending || viewModel.isRecording)
TextField("Message", text: $viewModel.draft)
.textFieldStyle(.roundedBorder)
.onSubmit {
Task {
await viewModel.sendText()
}
}
Image(systemName: viewModel.isRecording ? "mic.fill" : "mic")
.foregroundStyle(viewModel.isRecording ? .red : .primary)
.frame(width: 28, height: 28)
.contentShape(Circle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
viewModel.startRecording()
}
.onEnded { _ in
viewModel.stopRecordingAndSend()
}
)
.help("Hold to record audio")
Button("Send") {
Task {
await viewModel.sendText()
}
}
.buttonStyle(.borderedProminent)
.disabled(
viewModel.isSending ||
viewModel.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
)
}
}
.padding()
}
}
private func chooseFile() {
let panel = NSOpenPanel()
panel.canChooseDirectories = false
panel.canChooseFiles = true
panel.allowsMultipleSelection = false
guard panel.runModal() == .OK, let url = panel.url else {
return
}
Task {
await viewModel.sendFile(url: url)
}
}
private func scrollToLatest(using proxy: ScrollViewProxy) {
guard let latestID = session.messages.last?.id else {
return
}
withAnimation {
proxy.scrollTo(latestID, anchor: .bottom)
}
}
}