Break down / flatten beginGeohashSampling

This commit is contained in:
islam
2025-09-18 02:09:05 +01:00
parent 2bc875dee5
commit abb7489e2f
+37 -22
View File
@@ -2042,14 +2042,18 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let toAdd = desired.subtracting(current) let toAdd = desired.subtracting(current)
let toRemove = current.subtracting(desired) let toRemove = current.subtracting(desired)
//
for (subID, gh) in geoSamplingSubs where toRemove.contains(gh) { for (subID, gh) in geoSamplingSubs where toRemove.contains(gh) {
NostrRelayManager.shared.unsubscribe(id: subID) NostrRelayManager.shared.unsubscribe(id: subID)
geoSamplingSubs.removeValue(forKey: subID) geoSamplingSubs.removeValue(forKey: subID)
} }
// Subscribe new
for gh in toAdd { for gh in toAdd {
subscribe(gh)
}
}
@MainActor
private func subscribe(_ gh: String) {
let subID = "geo-sample-\(gh)" let subID = "geo-sample-\(gh)"
geoSamplingSubs[subID] = gh geoSamplingSubs[subID] = gh
let filter = NostrFilter.geohashEphemeral( let filter = NostrFilter.geohashEphemeral(
@@ -2059,40 +2063,54 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
) )
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: gh, count: 5) let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: gh, count: 5)
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
guard let self = self else { return } self?.subscribeNostrEvent(event, gh: gh)
}
}
private func subscribeNostrEvent(_ event: NostrEvent, gh: String) {
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return } guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
// Compute current participant count (5-minute window) BEFORE updating with this event // Compute current participant count (5-minute window) BEFORE updating with this event
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds) let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
let existingCount: Int = { let existingCount = geoParticipants[gh]?.values.filter { $0 >= cutoff }.count ?? 0
let map = self.geoParticipants[gh] ?? [:]
return map.values.filter { $0 >= cutoff }.count
}()
// Update participants for this specific geohash // Update participants for this specific geohash
self.recordGeoParticipant(pubkeyHex: event.pubkey, geohash: gh) recordGeoParticipant(pubkeyHex: event.pubkey, geohash: gh)
// Notify only on rising-edge: previously zero people, now someone sends a chat // Notify only on rising-edge: previously zero people, now someone sends a chat
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines) let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
guard !content.isEmpty else { return } guard !content.isEmpty else { return }
// Respect geohash blocks // Respect geohash blocks
if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased()) { return } if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased()) { return }
// Skip self identity for this geohash // Skip self identity for this geohash
if let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh), my.publicKeyHex.lowercased() == event.pubkey.lowercased() { return } if let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh), my.publicKeyHex.lowercased() == event.pubkey.lowercased() { return }
// Only trigger when there were zero participants in this geohash recently // Only trigger when there were zero participants in this geohash recently
guard existingCount == 0 else { return } guard existingCount == 0 else { return }
// Avoid notifications for old sampled events when launching or (re)subscribing // Avoid notifications for old sampled events when launching or (re)subscribing
let eventTime = Date(timeIntervalSince1970: TimeInterval(event.created_at)) let eventTime = Date(timeIntervalSince1970: TimeInterval(event.created_at))
if Date().timeIntervalSince(eventTime) > 30 { return } if Date().timeIntervalSince(eventTime) > 30 { return }
// Foreground-only notifications: app must be active, and not already viewing this geohash // Foreground-only notifications: app must be active, and not already viewing this geohash
#if os(iOS) #if os(iOS)
guard UIApplication.shared.applicationState == .active else { return } guard UIApplication.shared.applicationState == .active else { return }
if case .location(let ch) = self.activeChannel, ch.geohash == gh { return } if case .location(let ch) = activeChannel, ch.geohash == gh { return }
#elseif os(macOS) #elseif os(macOS)
guard NSApplication.shared.isActive else { return } guard NSApplication.shared.isActive else { return }
if case .location(let ch) = self.activeChannel, ch.geohash == gh { return } if case .location(let ch) = activeChannel, ch.geohash == gh { return }
#endif #endif
// Cooldown per geohash
cooldownPerGeohash(gh, content: content, event: event)
}
private func cooldownPerGeohash(_ gh: String, content: String, event: NostrEvent) {
let now = Date() let now = Date()
let last = self.lastGeoNotificationAt[gh] ?? .distantPast let last = lastGeoNotificationAt[gh] ?? .distantPast
if now.timeIntervalSince(last) < TransportConfig.uiGeoNotifyCooldownSeconds { return } if now.timeIntervalSince(last) < TransportConfig.uiGeoNotifyCooldownSeconds { return }
// Compose a short preview // Compose a short preview
let preview: String = { let preview: String = {
let maxLen = TransportConfig.uiGeoNotifySnippetMaxLen let maxLen = TransportConfig.uiGeoNotifySnippetMaxLen
@@ -2100,13 +2118,15 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let idx = content.index(content.startIndex, offsetBy: maxLen) let idx = content.index(content.startIndex, offsetBy: maxLen)
return String(content[..<idx]) + "" return String(content[..<idx]) + ""
}() }()
Task { @MainActor in Task { @MainActor in
self.lastGeoNotificationAt[gh] = now lastGeoNotificationAt[gh] = now
// Pre-populate the target geohash timeline so the triggering message appears when user opens it // Pre-populate the target geohash timeline so the triggering message appears when user opens it
var arr = self.geoTimelines[gh] ?? [] var arr = geoTimelines[gh] ?? []
let senderSuffix = String(event.pubkey.suffix(4)) let senderSuffix = String(event.pubkey.suffix(4))
let nick = self.geoNicknames[event.pubkey.lowercased()] let nick = geoNicknames[event.pubkey.lowercased()]
let senderName = (nick?.isEmpty == false ? nick! : "anon") + "#" + senderSuffix let senderName = (nick?.isEmpty == false ? nick! : "anon") + "#" + senderSuffix
// Clamp future timestamps // Clamp future timestamps
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at)) let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let ts = min(rawTs, Date()) let ts = min(rawTs, Date())
@@ -2117,22 +2137,17 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
content: content, content: content,
timestamp: ts, timestamp: ts,
isRelay: false, isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "nostr:\(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength))", senderPeerID: "nostr:\(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength))",
mentions: mentions.isEmpty ? nil : mentions mentions: mentions.isEmpty ? nil : mentions
) )
if !arr.contains(where: { $0.id == msg.id }) { if !arr.contains(where: { $0.id == msg.id }) {
arr.append(msg) arr.append(msg)
if arr.count > self.geoTimelineCap { arr = Array(arr.suffix(self.geoTimelineCap)) } if arr.count > geoTimelineCap { arr = Array(arr.suffix(geoTimelineCap)) }
self.geoTimelines[gh] = arr geoTimelines[gh] = arr
} }
NotificationService.shared.sendGeohashActivityNotification(geohash: gh, bodyPreview: preview) NotificationService.shared.sendGeohashActivityNotification(geohash: gh, bodyPreview: preview)
} }
} }
}
}
/// Stop sampling all extra geohashes. /// Stop sampling all extra geohashes.
@MainActor @MainActor