mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-10-25 07:41:41 +00:00
Merge 7162d3f451e6379173619df8c8fddb0ae58d277d into 9b84b361225facd8e623f25d76f889d3dc54a879
This commit is contained in:
commit
cab7a04bfc
@ -41,7 +41,9 @@ final class MessageFormattingEngine {
|
||||
enum Patterns {
|
||||
static let hashtag = SafeRegex.compile("#([a-zA-Z0-9_]+)")
|
||||
|
||||
static let mention = SafeRegex.compile("@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)")
|
||||
// Include \p{M} so NFD nicknames (base letter + combining mark) still
|
||||
// parse as one mention token; NFC forms are covered by \p{L} alone.
|
||||
static let mention = SafeRegex.compile("@([\\p{L}\\p{M}0-9_]+(?:#[a-fA-F0-9]{4})?)")
|
||||
|
||||
static let cashu = SafeRegex.compile("\\bcashu[AB][A-Za-z0-9._-]{40,}\\b")
|
||||
|
||||
|
||||
@ -116,6 +116,9 @@ final class ChatComposerCoordinator {
|
||||
}
|
||||
|
||||
func parseMentions(from content: String) -> [String] {
|
||||
// Canonicalize before matching so NFD typed text and NFC peer tokens
|
||||
// share one codepoint sequence (regex also accepts combining marks).
|
||||
let content = content.precomposedStringWithCanonicalMapping
|
||||
let regex = ChatViewModel.Patterns.mention
|
||||
let nsContent = content as NSString
|
||||
let matches = regex.matches(
|
||||
@ -125,16 +128,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user