@@ -3161,9 +3161,12 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
31613161
31623162 // For extremely long content, render as plain text to avoid heavy regex/layout work,
31633163 // unless the content includes Cashu tokens we want to chip-render below
3164+ // Compute NSString-backed length for regex/nsrange correctness with multi-byte characters
3165+ let nsContent = content as NSString
3166+ let nsLen = nsContent. length
31643167 let containsCashuEarly : Bool = {
31653168 let rx = Regexes . quickCashuPresence
3166- return rx. numberOfMatches ( in: content, options: [ ] , range: NSRange ( location: 0 , length: content . count ) ) > 0
3169+ return rx. numberOfMatches ( in: content, options: [ ] , range: NSRange ( location: 0 , length: nsLen ) ) > 0
31673170 } ( )
31683171 if ( content. count > 4000 || content. hasVeryLongToken ( threshold: 1024 ) ) && !containsCashuEarly {
31693172 var plainStyle = AttributeContainer ( )
@@ -3181,8 +3184,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
31813184 let lnurlRegex = Regexes . lnurl
31823185 let lightningSchemeRegex = Regexes . lightningScheme
31833186 let detector = Regexes . linkDetector
3184-
3185- let nsLen = content. count
31863187 let hasMentionsHint = content. contains ( " @ " )
31873188 let hasHashtagsHint = content. contains ( " # " )
31883189 let hasURLHint = content. contains ( " :// " ) || content. contains ( " www. " ) || content. contains ( " http " )
@@ -3262,17 +3263,19 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
32623263 for (range, type) in allMatches {
32633264 // Add text before match
32643265 if let nsRange = Range ( range, in: content) {
3265- let beforeText = String ( content [ lastEnd..< nsRange. lowerBound] )
3266- if !beforeText. isEmpty {
3267- var beforeStyle = AttributeContainer ( )
3268- beforeStyle. foregroundColor = baseColor
3269- beforeStyle. font = isSelf
3270- ? . system( size: 14 , weight: . bold, design: . monospaced)
3271- : . system( size: 14 , design: . monospaced)
3272- if isMentioned {
3273- beforeStyle. font = beforeStyle. font? . bold ( )
3266+ if lastEnd < nsRange. lowerBound {
3267+ let beforeText = String ( content [ lastEnd..< nsRange. lowerBound] )
3268+ if !beforeText. isEmpty {
3269+ var beforeStyle = AttributeContainer ( )
3270+ beforeStyle. foregroundColor = baseColor
3271+ beforeStyle. font = isSelf
3272+ ? . system( size: 14 , weight: . bold, design: . monospaced)
3273+ : . system( size: 14 , design: . monospaced)
3274+ if isMentioned {
3275+ beforeStyle. font = beforeStyle. font? . bold ( )
3276+ }
3277+ result. append ( AttributedString ( beforeText) . mergingAttributes ( beforeStyle) )
32743278 }
3275- result. append ( AttributedString ( beforeText) . mergingAttributes ( beforeStyle) )
32763279 }
32773280
32783281 // Add styled match
@@ -3380,7 +3383,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
33803383 result. append ( AttributedString ( matchText) . mergingAttributes ( matchStyle) )
33813384 }
33823385 }
3383- lastEnd = nsRange. upperBound
3386+ // Advance lastEnd safely in case of overlaps
3387+ if lastEnd < nsRange. upperBound {
3388+ lastEnd = nsRange. upperBound
3389+ }
33843390 }
33853391 }
33863392
0 commit comments