mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 20:05:22 +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")
|
static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
||||||
|
|
||||||
// Default per-fragment chunk size when link limits are unknown
|
// Default per-fragment chunk size when link limits are unknown
|
||||||
private let defaultFragmentSize = 469 // ~512 MTU minus protocol overhead
|
private let defaultFragmentSize = TransportConfig.bleDefaultFragmentSize
|
||||||
private let maxMessageLength = 10_000
|
private let maxMessageLength = InputValidator.Limits.maxMessageLength
|
||||||
private let messageTTL: UInt8 = 7
|
private let messageTTL: UInt8 = TransportConfig.messageTTLDefault
|
||||||
// Flood/battery controls
|
// Flood/battery controls
|
||||||
private let maxInFlightAssemblies = 128 // cap concurrent fragment assemblies
|
private let maxInFlightAssemblies = TransportConfig.bleMaxInFlightAssemblies // cap concurrent fragment assemblies
|
||||||
private let highDegreeThreshold = 6 // for adaptive TTL/probabilistic relays
|
private let highDegreeThreshold = TransportConfig.bleHighDegreeThreshold // for adaptive TTL/probabilistic relays
|
||||||
|
|
||||||
// MARK: - Core State (5 Essential Collections)
|
// MARK: - Core State (5 Essential Collections)
|
||||||
|
|
||||||
@@ -257,20 +257,15 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
func currentPeerSnapshots() -> [TransportPeerSnapshot] {
|
func currentPeerSnapshots() -> [TransportPeerSnapshot] {
|
||||||
collectionsQueue.sync {
|
collectionsQueue.sync {
|
||||||
// Compute nickname collision counts for connected peers
|
let snapshot = Array(peers.values)
|
||||||
let connected = peers.values.filter { $0.isConnected }
|
let resolvedNames = PeerDisplayNameResolver.resolve(
|
||||||
var counts: [String: Int] = [:]
|
snapshot.map { ($0.id, $0.nickname, $0.isConnected) },
|
||||||
for p in connected { counts[p.nickname, default: 0] += 1 }
|
selfNickname: myNickname
|
||||||
// Include our own nickname in collision counts so remote matching ours gets suffixed
|
)
|
||||||
counts[myNickname, default: 0] += 1
|
return snapshot.map { info in
|
||||||
return peers.values.map { info in
|
TransportPeerSnapshot(
|
||||||
var display = info.nickname
|
|
||||||
if info.isConnected, (counts[info.nickname] ?? 0) > 1 {
|
|
||||||
display += "#" + String(info.id.prefix(4))
|
|
||||||
}
|
|
||||||
return TransportPeerSnapshot(
|
|
||||||
id: info.id,
|
id: info.id,
|
||||||
nickname: display,
|
nickname: resolvedNames[info.id] ?? info.nickname,
|
||||||
isConnected: info.isConnected,
|
isConnected: info.isConnected,
|
||||||
noisePublicKey: info.noisePublicKey,
|
noisePublicKey: info.noisePublicKey,
|
||||||
lastSeen: info.lastSeen
|
lastSeen: info.lastSeen
|
||||||
@@ -514,22 +509,9 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
func getPeerNicknames() -> [String: String] {
|
func getPeerNicknames() -> [String: String] {
|
||||||
return collectionsQueue.sync {
|
return collectionsQueue.sync {
|
||||||
// Only connected peers
|
|
||||||
let connected = peers.filter { $0.value.isConnected }
|
let connected = peers.filter { $0.value.isConnected }
|
||||||
// Count collisions by nickname (include our own nickname)
|
let tuples = connected.map { ($0.key, $0.value.nickname, true) }
|
||||||
var counts: [String: Int] = [:]
|
return PeerDisplayNameResolver.resolve(tuples, selfNickname: myNickname)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class PrivateChatManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cap for messages stored per private chat
|
// Cap for messages stored per private chat
|
||||||
private let privateChatCap = 1337
|
private let privateChatCap = TransportConfig.privateChatCap
|
||||||
|
|
||||||
/// Start a private chat with a peer
|
/// Start a private chat with a peer
|
||||||
func startChat(with peerID: String) {
|
func startChat(with peerID: String) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Combine
|
|||||||
|
|
||||||
/// Abstract transport interface used by ChatViewModel and services.
|
/// Abstract transport interface used by ChatViewModel and services.
|
||||||
/// BLEService implements this protocol; a future Nostr transport can too.
|
/// BLEService implements this protocol; a future Nostr transport can too.
|
||||||
struct TransportPeerSnapshot {
|
struct TransportPeerSnapshot: Equatable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let nickname: String
|
let nickname: String
|
||||||
let isConnected: Bool
|
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
|
// Persistent recent content map (LRU) to speed near-duplicate checks
|
||||||
private var contentLRUMap: [String: Date] = [:]
|
private var contentLRUMap: [String: Date] = [:]
|
||||||
private var contentLRUOrder: [String] = []
|
private var contentLRUOrder: [String] = []
|
||||||
private let contentLRUCap = 2000
|
private let contentLRUCap = TransportConfig.contentLRUCap
|
||||||
private func recordContentKey(_ key: String, timestamp: Date) {
|
private func recordContentKey(_ key: String, timestamp: Date) {
|
||||||
if contentLRUMap[key] == nil { contentLRUOrder.append(key) }
|
if contentLRUMap[key] == nil { contentLRUOrder.append(key) }
|
||||||
contentLRUMap[key] = timestamp
|
contentLRUMap[key] = timestamp
|
||||||
@@ -219,13 +219,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
@Published var messages: [BitchatMessage] = []
|
@Published var messages: [BitchatMessage] = []
|
||||||
@Published var currentColorScheme: ColorScheme = .light
|
@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
|
@Published var isConnected = false
|
||||||
private var hasNotifiedNetworkAvailable = false
|
private var hasNotifiedNetworkAvailable = false
|
||||||
private var recentlySeenPeers: Set<String> = []
|
private var recentlySeenPeers: Set<String> = []
|
||||||
private var lastNetworkNotificationTime = Date.distantPast
|
private var lastNetworkNotificationTime = Date.distantPast
|
||||||
private var networkResetTimer: Timer? = nil
|
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 = "" {
|
@Published var nickname: String = "" {
|
||||||
didSet {
|
didSet {
|
||||||
// Trim whitespace whenever nickname is set
|
// Trim whitespace whenever nickname is set
|
||||||
@@ -386,10 +386,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Messages are naturally ephemeral - no persistent storage
|
// Messages are naturally ephemeral - no persistent storage
|
||||||
// Persist mesh public timeline across channel switches
|
// Persist mesh public timeline across channel switches
|
||||||
private var meshTimeline: [BitchatMessage] = []
|
private var meshTimeline: [BitchatMessage] = []
|
||||||
private let meshTimelineCap = 1337
|
private let meshTimelineCap = TransportConfig.meshTimelineCap
|
||||||
// Persist per-geohash public timelines across switches
|
// Persist per-geohash public timelines across switches
|
||||||
private var geoTimelines: [String: [BitchatMessage]] = [:] // geohash -> messages
|
private var geoTimelines: [String: [BitchatMessage]] = [:] // geohash -> messages
|
||||||
private let geoTimelineCap = 1337
|
private let geoTimelineCap = TransportConfig.geoTimelineCap
|
||||||
// Channel activity tracking for background nudges
|
// Channel activity tracking for background nudges
|
||||||
private var lastPublicActivityAt: [String: Date] = [:] // channelKey -> last activity time
|
private var lastPublicActivityAt: [String: Date] = [:] // channelKey -> last activity time
|
||||||
private var lastPublicActivityNotifyAt: [String: Date] = [:]
|
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
|
// Buffer incoming public messages and flush in small batches to reduce UI invalidations
|
||||||
private var publicBuffer: [BitchatMessage] = []
|
private var publicBuffer: [BitchatMessage] = []
|
||||||
private var publicBufferTimer: Timer? = nil
|
private var publicBufferTimer: Timer? = nil
|
||||||
private let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching
|
private let basePublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval
|
||||||
private var dynamicPublicFlushInterval: TimeInterval = 0.08
|
private var dynamicPublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval
|
||||||
private var recentBatchSizes: [Int] = []
|
private var recentBatchSizes: [Int] = []
|
||||||
@Published private(set) var isBatchingPublic: Bool = false
|
@Published private(set) var isBatchingPublic: Bool = false
|
||||||
private let lateInsertThreshold: TimeInterval = 15.0
|
private let lateInsertThreshold: TimeInterval = 15.0
|
||||||
@@ -441,21 +441,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Only persist if there are changes
|
// Only persist if there are changes
|
||||||
guard oldValue != sentReadReceipts else { return }
|
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)) {
|
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
|
||||||
UserDefaults.standard.set(data, forKey: "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 {
|
} else {
|
||||||
SecureLogger.log("❌ Failed to encode read receipts for persistence",
|
SecureLogger.log("❌ Failed to encode read receipts for persistence",
|
||||||
category: SecureLogger.session, level: .error)
|
category: SecureLogger.session, level: .error)
|
||||||
|
|||||||
Reference in New Issue
Block a user