Compare commits

..
Author SHA1 Message Date
jack 2b363b7062 Resolve merge conflicts in ChatViewModel.swift (geohash sampling comments and removed background nudge) 2025-08-28 09:09:55 +01:00
jack 7d672f2d69 Geohash notifications: ensure triggering message appears\n\n- On zero→alive sampling, pre-populate geoTimelines[gh] with the triggering message\n- Dedup on per-geohash timeline and UI messages by message ID to avoid duplicates after subscribe\n- Keeps participant update, block/self checks, and cooldown intact 2025-08-28 08:58:12 +01:00
jack d35d3f9612 Notifications: remove background 'new chats!' nudge; geohash activity only on zero→alive; mesh 'nearby' only on zero→alive\n\n- Geohash sampling: notify only when previous participant count in last 5m was zero, with 30s freshness gate\n- Suppress old sampled events after (re)subscribe\n- Remove channel inactivity nudge\n- Mesh: reset rising-edge gate immediately when peer list goes empty (strict zero→alive) 2025-08-28 08:54:55 +01:00
jack 9e79b18dcb Add geohash bookmarks: persistence, sampling integration, and UI\n\n- Add GeohashBookmarksStore with UserDefaults persistence and toggle API\n- Sample union of regional + bookmarked geohashes for activity notifications\n- LocationChannelsSheet: bookmark icons on nearby rows and a Bookmarked section\n- Header toolbar: toggle bookmark for current geohash\n- Tests: GeohashBookmarksStoreTests for normalization and persistence\n\nRationale: Bookmarked geohashes are always sampled for new activity notifications and quickly selectable from the sheet. 2025-08-28 00:44:42 +01:00
+9 -25
View File
@@ -3043,10 +3043,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
let mentionRegex = try? NSRegularExpression(pattern: mentionPattern, options: []) let mentionRegex = try? NSRegularExpression(pattern: mentionPattern, options: [])
let hashtagRegex = try? NSRegularExpression(pattern: hashtagPattern, options: []) let hashtagRegex = try? NSRegularExpression(pattern: hashtagPattern, options: [])
let nsContent = contentText as NSString let mentionMatches = mentionRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? []
let nsLen = nsContent.length let hashtagMatches = hashtagRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? []
let mentionMatches = mentionRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
let hashtagMatches = hashtagRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
// Combine and sort all matches // Combine and sort all matches
var allMatches: [(range: NSRange, type: String)] = [] var allMatches: [(range: NSRange, type: String)] = []
@@ -3063,7 +3061,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
for (matchRange, matchType) in allMatches { for (matchRange, matchType) in allMatches {
// Add text before the match // Add text before the match
if let range = Range(matchRange, in: contentText) { if let range = Range(matchRange, in: contentText) {
if lastEndIndex < range.lowerBound {
let beforeText = String(contentText[lastEndIndex..<range.lowerBound]) let beforeText = String(contentText[lastEndIndex..<range.lowerBound])
if !beforeText.isEmpty { if !beforeText.isEmpty {
var normalStyle = AttributeContainer() var normalStyle = AttributeContainer()
@@ -3071,7 +3068,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
normalStyle.foregroundColor = isDark ? Color.white : Color.black normalStyle.foregroundColor = isDark ? Color.white : Color.black
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle)) processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle))
} }
}
// Add the match with appropriate styling // Add the match with appropriate styling
let matchText = String(contentText[range]) let matchText = String(contentText[range])
@@ -3088,7 +3084,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
processedContent.append(AttributedString(matchText).mergingAttributes(matchStyle)) processedContent.append(AttributedString(matchText).mergingAttributes(matchStyle))
if lastEndIndex < range.upperBound { lastEndIndex = range.upperBound } lastEndIndex = range.upperBound
} }
} }
@@ -3165,12 +3161,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// For extremely long content, render as plain text to avoid heavy regex/layout work, // For extremely long content, render as plain text to avoid heavy regex/layout work,
// unless the content includes Cashu tokens we want to chip-render below // unless the content includes Cashu tokens we want to chip-render below
// Compute NSString-backed length for regex/nsrange correctness with multi-byte characters
let nsContent = content as NSString
let nsLen = nsContent.length
let containsCashuEarly: Bool = { let containsCashuEarly: Bool = {
let rx = Regexes.quickCashuPresence let rx = Regexes.quickCashuPresence
return rx.numberOfMatches(in: content, options: [], range: NSRange(location: 0, length: nsLen)) > 0 return rx.numberOfMatches(in: content, options: [], range: NSRange(location: 0, length: content.count)) > 0
}() }()
if (content.count > 4000 || content.hasVeryLongToken(threshold: 1024)) && !containsCashuEarly { if (content.count > 4000 || content.hasVeryLongToken(threshold: 1024)) && !containsCashuEarly {
var plainStyle = AttributeContainer() var plainStyle = AttributeContainer()
@@ -3188,6 +3181,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
let lnurlRegex = Regexes.lnurl let lnurlRegex = Regexes.lnurl
let lightningSchemeRegex = Regexes.lightningScheme let lightningSchemeRegex = Regexes.lightningScheme
let detector = Regexes.linkDetector let detector = Regexes.linkDetector
let nsLen = content.count
let hasMentionsHint = content.contains("@") let hasMentionsHint = content.contains("@")
let hasHashtagsHint = content.contains("#") let hasHashtagsHint = content.contains("#")
let hasURLHint = content.contains("://") || content.contains("www.") || content.contains("http") let hasURLHint = content.contains("://") || content.contains("www.") || content.contains("http")
@@ -3267,7 +3262,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
for (range, type) in allMatches { for (range, type) in allMatches {
// Add text before match // Add text before match
if let nsRange = Range(range, in: content) { if let nsRange = Range(range, in: content) {
if lastEnd < nsRange.lowerBound {
let beforeText = String(content[lastEnd..<nsRange.lowerBound]) let beforeText = String(content[lastEnd..<nsRange.lowerBound])
if !beforeText.isEmpty { if !beforeText.isEmpty {
var beforeStyle = AttributeContainer() var beforeStyle = AttributeContainer()
@@ -3280,7 +3274,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} }
result.append(AttributedString(beforeText).mergingAttributes(beforeStyle)) result.append(AttributedString(beforeText).mergingAttributes(beforeStyle))
} }
}
// Add styled match // Add styled match
let matchText = String(content[nsRange]) let matchText = String(content[nsRange])
@@ -3387,12 +3380,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
result.append(AttributedString(matchText).mergingAttributes(matchStyle)) result.append(AttributedString(matchText).mergingAttributes(matchStyle))
} }
} }
// Advance lastEnd safely in case of overlaps
if lastEnd < nsRange.upperBound {
lastEnd = nsRange.upperBound lastEnd = nsRange.upperBound
} }
} }
}
// Add remaining text // Add remaining text
if lastEnd < content.endIndex { if lastEnd < content.endIndex {
@@ -3488,16 +3478,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Regular expression to find @mentions // Regular expression to find @mentions
let pattern = "@([\\p{L}0-9_]+)" let pattern = "@([\\p{L}0-9_]+)"
let regex = try? NSRegularExpression(pattern: pattern, options: []) let regex = try? NSRegularExpression(pattern: pattern, options: [])
let nsContent = contentText as NSString let matches = regex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? []
let nsLen = nsContent.length
let matches = regex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
var lastEndIndex = contentText.startIndex var lastEndIndex = contentText.startIndex
for match in matches { for match in matches {
// Add text before the mention // Add text before the mention
if let range = Range(match.range(at: 0), in: contentText) { if let range = Range(match.range(at: 0), in: contentText) {
if lastEndIndex < range.lowerBound {
let beforeText = String(contentText[lastEndIndex..<range.lowerBound]) let beforeText = String(contentText[lastEndIndex..<range.lowerBound])
if !beforeText.isEmpty { if !beforeText.isEmpty {
var normalStyle = AttributeContainer() var normalStyle = AttributeContainer()
@@ -3505,7 +3492,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
normalStyle.foregroundColor = isDark ? Color.white : Color.black normalStyle.foregroundColor = isDark ? Color.white : Color.black
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle)) processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle))
} }
}
// Add the mention with highlight // Add the mention with highlight
let mentionText = String(contentText[range]) let mentionText = String(contentText[range])
@@ -3514,7 +3500,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
mentionStyle.foregroundColor = Color.orange mentionStyle.foregroundColor = Color.orange
processedContent.append(AttributedString(mentionText).mergingAttributes(mentionStyle)) processedContent.append(AttributedString(mentionText).mergingAttributes(mentionStyle))
if lastEndIndex < range.upperBound { lastEndIndex = range.upperBound } lastEndIndex = range.upperBound
} }
} }
@@ -4712,9 +4698,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Allow optional disambiguation suffix '#abcd' for duplicate nicknames // Allow optional disambiguation suffix '#abcd' for duplicate nicknames
let pattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)" let pattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)"
let regex = try? NSRegularExpression(pattern: pattern, options: []) let regex = try? NSRegularExpression(pattern: pattern, options: [])
let nsContent = content as NSString let matches = regex?.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) ?? []
let nsLen = nsContent.length
let matches = regex?.matches(in: content, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
var mentions: [String] = [] var mentions: [String] = []
let peerNicknames = meshService.getPeerNicknames() let peerNicknames = meshService.getPeerNicknames()