// AppState.swift
import Foundation
import Combine
import CryptoKit
@MainActor
public class AppState: ObservableObject {
// MARK: - Published
@Published public var peers: [PeerInfo] = []
@Published public var groups: [GroupModel] = []
@Published public var conversations: [String: [ChatMessage]] = [:] // peerID ou groupID → messages
@Published public var selectedConversationID: String? = nil
@Published public var errorMessage: String? = nil
// MARK: - Dependencies
public let mesh: MeshManager
private let identity = IdentityManager.shared
private var cancellables = Set<AnyCancellable>()
// MARK: - Init
public init() {
let displayName = IdentityManager.shared.localPeerID
self.mesh = MeshManager(displayName: displayName)
// Charger les groupes persistés
self.groups = GroupKeyManager.shared.allGroups
setupBindings()
mesh.start()
}
// MARK: - DM
public func sendMessage(text: String, to peerID: String) {
appendOutgoing(text: text, conversationID: peerID, senderID: identity.localPeerID)
do {
let messageID = try mesh.sendMessage(text: text, to: peerID)
// Mettre à jour le message avec l'ID pour tracking ACK
if let lastIndex = conversations[peerID]?.indices.last {
conversations[peerID]?[lastIndex].id = messageID
}
} catch {
errorMessage = error.localizedDescription
}
}
// MARK: - Groupe
public func createGroup(name: String, memberIDs: [String]) {
let groupID = UUID().uuidString
let groupKey = SymmetricKey(size: .bits256)
let groupKeyData = groupKey.withUnsafeBytes { Data($0) }
let allMembers = Array(Set(memberIDs + [identity.localPeerID]))
let group = GroupModel(id: groupID, name: name, memberIDs: allMembers, creatorID: identity.localPeerID)
GroupKeyManager.shared.store(group: group, key: groupKey)
groups.append(group)
// Distribuer la clé chiffrée à chaque membre (sauf soi-même)
for memberID in memberIDs where memberID != identity.localPeerID {
do {
try mesh.sendGroupKeyDistrib(group: group, groupKeyData: groupKeyData, to: memberID)
} catch {
errorMessage = "Erreur distribution clé à \(memberID.prefix(8)): \(error.localizedDescription)"
}
}
}
public func sendGroupMessage(text: String, to groupID: String) {
appendOutgoing(text: text, conversationID: groupID, senderID: identity.localPeerID)
do {
try mesh.sendGroupMessage(text: text, to: groupID)
} catch {
errorMessage = error.localizedDescription
}
}
/// Point d'entrée unifié — route vers DM ou groupe automatiquement
public func send(text: String, to conversationID: String) {
if isGroup(conversationID) {
sendGroupMessage(text: text, to: conversationID)
} else {
sendMessage(text: text, to: conversationID)
}
}
public func messages(for conversationID: String) -> [ChatMessage] {
conversations[conversationID] ?? []
}
public func isGroup(_ id: String) -> Bool {
groups.contains(where: { $0.id == id })
}
// MARK: - Private
private func appendOutgoing(text: String, conversationID: String, senderID: String) {
let msg = ChatMessage(
id: UUID().uuidString,
conversationID: conversationID,
senderID: senderID,
text: text,
timestamp: Date(),
delivered: false
)
conversations[conversationID, default: []].append(msg)
}
private func setupBindings() {
mesh.$connectedPeers
.sink { [weak self] mcPeers in
Task { @MainActor [weak self] in
guard let self else { return }
for mcPeer in mcPeers {
if !self.peers.contains(where: { $0.id == mcPeer.displayName }) {
self.peers.append(PeerInfo(
id: mcPeer.displayName,
displayName: mcPeer.displayName,
publicKey: nil,
lastSeen: Date()
))
}
}
}
}
.store(in: &cancellables)
mesh.$knownPublicKeys
.sink { [weak self] keyMap in
Task { @MainActor [weak self] in
guard let self else { return }
for (peerID, pubKey) in keyMap {
if let idx = self.peers.firstIndex(where: { $0.id == peerID }) {
self.peers[idx].publicKey = pubKey
self.peers[idx].lastSeen = Date()
} else {
self.peers.append(PeerInfo(
id: peerID,
displayName: peerID,
publicKey: pubKey,
lastSeen: Date()
))
}
}
}
}
.store(in: &cancellables)
mesh.$incomingMessages
.sink { [weak self] received in
Task { @MainActor [weak self] in
guard let self, let last = received.last else { return }
let msg = ChatMessage(
id: last.messageID,
conversationID: last.conversationID,
senderID: last.senderID,
text: last.text,
timestamp: last.timestamp,
delivered: true
)
self.conversations[last.conversationID, default: []].append(msg)
}
}
.store(in: &cancellables)
mesh.onGroupKeyReceived = { [weak self] group in
Task { @MainActor [weak self] in
guard let self else { return }
if !self.groups.contains(where: { $0.id == group.id }) {
self.groups.append(group)
}
}
}
mesh.onMessageDelivered = { [weak self] messageID in
Task { @MainActor [weak self] in
self?.markMessageAsDelivered(messageID: messageID)
}
}
}
private func markMessageAsDelivered(messageID: String) {
for (conversationID, var messages) in conversations {
if let index = messages.firstIndex(where: { $0.id == messageID }) {
messages[index].delivered = true
conversations[conversationID] = messages
break
}
}
}
}