mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 01:45:18 +00:00
docs(plans): add refactor plan; chore(config): introduce TransportConfig and use in BLEService/ChatViewModel/PrivateChatManager; feat: add PeerDisplayNameResolver and apply in BLEService; chore: make TransportPeerSnapshot Equatable/Hashable; perf: simplify read-receipt persistence (no synchronize)
This commit is contained in:
@@ -22,12 +22,12 @@ final class BLEService: NSObject {
|
||||
static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
||||
|
||||
// Default per-fragment chunk size when link limits are unknown
|
||||
private let defaultFragmentSize = 469 // ~512 MTU minus protocol overhead
|
||||
private let maxMessageLength = 10_000
|
||||
private let messageTTL: UInt8 = 7
|
||||
private let defaultFragmentSize = TransportConfig.bleDefaultFragmentSize
|
||||
private let maxMessageLength = InputValidator.Limits.maxMessageLength
|
||||
private let messageTTL: UInt8 = TransportConfig.messageTTLDefault
|
||||
// Flood/battery controls
|
||||
private let maxInFlightAssemblies = 128 // cap concurrent fragment assemblies
|
||||
private let highDegreeThreshold = 6 // for adaptive TTL/probabilistic relays
|
||||
private let maxInFlightAssemblies = TransportConfig.bleMaxInFlightAssemblies // cap concurrent fragment assemblies
|
||||
private let highDegreeThreshold = TransportConfig.bleHighDegreeThreshold // for adaptive TTL/probabilistic relays
|
||||
|
||||
// MARK: - Core State (5 Essential Collections)
|
||||
|
||||
@@ -257,20 +257,15 @@ final class BLEService: NSObject {
|
||||
|
||||
func currentPeerSnapshots() -> [TransportPeerSnapshot] {
|
||||
collectionsQueue.sync {
|
||||
// Compute nickname collision counts for connected peers
|
||||
let connected = peers.values.filter { $0.isConnected }
|
||||
var counts: [String: Int] = [:]
|
||||
for p in connected { counts[p.nickname, default: 0] += 1 }
|
||||
// Include our own nickname in collision counts so remote matching ours gets suffixed
|
||||
counts[myNickname, default: 0] += 1
|
||||
return peers.values.map { info in
|
||||
var display = info.nickname
|
||||
if info.isConnected, (counts[info.nickname] ?? 0) > 1 {
|
||||
display += "#" + String(info.id.prefix(4))
|
||||
}
|
||||
return TransportPeerSnapshot(
|
||||
let snapshot = Array(peers.values)
|
||||
let resolvedNames = PeerDisplayNameResolver.resolve(
|
||||
snapshot.map { ($0.id, $0.nickname, $0.isConnected) },
|
||||
selfNickname: myNickname
|
||||
)
|
||||
return snapshot.map { info in
|
||||
TransportPeerSnapshot(
|
||||
id: info.id,
|
||||
nickname: display,
|
||||
nickname: resolvedNames[info.id] ?? info.nickname,
|
||||
isConnected: info.isConnected,
|
||||
noisePublicKey: info.noisePublicKey,
|
||||
lastSeen: info.lastSeen
|
||||
@@ -514,22 +509,9 @@ final class BLEService: NSObject {
|
||||
|
||||
func getPeerNicknames() -> [String: String] {
|
||||
return collectionsQueue.sync {
|
||||
// Only connected peers
|
||||
let connected = peers.filter { $0.value.isConnected }
|
||||
// Count collisions by nickname (include our own nickname)
|
||||
var counts: [String: Int] = [:]
|
||||
for (_, info) in connected { counts[info.nickname, default: 0] += 1 }
|
||||
counts[myNickname, default: 0] += 1
|
||||
// Build map with suffix for collisions
|
||||
var result: [String: String] = [:]
|
||||
for (id, info) in connected {
|
||||
var name = info.nickname
|
||||
if (counts[info.nickname] ?? 0) > 1 {
|
||||
name += "#" + String(id.prefix(4))
|
||||
}
|
||||
result[id] = name
|
||||
}
|
||||
return result
|
||||
let tuples = connected.map { ($0.key, $0.value.nickname, true) }
|
||||
return PeerDisplayNameResolver.resolve(tuples, selfNickname: myNickname)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class PrivateChatManager: ObservableObject {
|
||||
}
|
||||
|
||||
// Cap for messages stored per private chat
|
||||
private let privateChatCap = 1337
|
||||
private let privateChatCap = TransportConfig.privateChatCap
|
||||
|
||||
/// Start a private chat with a peer
|
||||
func startChat(with peerID: String) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import Combine
|
||||
|
||||
/// Abstract transport interface used by ChatViewModel and services.
|
||||
/// BLEService implements this protocol; a future Nostr transport can too.
|
||||
struct TransportPeerSnapshot {
|
||||
struct TransportPeerSnapshot: Equatable, Hashable {
|
||||
let id: String
|
||||
let nickname: String
|
||||
let isConnected: Bool
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
|
||||
/// Centralized knobs for transport- and UI-related limits.
|
||||
/// Keep values aligned with existing behavior when replacing magic numbers.
|
||||
enum TransportConfig {
|
||||
// BLE / Protocol
|
||||
static let bleDefaultFragmentSize: Int = 469 // ~512 MTU minus protocol overhead
|
||||
static let messageTTLDefault: UInt8 = 7 // Default TTL for mesh flooding
|
||||
static let bleMaxInFlightAssemblies: Int = 128 // Cap concurrent fragment assemblies
|
||||
static let bleHighDegreeThreshold: Int = 6 // For adaptive TTL/probabilistic relays
|
||||
|
||||
// UI / Storage Caps
|
||||
static let privateChatCap: Int = 1337
|
||||
static let meshTimelineCap: Int = 1337
|
||||
static let geoTimelineCap: Int = 1337
|
||||
static let contentLRUCap: Int = 2000
|
||||
|
||||
// Timers
|
||||
static let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes
|
||||
static let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
/// Resolves a stable display name for peers, adding a short suffix when collisions exist.
|
||||
struct PeerDisplayNameResolver {
|
||||
/// Computes display names with a `#xxxx` suffix for connected peers when nickname collisions occur.
|
||||
/// - Parameters:
|
||||
/// - peers: Array of tuples (id, nickname, isConnected).
|
||||
/// - selfNickname: The local user's current nickname, included in collision counts to suffix remotes matching it.
|
||||
/// - Returns: Map of peerID -> displayName.
|
||||
static func resolve(_ peers: [(id: String, nickname: String, isConnected: Bool)], selfNickname: String) -> [String: String] {
|
||||
// Count collisions among connected peers and include our own nickname
|
||||
var counts: [String: Int] = [:]
|
||||
for p in peers where p.isConnected {
|
||||
counts[p.nickname, default: 0] += 1
|
||||
}
|
||||
counts[selfNickname, default: 0] += 1
|
||||
|
||||
var result: [String: String] = [:]
|
||||
for p in peers {
|
||||
var name = p.nickname
|
||||
if p.isConnected, (counts[p.nickname] ?? 0) > 1 {
|
||||
name += "#" + String(p.id.prefix(4))
|
||||
}
|
||||
result[p.id] = name
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Persistent recent content map (LRU) to speed near-duplicate checks
|
||||
private var contentLRUMap: [String: Date] = [:]
|
||||
private var contentLRUOrder: [String] = []
|
||||
private let contentLRUCap = 2000
|
||||
private let contentLRUCap = TransportConfig.contentLRUCap
|
||||
private func recordContentKey(_ key: String, timestamp: Date) {
|
||||
if contentLRUMap[key] == nil { contentLRUOrder.append(key) }
|
||||
contentLRUMap[key] = timestamp
|
||||
@@ -219,13 +219,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
@Published var messages: [BitchatMessage] = []
|
||||
@Published var currentColorScheme: ColorScheme = .light
|
||||
private let maxMessages = 1337 // Maximum messages before oldest are removed
|
||||
private let maxMessages = TransportConfig.meshTimelineCap // Maximum messages before oldest are removed
|
||||
@Published var isConnected = false
|
||||
private var hasNotifiedNetworkAvailable = false
|
||||
private var recentlySeenPeers: Set<String> = []
|
||||
private var lastNetworkNotificationTime = Date.distantPast
|
||||
private var networkResetTimer: Timer? = nil
|
||||
private let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes; avoid refiring on short drops/reconnects
|
||||
private let networkResetGraceSeconds: TimeInterval = TransportConfig.networkResetGraceSeconds // avoid refiring on short drops/reconnects
|
||||
@Published var nickname: String = "" {
|
||||
didSet {
|
||||
// Trim whitespace whenever nickname is set
|
||||
@@ -386,10 +386,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Messages are naturally ephemeral - no persistent storage
|
||||
// Persist mesh public timeline across channel switches
|
||||
private var meshTimeline: [BitchatMessage] = []
|
||||
private let meshTimelineCap = 1337
|
||||
private let meshTimelineCap = TransportConfig.meshTimelineCap
|
||||
// Persist per-geohash public timelines across switches
|
||||
private var geoTimelines: [String: [BitchatMessage]] = [:] // geohash -> messages
|
||||
private let geoTimelineCap = 1337
|
||||
private let geoTimelineCap = TransportConfig.geoTimelineCap
|
||||
// Channel activity tracking for background nudges
|
||||
private var lastPublicActivityAt: [String: Date] = [:] // channelKey -> last activity time
|
||||
private var lastPublicActivityNotifyAt: [String: Date] = [:]
|
||||
@@ -428,8 +428,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Buffer incoming public messages and flush in small batches to reduce UI invalidations
|
||||
private var publicBuffer: [BitchatMessage] = []
|
||||
private var publicBufferTimer: Timer? = nil
|
||||
private let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching
|
||||
private var dynamicPublicFlushInterval: TimeInterval = 0.08
|
||||
private let basePublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval
|
||||
private var dynamicPublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval
|
||||
private var recentBatchSizes: [Int] = []
|
||||
@Published private(set) var isBatchingPublic: Bool = false
|
||||
private let lateInsertThreshold: TimeInterval = 15.0
|
||||
@@ -441,21 +441,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Only persist if there are changes
|
||||
guard oldValue != sentReadReceipts else { return }
|
||||
|
||||
// Persist to UserDefaults whenever it changes
|
||||
// Persist to UserDefaults whenever it changes (no manual synchronize/verify re-read)
|
||||
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
|
||||
UserDefaults.standard.set(data, forKey: "sentReadReceipts")
|
||||
// Force synchronization for immediate persistence (ensures data is written to disk)
|
||||
UserDefaults.standard.synchronize()
|
||||
|
||||
// Verify persistence by re-reading
|
||||
if let verifyData = UserDefaults.standard.data(forKey: "sentReadReceipts"),
|
||||
let _ = try? JSONDecoder().decode([String].self, from: verifyData) {
|
||||
// Only log errors, not successful persistence
|
||||
// Successfully persisted
|
||||
} else {
|
||||
SecureLogger.log("⚠️ Failed to verify persistence of read receipts",
|
||||
category: SecureLogger.session, level: .error)
|
||||
}
|
||||
} else {
|
||||
SecureLogger.log("❌ Failed to encode read receipts for persistence",
|
||||
category: SecureLogger.session, level: .error)
|
||||
|
||||
Reference in New Issue
Block a user