From 6167c9f6eabedf15a39302345935348ef7d1045a Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 3 Aug 2026 17:27:03 +0530 Subject: [PATCH] fix(dm): NFC-normalize nicknames when consolidating geo DM threads Wire senders can arrive NFD while stored peer nicknames are NFC after #1502; lowercase-only matching left temp geo DM history orphaned. --- bitchat/Services/PrivateChatManager.swift | 12 ++++-- .../Services/PrivateChatManagerTests.swift | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/bitchat/Services/PrivateChatManager.swift b/bitchat/Services/PrivateChatManager.swift index dcf4631b..427a1234 100644 --- a/bitchat/Services/PrivateChatManager.swift +++ b/bitchat/Services/PrivateChatManager.swift @@ -87,7 +87,7 @@ final class PrivateChatManager: ObservableObject { /// This ensures messages from stable Noise keys and temporary Nostr peer IDs are merged. /// - Parameters: /// - peerID: The target peer ID to consolidate messages into - /// - peerNickname: The peer's display name (lowercased for matching) + /// - peerNickname: The peer's display name (NFC + lowercased for matching) /// - persistedReadReceipts: The persisted read receipts set from ChatViewModel (UserDefaults-backed) /// - Returns: True if any unread messages were found during consolidation @MainActor @@ -140,13 +140,17 @@ final class PrivateChatManager: ObservableObject { } } - // 2. Consolidate from temporary Nostr peer IDs (nostr_* prefixed) - let normalizedNickname = peerNickname.lowercased() + // 2. Consolidate from temporary Nostr peer IDs (nostr_* prefixed). + // Match on NFC so a wire sender typed with combining accents still + // merges into the stored peer nickname (precomposed after #1502). + let normalizedNickname = peerNickname.normalizedNickname.lowercased() var tempPeerIDsToConsolidate: [PeerID] = [] for (storedPeerID, messages) in privateChats { if storedPeerID.isGeoDM && storedPeerID != peerID { - let nicknamesMatch = messages.allSatisfy { $0.sender.lowercased() == normalizedNickname } + let nicknamesMatch = messages.allSatisfy { + $0.sender.normalizedNickname.lowercased() == normalizedNickname + } if nicknamesMatch && !messages.isEmpty { tempPeerIDsToConsolidate.append(storedPeerID) } diff --git a/bitchatTests/Services/PrivateChatManagerTests.swift b/bitchatTests/Services/PrivateChatManagerTests.swift index 69b49aaf..49bb5ca8 100644 --- a/bitchatTests/Services/PrivateChatManagerTests.swift +++ b/bitchatTests/Services/PrivateChatManagerTests.swift @@ -258,6 +258,44 @@ struct PrivateChatManagerTests { #expect(!manager.unreadMessages.contains(tempPeerID)) } + @Test @MainActor + func consolidateMessages_movesTemporaryGeoDMHistoryByCanonicalNickname() async { + // Wire sender can arrive NFD while the peer nickname is stored NFC + // (#1502). Consolidation must still merge the temp geo DM thread. + let transport = MockTransport() + let (manager, store) = Self.makeManager(transport: transport) + let peerID = PeerID(str: "0011223344556677") + let tempPeerID = PeerID(nostr_: "0000000000000000000000000000000000000000000000000000000000000042") + let decomposed = "cafe\u{0301}" + let precomposed = "caf\u{00E9}" + + store.append( + BitchatMessage( + id: "geo-nfc-msg", + sender: decomposed, + content: "Geo hello", + timestamp: Date(), + isRelay: false, + isPrivate: true, + recipientNickname: "Me", + senderPeerID: tempPeerID + ), + to: .directPeer(tempPeerID) + ) + store.markUnread(.directPeer(tempPeerID)) + + let hadUnread = manager.consolidateMessages( + for: peerID, + peerNickname: precomposed, + persistedReadReceipts: [] + ) + + #expect(hadUnread) + #expect(manager.privateChats[tempPeerID] == nil) + #expect(manager.privateChats[peerID]?.count == 1) + #expect(manager.unreadMessages.contains(peerID)) + } + @Test @MainActor func syncReadReceiptsForSentMessages_onlyCopiesDeliveredAndRead() async { let transport = MockTransport()