// ChatView.swift
import SwiftUI
struct ChatView: View {
@EnvironmentObject var appState: AppState
let conversationID: String
let title: String
@State private var inputText = ""
var messages: [ChatMessage] {
appState.messages(for: conversationID)
}
var body: some View {
VStack(spacing: 0) {
// Badge de sécurité
SecurityBadge(conversationID: conversationID)
.environmentObject(appState)
Divider()
// Liste des messages
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 4) {
ForEach(messages) { msg in
MessageBubble(message: msg)
.id(msg.id)
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
.onAppear { scrollToBottom(proxy: proxy) }
.onChange(of: messages.count) { _ in scrollToBottom(proxy: proxy) }
}
Divider()
// Barre de saisie
HStack(spacing: 8) {
TextField("Message", text: $inputText, axis: .vertical)
.textFieldStyle(.roundedBorder)
.lineLimit(1...5)
.onSubmit { send() }
Button(action: send) {
Image(systemName: "paperplane.fill")
.foregroundColor(inputText.isEmpty ? .gray : .blue)
}
.disabled(inputText.isEmpty)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
}
private func send() {
let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return }
inputText = ""
appState.send(text: text, to: conversationID)
}
private func scrollToBottom(proxy: ScrollViewProxy) {
if let last = messages.last {
withAnimation { proxy.scrollTo(last.id, anchor: .bottom) }
}
}
}
// MARK: - Security Badge
private struct SecurityBadge: View {
@EnvironmentObject var appState: AppState
let conversationID: String
var body: some View {
HStack(spacing: 6) {
if appState.isGroup(conversationID) {
Image(systemName: "lock.fill")
.font(.caption)
.foregroundColor(.blue)
Text("Groupe · AES-256-GCM")
.font(.caption2)
.foregroundColor(.secondary)
} else {
let peer = appState.peers.first(where: { $0.id == conversationID })
Image(systemName: peer?.isKeyKnown == true ? "lock.fill" : "lock.open")
.font(.caption)
.foregroundColor(peer?.isKeyKnown == true ? .green : .orange)
Text(peer?.isKeyKnown == true
? "XWing (ML-KEM-768 + X25519) · AES-256-GCM"
: "Échange de clé en cours…")
.font(.caption2)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 4)
}
}