125 lines
4.1 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|