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) } } }