mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 05:05:19 +00:00
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so platform-specific code is never touched), with test targets indexed and the share extension built. 277 dead declarations removed or demoted: dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed- feature remnants (autocomplete command suggestions, back-swipe tuning, MediaSendError, GeohashParticipantTracker), unused Tor dormancy bindings, assign-only properties, unused parameters (renamed to _), and redundant public accessibility. 13 orphaned localization keys deleted across all 29 locales (old pre-#1392 location-notes UI, app_info warnings). Two real tests were flagged as unused because they never ran: Swift Testing methods missing @Test (NostrProtocolTests. testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests. testAssemblesCompressedLargeFrame). Re-armed both; they pass. Deliberately kept, now recorded in .periphery.baseline.json: iOS-only code invisible to the CI macOS scan, C FFI signatures, keep-alive NWPathMonitor reference, InboundEventKey.eventID (dedup semantics), wifiBulk capability bit (reserved for Wi-Fi bulk work, used by BitFoundation package tests), and the String secureClear cluster (exercised by package tests). New: .periphery.yml config and an advisory Dead Code CI job (mirrors the SwiftLint precedent from #1361) that fails on findings not in the committed baseline. Verified: full macOS app suite, BitFoundation (119) and BitLogger (13) package tests green; periphery scan --strict exits clean. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
163 lines
5.0 KiB
Swift
163 lines
5.0 KiB
Swift
//
|
|
// GeohashParticipantTracker.swift
|
|
// bitchat
|
|
//
|
|
// Tracks participants in geohash-based location channels.
|
|
// This is free and unencumbered software released into the public domain.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Represents a participant in a geohash channel
|
|
struct GeoPerson: Identifiable, Equatable, Sendable {
|
|
let id: String // pubkey hex (lowercased)
|
|
let displayName: String
|
|
let lastSeen: Date
|
|
|
|
init(id: String, displayName: String, lastSeen: Date) {
|
|
self.id = id
|
|
self.displayName = displayName
|
|
self.lastSeen = lastSeen
|
|
}
|
|
}
|
|
|
|
/// Protocol for resolving display names and checking block status
|
|
@MainActor
|
|
protocol GeohashParticipantContext: AnyObject {
|
|
/// Returns display name for a Nostr pubkey (e.g., "alice#a1b2" or "anon#c3d4")
|
|
func displayNameForPubkey(_ pubkeyHex: String) -> String
|
|
/// Returns true if the pubkey is blocked
|
|
func isBlocked(_ pubkeyHexLowercased: String) -> Bool
|
|
}
|
|
|
|
/// Tracks participants across multiple geohash channels
|
|
@MainActor
|
|
final class GeohashParticipantTracker: ObservableObject {
|
|
|
|
/// Activity cutoff duration (defaults to 5 minutes)
|
|
let activityCutoff: TimeInterval
|
|
|
|
/// Per-geohash participant map: [geohash: [pubkeyHex: lastSeen]]
|
|
private var participants: [String: [String: Date]] = [:]
|
|
|
|
/// Currently visible people for the active geohash
|
|
@Published private(set) var visiblePeople: [GeoPerson] = []
|
|
|
|
/// The currently active geohash (if any)
|
|
private var activeGeohash: String?
|
|
|
|
/// Context for display name resolution and block checking
|
|
private weak var context: GeohashParticipantContext?
|
|
|
|
/// Timer for periodic refresh
|
|
private var refreshTimer: Timer?
|
|
|
|
init(activityCutoff: TimeInterval = -300) { // default 5 minutes
|
|
self.activityCutoff = activityCutoff
|
|
}
|
|
|
|
/// Configure with a context provider
|
|
func configure(context: GeohashParticipantContext) {
|
|
self.context = context
|
|
}
|
|
|
|
/// Set the currently active geohash
|
|
func setActiveGeohash(_ geohash: String?) {
|
|
activeGeohash = geohash
|
|
if geohash == nil {
|
|
visiblePeople = []
|
|
} else {
|
|
refresh()
|
|
}
|
|
}
|
|
|
|
/// Record activity from a participant in the current active geohash
|
|
func recordParticipant(pubkeyHex: String) {
|
|
guard let gh = activeGeohash else { return }
|
|
recordParticipant(pubkeyHex: pubkeyHex, geohash: gh)
|
|
}
|
|
|
|
/// Record activity from a participant in a specific geohash
|
|
func recordParticipant(pubkeyHex: String, geohash: String) {
|
|
let key = pubkeyHex.lowercased()
|
|
var map = participants[geohash] ?? [:]
|
|
map[key] = Date()
|
|
participants[geohash] = map
|
|
|
|
// Always notify observers that state has changed so counts in UI update
|
|
objectWillChange.send()
|
|
|
|
// Only refresh visible list if this geohash is currently active
|
|
if activeGeohash == geohash {
|
|
refresh()
|
|
}
|
|
}
|
|
|
|
/// Remove a participant from all geohashes (used when blocking)
|
|
func removeParticipant(pubkeyHex: String) {
|
|
let key = pubkeyHex.lowercased()
|
|
for (gh, var map) in participants {
|
|
map.removeValue(forKey: key)
|
|
participants[gh] = map
|
|
}
|
|
refresh()
|
|
}
|
|
|
|
/// Get participant count for a specific geohash
|
|
func participantCount(for geohash: String) -> Int {
|
|
let cutoff = Date().addingTimeInterval(activityCutoff)
|
|
let map = participants[geohash] ?? [:]
|
|
return map.values.filter { $0 >= cutoff }.count
|
|
}
|
|
|
|
/// Get the visible people list for the active geohash (read-only query)
|
|
func getVisiblePeople() -> [GeoPerson] {
|
|
guard let gh = activeGeohash, let context = context else { return [] }
|
|
let cutoff = Date().addingTimeInterval(activityCutoff)
|
|
let map = (participants[gh] ?? [:])
|
|
.filter { $0.value >= cutoff }
|
|
.filter { !context.isBlocked($0.key) }
|
|
|
|
return map
|
|
.map { (pub, seen) in
|
|
GeoPerson(id: pub, displayName: context.displayNameForPubkey(pub), lastSeen: seen)
|
|
}
|
|
.sorted { $0.lastSeen > $1.lastSeen }
|
|
}
|
|
|
|
/// Refresh the visible people list
|
|
func refresh() {
|
|
visiblePeople = getVisiblePeople()
|
|
}
|
|
|
|
/// Start the periodic refresh timer
|
|
func startRefreshTimer(interval: TimeInterval = 30.0) {
|
|
stopRefreshTimer()
|
|
refreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
|
Task { @MainActor in
|
|
self?.refresh()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stop the periodic refresh timer
|
|
func stopRefreshTimer() {
|
|
refreshTimer?.invalidate()
|
|
refreshTimer = nil
|
|
}
|
|
|
|
/// Clear all participant data
|
|
func clear() {
|
|
participants.removeAll()
|
|
visiblePeople = []
|
|
}
|
|
|
|
/// Clear participant data for a specific geohash
|
|
func clear(geohash: String) {
|
|
participants.removeValue(forKey: geohash)
|
|
if activeGeohash == geohash {
|
|
visiblePeople = []
|
|
}
|
|
}
|
|
}
|