Migrate five more coordinators to narrow context protocols

ChatTransportEventCoordinator, ChatPeerIdentityCoordinator,
ChatMediaTransferCoordinator, ChatVerificationCoordinator, and
ChatLifecycleCoordinator drop their unowned ChatViewModel back-refs for
narrow @MainActor contexts (20-36 members each), reusing shared
witnesses across protocols. The two remaining raw writers of
sentReadReceipts now route through owner intent ops
(unmarkReadReceiptsSent, syncReadReceiptsForSentMessages), closing the
gaps noted in the previous commit. NoiseEncryptionService stays fully
out of the verification coordinator via installNoiseSessionCallbacks.
26 new mock-context tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-10 21:54:54 +02:00
co-authored by Claude Fable 5
parent 707b22878d
commit 82736c4991
11 changed files with 2376 additions and 350 deletions
@@ -3,24 +3,206 @@ import BitLogger
import CoreBluetooth
import Foundation
final class ChatPeerIdentityCoordinator {
private unowned let viewModel: ChatViewModel
/// The narrow surface `ChatPeerIdentityCoordinator` needs from its owner.
///
/// Follows the `ChatDeliveryContext` exemplar: the coordinator depends on the
/// minimal context it actually uses instead of holding an `unowned` back-ref
/// to the whole `ChatViewModel`. This keeps the coordinator independently
/// testable (see `ChatPeerIdentityCoordinatorContextTests`) and makes its true
/// dependencies explicit. Several members are flattened service accesses
/// this coordinator implements the `ChatViewModel`-level peer-identity API, so
/// its context members deliberately sit one level below those wrappers
/// (`unifiedIsBlocked(_:)` vs `isPeerBlocked(_:)`, `unifiedFingerprint(for:)`
/// vs `getFingerprint(for:)`, ) to avoid call cycles.
@MainActor
protocol ChatPeerIdentityContext: AnyObject {
// MARK: Conversation state
var privateChats: [PeerID: [BitchatMessage]] { get set }
var unreadPrivateMessages: Set<PeerID> { get set }
var selectedPrivateChatPeer: PeerID? { get set }
var selectedPrivateChatFingerprint: String? { get set }
var nickname: String { get }
var myPeerID: PeerID { get }
var activeChannel: ChannelID { get }
/// Signals that message state changed so observers refresh (e.g. `objectWillChange.send()`).
func notifyUIChanged()
func addSystemMessage(_ content: String)
init(viewModel: ChatViewModel) {
self.viewModel = viewModel
// MARK: Private chat session lifecycle
/// Merges messages stored under alternate peer-ID representations into `peerID`'s chat.
/// Returns `true` when unread messages were discovered during consolidation.
@discardableResult
func consolidatePrivateMessages(for peerID: PeerID, peerNickname: String) -> Bool
/// Marks read receipts as sent for own messages already delivered/read in
/// `peerID`'s chat. (Single mutation path into the owner's
/// `sentReadReceipts`; this coordinator never touches the raw set.)
func syncReadReceiptsForSentMessages(for peerID: PeerID)
/// Re-targets the private chat session in the chat manager (no store-sync side effects).
func beginPrivateChatSession(with peerID: PeerID)
func synchronizePrivateConversationStore()
func synchronizeConversationSelectionStore()
func markPrivateMessagesAsRead(from peerID: PeerID)
// MARK: Unified peer service
var connectedPeers: Set<PeerID> { get }
/// The peer's current entry in the unified peer service, if known.
func unifiedPeer(for peerID: PeerID) -> BitchatPeer?
func unifiedIsBlocked(_ peerID: PeerID) -> Bool
func unifiedToggleFavorite(_ peerID: PeerID)
func unifiedFingerprint(for peerID: PeerID) -> String?
func unifiedPeerID(forNickname nickname: String) -> PeerID?
/// Resolves the ephemeral (short) peer ID for a known Noise public key, if connected.
func ephemeralPeerID(forNoiseKey noiseKey: Data) -> PeerID?
// MARK: Mesh & Noise sessions
func peerNickname(for peerID: PeerID) -> String?
func meshPeerNicknames() -> [PeerID: String]
func noiseSessionState(for peerID: PeerID) -> LazyHandshakeState
func triggerHandshake(with peerID: PeerID)
func hasEstablishedNoiseSession(with peerID: PeerID) -> Bool
func hasNoiseSession(with peerID: PeerID) -> Bool
/// Our own Noise identity fingerprint.
func noiseIdentityFingerprint() -> String
// MARK: Identity store (fingerprints & encryption status)
func setStoredFingerprint(_ fingerprint: String, for peerID: PeerID)
/// Moves the stored fingerprint mapping from `oldPeerID` to `newPeerID`,
/// falling back to `fallback` when none was stored. Returns the migrated fingerprint.
func migrateFingerprintMapping(from oldPeerID: PeerID, to newPeerID: PeerID, fallback: String?) -> String?
func isVerifiedFingerprint(_ fingerprint: String) -> Bool
func setEncryptionStatus(_ status: EncryptionStatus?, for peerID: PeerID)
func cachedEncryptionStatus(for peerID: PeerID) -> EncryptionStatus?
func setCachedEncryptionStatus(_ status: EncryptionStatus, for peerID: PeerID)
func invalidateStoredEncryptionCache(for peerID: PeerID?)
func socialIdentity(forFingerprint fingerprint: String) -> SocialIdentity?
// MARK: Geohash & Nostr
var geoNicknames: [String: String] { get }
func visibleGeohashPeople() -> [GeoPerson]
/// Records the Nostr pubkey behind a (possibly virtual) peer ID.
func registerNostrKeyMapping(_ pubkey: String, for peerID: PeerID)
func bridgedNostrPublicKey(for noiseKey: Data) -> String?
func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool)
}
extension ChatViewModel: ChatPeerIdentityContext {
// `privateChats`, `unreadPrivateMessages`, `selectedPrivateChatPeer`,
// `selectedPrivateChatFingerprint`, `nickname`, `myPeerID`,
// `activeChannel`, `connectedPeers`, `geoNicknames`, `notifyUIChanged()`,
// `addSystemMessage(_:)`, `peerNickname(for:)`, `meshPeerNicknames()`,
// `ephemeralPeerID(forNoiseKey:)`, `unifiedPeer(for:)`,
// `registerNostrKeyMapping(_:for:)`, `visibleGeohashPeople()`,
// `markPrivateMessagesAsRead(from:)`, `sendFavoriteNotificationViaNostr`,
// and the conversation-store sync methods are shared requirements with
// the other contexts or satisfied by existing `ChatViewModel` members.
// The single-writer intent op `syncReadReceiptsForSentMessages(for:)`
// lives next to its backing state in `ChatViewModel`. The members below
// flatten nested service accesses into intent-named calls.
@discardableResult
func consolidatePrivateMessages(for peerID: PeerID, peerNickname: String) -> Bool {
privateChatManager.consolidateMessages(
for: peerID,
peerNickname: peerNickname,
persistedReadReceipts: sentReadReceipts
)
}
func beginPrivateChatSession(with peerID: PeerID) {
privateChatManager.startChat(with: peerID)
}
func unifiedIsBlocked(_ peerID: PeerID) -> Bool {
unifiedPeerService.isBlocked(peerID)
}
func unifiedToggleFavorite(_ peerID: PeerID) {
unifiedPeerService.toggleFavorite(peerID)
}
func unifiedFingerprint(for peerID: PeerID) -> String? {
unifiedPeerService.getFingerprint(for: peerID)
}
func unifiedPeerID(forNickname nickname: String) -> PeerID? {
unifiedPeerService.getPeerID(for: nickname)
}
func noiseSessionState(for peerID: PeerID) -> LazyHandshakeState {
meshService.getNoiseSessionState(for: peerID)
}
func triggerHandshake(with peerID: PeerID) {
meshService.triggerHandshake(with: peerID)
}
func hasEstablishedNoiseSession(with peerID: PeerID) -> Bool {
meshService.getNoiseService().hasEstablishedSession(with: peerID)
}
func hasNoiseSession(with peerID: PeerID) -> Bool {
meshService.getNoiseService().hasSession(with: peerID)
}
func noiseIdentityFingerprint() -> String {
meshService.getNoiseService().getIdentityFingerprint()
}
func setStoredFingerprint(_ fingerprint: String, for peerID: PeerID) {
peerIdentityStore.setFingerprint(fingerprint, for: peerID)
}
func migrateFingerprintMapping(from oldPeerID: PeerID, to newPeerID: PeerID, fallback: String?) -> String? {
peerIdentityStore.migrateFingerprintMapping(from: oldPeerID, to: newPeerID, fallback: fallback)
}
func isVerifiedFingerprint(_ fingerprint: String) -> Bool {
peerIdentityStore.isVerified(fingerprint)
}
func setEncryptionStatus(_ status: EncryptionStatus?, for peerID: PeerID) {
peerIdentityStore.setEncryptionStatus(status, for: peerID)
}
func cachedEncryptionStatus(for peerID: PeerID) -> EncryptionStatus? {
peerIdentityStore.cachedEncryptionStatus(for: peerID)
}
func setCachedEncryptionStatus(_ status: EncryptionStatus, for peerID: PeerID) {
peerIdentityStore.setCachedEncryptionStatus(status, for: peerID)
}
func invalidateStoredEncryptionCache(for peerID: PeerID?) {
peerIdentityStore.invalidateEncryptionCache(for: peerID)
}
func socialIdentity(forFingerprint fingerprint: String) -> SocialIdentity? {
identityManager.getSocialIdentity(for: fingerprint)
}
func bridgedNostrPublicKey(for noiseKey: Data) -> String? {
idBridge.getNostrPublicKey(for: noiseKey)
}
}
final class ChatPeerIdentityCoordinator {
private unowned let context: any ChatPeerIdentityContext
init(context: any ChatPeerIdentityContext) {
self.context = context
}
@MainActor
func openMostRelevantPrivateChat() {
let unreadSorted = viewModel.unreadPrivateMessages
.map { ($0, viewModel.privateChats[$0]?.last?.timestamp ?? Date.distantPast) }
let unreadSorted = context.unreadPrivateMessages
.map { ($0, context.privateChats[$0]?.last?.timestamp ?? Date.distantPast) }
.sorted { $0.1 > $1.1 }
if let target = unreadSorted.first?.0 {
startPrivateChat(with: target)
return
}
let recent = viewModel.privateChats
let recent = context.privateChats
.map { (id: $0.key, ts: $0.value.last?.timestamp ?? Date.distantPast) }
.sorted { $0.ts > $1.ts }
if let target = recent.first?.id {
@@ -30,7 +212,7 @@ final class ChatPeerIdentityCoordinator {
@MainActor
func isPeerBlocked(_ peerID: PeerID) -> Bool {
viewModel.unifiedPeerService.isBlocked(peerID)
context.unifiedIsBlocked(peerID)
}
@MainActor
@@ -38,24 +220,24 @@ final class ChatPeerIdentityCoordinator {
var noiseKeyPeerID: PeerID?
var nostrPeerID: PeerID?
if let peer = viewModel.unifiedPeerService.getPeer(by: peerID) {
if let peer = context.unifiedPeer(for: peerID) {
noiseKeyPeerID = PeerID(hexData: peer.noisePublicKey)
if let nostrHex = peer.nostrPublicKey {
nostrPeerID = PeerID(nostr_: nostrHex)
}
}
let context = ChatUnreadPeerContext(
let unreadContext = ChatUnreadPeerContext(
peerID: peerID,
noiseKeyPeerID: noiseKeyPeerID,
nostrPeerID: nostrPeerID,
nickname: viewModel.meshService.peerNickname(peerID: peerID)
nickname: context.peerNickname(for: peerID)
)
return ChatUnreadStateResolver.hasUnreadMessages(
for: context,
unreadPrivateMessages: viewModel.unreadPrivateMessages,
privateChats: viewModel.privateChats
for: unreadContext,
unreadPrivateMessages: context.unreadPrivateMessages,
privateChats: context.privateChats
)
}
@@ -66,8 +248,8 @@ final class ChatPeerIdentityCoordinator {
return
}
viewModel.unifiedPeerService.toggleFavorite(peerID)
viewModel.objectWillChange.send()
context.unifiedToggleFavorite(peerID)
context.notifyUIChanged()
}
@MainActor
@@ -76,36 +258,36 @@ final class ChatPeerIdentityCoordinator {
return FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey)?.isFavorite ?? false
}
return viewModel.unifiedPeerService.getPeer(by: peerID)?.isFavorite ?? false
return context.unifiedPeer(for: peerID)?.isFavorite ?? false
}
@MainActor
func updatePrivateChatPeerIfNeeded() {
guard let chatFingerprint = viewModel.selectedPrivateChatFingerprint,
guard let chatFingerprint = context.selectedPrivateChatFingerprint,
let currentPeerID = currentPeerID(forFingerprint: chatFingerprint) else {
return
}
if let oldPeerID = viewModel.selectedPrivateChatPeer, oldPeerID != currentPeerID {
if let oldPeerID = context.selectedPrivateChatPeer, oldPeerID != currentPeerID {
migrateChatState(from: oldPeerID, to: currentPeerID)
viewModel.selectedPrivateChatPeer = currentPeerID
} else if viewModel.selectedPrivateChatPeer == nil {
viewModel.selectedPrivateChatPeer = currentPeerID
context.selectedPrivateChatPeer = currentPeerID
} else if context.selectedPrivateChatPeer == nil {
context.selectedPrivateChatPeer = currentPeerID
}
var unread = viewModel.unreadPrivateMessages
var unread = context.unreadPrivateMessages
unread.remove(currentPeerID)
viewModel.unreadPrivateMessages = unread
context.unreadPrivateMessages = unread
}
@MainActor
func startPrivateChat(with peerID: PeerID) {
guard peerID != viewModel.meshService.myPeerID else { return }
guard peerID != context.myPeerID else { return }
let peerNickname = viewModel.meshService.peerNickname(peerID: peerID) ?? "unknown"
let peerNickname = context.peerNickname(for: peerID) ?? "unknown"
if viewModel.unifiedPeerService.isBlocked(peerID) {
viewModel.addSystemMessage(
if context.unifiedIsBlocked(peerID) {
context.addSystemMessage(
String(
format: String(
localized: "system.chat.blocked",
@@ -118,9 +300,9 @@ final class ChatPeerIdentityCoordinator {
return
}
if let peer = viewModel.unifiedPeerService.getPeer(by: peerID),
if let peer = context.unifiedPeer(for: peerID),
peer.isFavorite && !peer.theyFavoritedUs && !peer.isConnected {
viewModel.addSystemMessage(
context.addSystemMessage(
String(
format: String(
localized: "system.chat.requires_favorite",
@@ -133,16 +315,12 @@ final class ChatPeerIdentityCoordinator {
return
}
_ = viewModel.privateChatManager.consolidateMessages(
for: peerID,
peerNickname: peerNickname,
persistedReadReceipts: viewModel.sentReadReceipts
)
_ = context.consolidatePrivateMessages(for: peerID, peerNickname: peerNickname)
if !peerID.isGeoDM && !peerID.isGeoChat {
switch viewModel.meshService.getNoiseSessionState(for: peerID) {
switch context.noiseSessionState(for: peerID) {
case .none, .failed:
viewModel.meshService.triggerHandshake(with: peerID)
context.triggerHandshake(with: peerID)
case .handshakeQueued, .handshaking, .established:
break
}
@@ -150,28 +328,24 @@ final class ChatPeerIdentityCoordinator {
SecureLogger.debug("GeoDM: skipping mesh handshake for virtual peerID=\(peerID)", category: .session)
}
viewModel.privateChatManager.syncReadReceiptsForSentMessages(
peerID: peerID,
nickname: viewModel.nickname,
externalReceipts: &viewModel.sentReadReceipts
)
context.syncReadReceiptsForSentMessages(for: peerID)
if let fingerprint = getFingerprint(for: peerID) {
viewModel.peerIdentityStore.setFingerprint(fingerprint, for: peerID)
viewModel.peerIdentityStore.setSelectedPrivateChatFingerprint(fingerprint)
context.setStoredFingerprint(fingerprint, for: peerID)
context.selectedPrivateChatFingerprint = fingerprint
} else {
viewModel.peerIdentityStore.setSelectedPrivateChatFingerprint(nil)
context.selectedPrivateChatFingerprint = nil
}
viewModel.privateChatManager.startChat(with: peerID)
viewModel.synchronizePrivateConversationStore()
viewModel.synchronizeConversationSelectionStore()
viewModel.markPrivateMessagesAsRead(from: peerID)
context.beginPrivateChatSession(with: peerID)
context.synchronizePrivateConversationStore()
context.synchronizeConversationSelectionStore()
context.markPrivateMessagesAsRead(from: peerID)
}
@MainActor
func endPrivateChat() {
viewModel.selectedPrivateChatPeer = nil
viewModel.peerIdentityStore.setSelectedPrivateChatFingerprint(nil)
context.selectedPrivateChatPeer = nil
context.selectedPrivateChatFingerprint = nil
}
@MainActor
@@ -182,8 +356,8 @@ final class ChatPeerIdentityCoordinator {
func handleFavoriteStatusChanged(_ notification: Notification) {
guard let peerPublicKey = notification.userInfo?["peerPublicKey"] as? Data else { return }
Task { @MainActor [weak viewModel] in
guard let viewModel else { return }
Task { @MainActor [weak context = self.context] in
guard let context else { return }
if let isKeyUpdate = notification.userInfo?["isKeyUpdate"] as? Bool,
isKeyUpdate,
@@ -200,28 +374,26 @@ final class ChatPeerIdentityCoordinator {
let peerID = PeerID(hexData: peerPublicKey)
let action = isFavorite ? "favorited" : "unfavorited"
let peerNickname = favoriteNotificationNickname(for: peerID, peerPublicKey: peerPublicKey)
viewModel.addSystemMessage("\(peerNickname) \(action) you")
context.addSystemMessage("\(peerNickname) \(action) you")
}
}
}
@MainActor
func updateEncryptionStatusForPeers() {
for peerID in viewModel.connectedPeers {
for peerID in context.connectedPeers {
updateEncryptionStatus(for: peerID)
}
}
@MainActor
func updateEncryptionStatus(for peerID: PeerID) {
let noiseService = viewModel.meshService.getNoiseService()
if noiseService.hasEstablishedSession(with: peerID) {
viewModel.peerIdentityStore.setEncryptionStatus(verifiedEncryptionStatus(for: peerID), for: peerID)
} else if noiseService.hasSession(with: peerID) {
viewModel.peerIdentityStore.setEncryptionStatus(.noiseHandshaking, for: peerID)
if context.hasEstablishedNoiseSession(with: peerID) {
context.setEncryptionStatus(verifiedEncryptionStatus(for: peerID), for: peerID)
} else if context.hasNoiseSession(with: peerID) {
context.setEncryptionStatus(.noiseHandshaking, for: peerID)
} else {
viewModel.peerIdentityStore.setEncryptionStatus(nil, for: peerID)
context.setEncryptionStatus(nil, for: peerID)
}
invalidateEncryptionCache(for: peerID)
@@ -229,12 +401,12 @@ final class ChatPeerIdentityCoordinator {
@MainActor
func getEncryptionStatus(for peerID: PeerID) -> EncryptionStatus {
if let cachedStatus = viewModel.peerIdentityStore.cachedEncryptionStatus(for: peerID) {
if let cachedStatus = context.cachedEncryptionStatus(for: peerID) {
return cachedStatus
}
let hasEverEstablishedSession = getFingerprint(for: peerID) != nil
let sessionState = viewModel.meshService.getNoiseSessionState(for: peerID)
let sessionState = context.noiseSessionState(for: peerID)
let status: EncryptionStatus
switch sessionState {
@@ -248,18 +420,18 @@ final class ChatPeerIdentityCoordinator {
status = hasEverEstablishedSession ? verifiedEncryptionStatus(for: peerID) : .none
}
viewModel.peerIdentityStore.setCachedEncryptionStatus(status, for: peerID)
context.setCachedEncryptionStatus(status, for: peerID)
return status
}
@MainActor
func invalidateEncryptionCache(for peerID: PeerID? = nil) {
viewModel.peerIdentityStore.invalidateEncryptionCache(for: peerID)
context.invalidateStoredEncryptionCache(for: peerID)
}
@MainActor
func getFingerprint(for peerID: PeerID) -> String? {
viewModel.unifiedPeerService.getFingerprint(for: peerID)
context.unifiedFingerprint(for: peerID)
}
@MainActor
@@ -270,12 +442,12 @@ final class ChatPeerIdentityCoordinator {
return peerID.id
}
if let nickname = viewModel.meshService.getPeerNicknames()[peerID] {
if let nickname = context.meshPeerNicknames()[peerID] {
return nickname
}
if let fingerprint = getFingerprint(for: peerID),
let identity = viewModel.identityManager.getSocialIdentity(for: fingerprint) {
let identity = context.socialIdentity(forFingerprint: fingerprint) {
if let petname = identity.localPetname {
return petname
}
@@ -289,19 +461,18 @@ final class ChatPeerIdentityCoordinator {
@MainActor
func getMyFingerprint() -> String {
viewModel.meshService.getNoiseService().getIdentityFingerprint()
context.noiseIdentityFingerprint()
}
@MainActor
func getPeerIDForNickname(_ nickname: String) -> PeerID? {
switch viewModel.activeChannel {
switch context.activeChannel {
case .location:
if nickname.contains("#"),
let person = viewModel.publicConversationCoordinator
.visibleGeohashPeople()
let person = context.visibleGeohashPeople()
.first(where: { $0.displayName == nickname }) {
let conversationKey = PeerID(nostr_: person.id)
viewModel.registerNostrKeyMapping(person.id, for: conversationKey)
context.registerNostrKeyMapping(person.id, for: conversationKey)
return conversationKey
}
@@ -310,9 +481,9 @@ final class ChatPeerIdentityCoordinator {
.first
.map(String.init)?
.lowercased() ?? nickname.lowercased()
if let pubkey = viewModel.geoNicknames.first(where: { $0.value.lowercased() == base })?.key {
if let pubkey = context.geoNicknames.first(where: { $0.value.lowercased() == base })?.key {
let conversationKey = PeerID(nostr_: pubkey)
viewModel.registerNostrKeyMapping(pubkey, for: conversationKey)
context.registerNostrKeyMapping(pubkey, for: conversationKey)
return conversationKey
}
@@ -320,12 +491,12 @@ final class ChatPeerIdentityCoordinator {
break
}
return viewModel.unifiedPeerService.getPeerID(for: nickname)
return context.unifiedPeerID(forNickname: nickname)
}
@MainActor
func nicknameForPeer(_ peerID: PeerID) -> String {
if let name = viewModel.meshService.peerNickname(peerID: peerID) {
if let name = context.peerNickname(for: peerID) {
return name
}
if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
@@ -344,7 +515,7 @@ final class ChatPeerIdentityCoordinator {
private extension ChatPeerIdentityCoordinator {
@MainActor
func currentPeerID(forFingerprint fingerprint: String) -> PeerID? {
for peerID in viewModel.connectedPeers where getFingerprint(for: peerID) == fingerprint {
for peerID in context.connectedPeers where getFingerprint(for: peerID) == fingerprint {
return peerID
}
return nil
@@ -352,8 +523,8 @@ private extension ChatPeerIdentityCoordinator {
@MainActor
func migrateChatState(from oldPeerID: PeerID, to newPeerID: PeerID) {
if let oldMessages = viewModel.privateChats[oldPeerID] {
var chats = viewModel.privateChats
if let oldMessages = context.privateChats[oldPeerID] {
var chats = context.privateChats
chats[newPeerID, default: []].append(contentsOf: oldMessages)
chats[newPeerID]?.sort { $0.timestamp < $1.timestamp }
@@ -367,45 +538,45 @@ private extension ChatPeerIdentityCoordinator {
}
chats.removeValue(forKey: oldPeerID)
viewModel.privateChats = chats
context.privateChats = chats
}
var unread = viewModel.unreadPrivateMessages
var unread = context.unreadPrivateMessages
if unread.contains(oldPeerID) {
unread.remove(oldPeerID)
unread.insert(newPeerID)
viewModel.unreadPrivateMessages = unread
context.unreadPrivateMessages = unread
}
}
@MainActor
func migrateNoiseKeyUpdate(oldPeerID: PeerID, newPeerID: PeerID) {
if viewModel.selectedPrivateChatPeer == oldPeerID {
if context.selectedPrivateChatPeer == oldPeerID {
SecureLogger.info("📱 Updating private chat peer ID due to key change: \(oldPeerID) -> \(newPeerID)", category: .session)
} else if viewModel.privateChats[oldPeerID] != nil {
} else if context.privateChats[oldPeerID] != nil {
SecureLogger.debug("📱 Migrating private chat messages from \(oldPeerID) to \(newPeerID)", category: .session)
}
migrateChatState(from: oldPeerID, to: newPeerID)
if viewModel.selectedPrivateChatPeer == oldPeerID {
viewModel.selectedPrivateChatPeer = newPeerID
if context.selectedPrivateChatPeer == oldPeerID {
context.selectedPrivateChatPeer = newPeerID
}
if let fingerprint = viewModel.peerIdentityStore.migrateFingerprintMapping(
if let fingerprint = context.migrateFingerprintMapping(
from: oldPeerID,
to: newPeerID,
fallback: getFingerprint(for: newPeerID)
) {
if viewModel.selectedPrivateChatPeer == newPeerID {
viewModel.peerIdentityStore.setSelectedPrivateChatFingerprint(fingerprint)
if context.selectedPrivateChatPeer == newPeerID {
context.selectedPrivateChatFingerprint = fingerprint
}
}
}
@MainActor
func favoriteNotificationNickname(for peerID: PeerID, peerPublicKey: Data) -> String {
if let nickname = viewModel.meshService.peerNickname(peerID: peerID) {
if let nickname = context.peerNickname(for: peerID) {
return nickname
}
if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: peerPublicKey) {
@@ -417,7 +588,7 @@ private extension ChatPeerIdentityCoordinator {
@MainActor
func verifiedEncryptionStatus(for peerID: PeerID) -> EncryptionStatus {
if let fingerprint = getFingerprint(for: peerID),
viewModel.peerIdentityStore.isVerified(fingerprint) {
context.isVerifiedFingerprint(fingerprint) {
return .noiseVerified
}
return .noiseSecured
@@ -425,18 +596,18 @@ private extension ChatPeerIdentityCoordinator {
@MainActor
func toggleFavoriteForNoiseKey(_ noisePublicKey: Data, peerID: PeerID) {
if let ephemeralID = viewModel.unifiedPeerService.peers.first(where: { $0.noisePublicKey == noisePublicKey })?.peerID {
viewModel.unifiedPeerService.toggleFavorite(ephemeralID)
viewModel.objectWillChange.send()
if let ephemeralID = context.ephemeralPeerID(forNoiseKey: noisePublicKey) {
context.unifiedToggleFavorite(ephemeralID)
context.notifyUIChanged()
return
}
let currentStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey)
let fallbackNickname = viewModel.privateChats[peerID]?.first { $0.senderPeerID == peerID }?.sender
let fallbackNickname = context.privateChats[peerID]?.first { $0.senderPeerID == peerID }?.sender
let plan = ChatFavoriteTogglePolicy.plan(
currentStatus: currentStatus.map(ChatFavoriteStatusSnapshot.init),
fallbackNickname: fallbackNickname,
bridgedNostrKey: viewModel.idBridge.getNostrPublicKey(for: noisePublicKey)
bridgedNostrKey: context.bridgedNostrPublicKey(for: noisePublicKey)
)
switch plan.persistenceAction {
@@ -451,10 +622,10 @@ private extension ChatPeerIdentityCoordinator {
FavoritesPersistenceService.shared.removeFavorite(peerNoisePublicKey: noisePublicKey)
}
viewModel.objectWillChange.send()
context.notifyUIChanged()
if case .send(let isFavorite) = plan.notification {
viewModel.sendFavoriteNotificationViaNostr(
context.sendFavoriteNotificationViaNostr(
noisePublicKey: noisePublicKey,
isFavorite: isFavorite
)