From 89b449a8a1adae605f47088336762a9bae524584 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 3 Aug 2026 17:27:27 +0530 Subject: [PATCH] fix(composer): match @mentions across Unicode NFC/NFD forms Stored nicknames are NFC after #1502, but typed mentions can be NFD; exact token contains() dropped them from the outbound mentions list. --- bitchat/ViewModels/ChatComposerCoordinator.swift | 16 +++++++++++----- .../ChatComposerCoordinatorContextTests.swift | 12 ++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/bitchat/ViewModels/ChatComposerCoordinator.swift b/bitchat/ViewModels/ChatComposerCoordinator.swift index d0d347f2..9eb8128b 100644 --- a/bitchat/ViewModels/ChatComposerCoordinator.swift +++ b/bitchat/ViewModels/ChatComposerCoordinator.swift @@ -125,16 +125,22 @@ final class ChatComposerCoordinator { ) let peerNicknames = context.meshPeerNicknames() - var validTokens = Set(peerNicknames.values) - validTokens.insert(context.nickname) - validTokens.insert(context.nickname + "#" + String(context.myPeerID.id.prefix(4))) + // Key by NFC so a typed combining-accent form still resolves to the + // stored (precomposed) nickname token used on the wire. + var tokensByCanonical = [String: String]() + for token in peerNicknames.values { + tokensByCanonical[token.normalizedNickname] = token + } + tokensByCanonical[context.nickname.normalizedNickname] = context.nickname + let selfSuffix = context.nickname + "#" + String(context.myPeerID.id.prefix(4)) + tokensByCanonical[selfSuffix.normalizedNickname] = selfSuffix var mentions: [String] = [] for match in matches { guard let range = Range(match.range(at: 1), in: content) else { continue } let mentionedName = String(content[range]) - if validTokens.contains(mentionedName) { - mentions.append(mentionedName) + if let canonical = tokensByCanonical[mentionedName.normalizedNickname] { + mentions.append(canonical) } } diff --git a/bitchatTests/ChatComposerCoordinatorContextTests.swift b/bitchatTests/ChatComposerCoordinatorContextTests.swift index d38ddc8a..71272253 100644 --- a/bitchatTests/ChatComposerCoordinatorContextTests.swift +++ b/bitchatTests/ChatComposerCoordinatorContextTests.swift @@ -192,4 +192,16 @@ struct ChatComposerCoordinatorContextTests { ) #expect(Set(mentions) == ["alice", "me", "me#0011"]) } + + @Test @MainActor + func parseMentions_matchesUnicodeEquivalentNicknames() { + let context = MockChatComposerContext() + let coordinator = ChatComposerCoordinator(context: context) + let precomposed = "caf\u{00E9}" + let decomposed = "cafe\u{0301}" + context.meshNicknamesByPeerID = [PeerID(str: "1111111111111111"): precomposed] + + let mentions = coordinator.parseMentions(from: "hi @\(decomposed)") + #expect(mentions == [precomposed]) + } }