// ChatListView.swift
import SwiftUI
struct ChatListView: View {
@EnvironmentObject var appState: AppState
@State private var showCreateGroup = false
var body: some View {
List {
// MARK: Pairs (DMs)
if !appState.peers.isEmpty {
Section("Contacts") {
ForEach(appState.peers) { peer in
NavigationLink(destination: ChatView(conversationID: peer.id, title: String(peer.displayName.prefix(8)).uppercased())) {
PeerRow(peer: peer, lastMessage: appState.conversations[peer.id]?.last)
}
}
}
}
// MARK: Groupes
if !appState.groups.isEmpty {
Section("Groupes") {
ForEach(appState.groups) { group in
NavigationLink(destination: ChatView(conversationID: group.id, title: group.name)) {
GroupRow(group: group, lastMessage: appState.conversations[group.id]?.last)
}
}
}
}
// MARK: Placeholder si vide
if appState.peers.isEmpty && appState.groups.isEmpty {
VStack(spacing: 12) {
Image(systemName: "antenna.radiowaves.left.and.right")
.font(.system(size: 44))
.foregroundColor(.secondary)
Text("Recherche de pairs…")
.foregroundColor(.secondary)
Text("Active Bluetooth et WiFi.\nLes appareils avec btmessage apparaîtront ici.")
.font(.caption)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 40)
.listRowBackground(Color.clear)
}
}
.navigationTitle("btmessage")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: { showCreateGroup = true }) {
Image(systemName: "person.3.fill")
}
ConnectionIndicator(count: appState.peers.count)
}
}
.sheet(isPresented: $showCreateGroup) {
CreateGroupView()
.environmentObject(appState)
}
.alert("Erreur",
isPresented: Binding(
get: { appState.errorMessage != nil },
set: { if !$0 { appState.errorMessage = nil } }
)
) {
Button("OK") { appState.errorMessage = nil }
} message: {
Text(appState.errorMessage ?? "")
}
}
}
// MARK: - PeerRow
private struct PeerRow: View {
let peer: PeerInfo
let lastMessage: ChatMessage?
var body: some View {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(peer.isKeyKnown ? Color.green.opacity(0.2) : Color.orange.opacity(0.2))
.frame(width: 44, height: 44)
Image(systemName: peer.isKeyKnown ? "lock.fill" : "lock.open")
.foregroundColor(peer.isKeyKnown ? .green : .orange)
}
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(String(peer.displayName.prefix(8)).uppercased())
.font(.headline)
Spacer()
if let msg = lastMessage {
Text(msg.timestamp, style: .time)
.font(.caption)
.foregroundColor(.secondary)
}
}
if let msg = lastMessage {
Text(msg.text)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1)
} else if peer.isKeyKnown {
Text("Clé PQC échangée")
.font(.caption)
.foregroundColor(.green)
} else {
Text("Échange de clé en cours…")
.font(.caption)
.foregroundColor(.orange)
}
}
}
.padding(.vertical, 4)
}
}
// MARK: - GroupRow
private struct GroupRow: View {
let group: GroupModel
let lastMessage: ChatMessage?
var body: some View {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(Color.blue.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: "person.3.fill")
.font(.system(size: 16))
.foregroundColor(.blue)
}
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(group.name)
.font(.headline)
Spacer()
if let msg = lastMessage {
Text(msg.timestamp, style: .time)
.font(.caption)
.foregroundColor(.secondary)
}
}
if let msg = lastMessage {
Text(msg.text)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1)
} else {
Text("\(group.memberIDs.count) membres · chiffré AES-256")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.padding(.vertical, 4)
}
}
// MARK: - ConnectionIndicator
private struct ConnectionIndicator: View {
let count: Int
var body: some View {
HStack(spacing: 4) {
Circle()
.fill(count > 0 ? Color.green : Color.gray)
.frame(width: 8, height: 8)
Text("\(count)")
.font(.caption)
.foregroundColor(.secondary)
}
}
}