mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 13:45:20 +00:00
Refactor/robustness (#446)
* Refactor BitChat for improved robustness and performance Major refactoring to simplify architecture and fix critical issues: Architecture Improvements: - Replace complex BluetoothMeshService with simplified SimplifiedBluetoothService - Consolidate message routing and peer management into unified services - Remove redundant caching layers and optimize performance Bug Fixes: - Fix critical BLE peer mapping corruption in mesh networks - Fix encrypted message routing failures in multi-peer scenarios - Fix app freezes and Main Thread Checker warnings - Fix BLE message delivery in dual-role connections - Fix favorite toggle UI not updating instantly - Fix Nostr offline messaging with 24-hour message filtering Features: - Add command processor for chat commands - Add autocomplete service for mentions and commands - Improve private chat management with dedicated service - Add unified peer service for consistent state management Performance: - Optimize BLE reconnection speed - Reduce excessive logging throughout codebase - Improve message deduplication efficiency - Optimize UI updates and state management * Improvements refactor robust (#441) * remove unused code * remove more * TLV for announcement * restore * restore * restore? * messages tlv too (#442) * Fix Nostr notification and read receipt issues, add TLV encoding, cleanup unused code ## Notification & Read Receipt Fixes - Fixed toolbar notification icon appearing incorrectly on app restart for already-read messages - Fixed read receipts being incorrectly deleted on startup when privateChats was empty - Fixed messages not being marked as read when opening chat for first time - Fixed senderPeerID not being updated during message consolidation - Added startup phase logic to block old messages (>30s) while allowing recent ones - Fixed unread status checking across all storage locations (ephemeral, stable Noise keys, temporary Nostr IDs) ## TLV Encoding Implementation - Implemented Type-Length-Value encoding for private message payloads - Added PrivateMessagePacket struct with TLV encode/decode methods - Enhanced message structure for better extensibility and robustness ## Code Cleanup - Removed unused PeerStateManager class and related dependencies - Removed dead protocol types (DeliveryAck, ProtocolAck/Nack, NoiseIdentityAnnouncement) - Cleaned up BitchatDelegate by removing unused methods - Removed excessive debug logging throughout ChatViewModel - Added test-only ProtocolNack helper for integration tests ## Technical Details - Messages stored under three ID types: ephemeral peer IDs, stable Noise key hexes, temporary Nostr IDs - Fixed cleanupOldReadReceipts() to skip when privateChats is empty or during startup - Updated message consolidation to properly update senderPeerID - Restored NIP-17 timestamp randomization (±15 minutes) for privacy --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
@@ -1,222 +0,0 @@
|
||||
//
|
||||
// BatteryOptimizer.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import IOKit.ps
|
||||
#endif
|
||||
|
||||
enum PowerMode {
|
||||
case performance // Max performance, battery drain OK
|
||||
case balanced // Default balanced mode
|
||||
case powerSaver // Aggressive power saving
|
||||
case ultraLowPower // Emergency mode
|
||||
|
||||
var scanDuration: TimeInterval {
|
||||
switch self {
|
||||
case .performance: return 3.0
|
||||
case .balanced: return 2.0
|
||||
case .powerSaver: return 1.0
|
||||
case .ultraLowPower: return 0.5
|
||||
}
|
||||
}
|
||||
|
||||
var scanPauseDuration: TimeInterval {
|
||||
switch self {
|
||||
case .performance: return 2.0
|
||||
case .balanced: return 3.0
|
||||
case .powerSaver: return 8.0
|
||||
case .ultraLowPower: return 20.0
|
||||
}
|
||||
}
|
||||
|
||||
var maxConnections: Int {
|
||||
switch self {
|
||||
case .performance: return 20
|
||||
case .balanced: return 10
|
||||
case .powerSaver: return 5
|
||||
case .ultraLowPower: return 2
|
||||
}
|
||||
}
|
||||
|
||||
var advertisingInterval: TimeInterval {
|
||||
// Note: iOS doesn't let us control this directly, but we can stop/start advertising
|
||||
switch self {
|
||||
case .performance: return 0.0 // Continuous
|
||||
case .balanced: return 5.0 // Advertise every 5 seconds
|
||||
case .powerSaver: return 15.0 // Advertise every 15 seconds
|
||||
case .ultraLowPower: return 30.0 // Advertise every 30 seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BatteryOptimizer {
|
||||
static let shared = BatteryOptimizer()
|
||||
|
||||
@Published var currentPowerMode: PowerMode = .balanced
|
||||
@Published var isInBackground: Bool = false
|
||||
@Published var batteryLevel: Float = 1.0
|
||||
@Published var isCharging: Bool = false
|
||||
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
|
||||
private init() {
|
||||
setupObservers()
|
||||
updateBatteryStatus()
|
||||
}
|
||||
|
||||
deinit {
|
||||
observers.forEach { NotificationCenter.default.removeObserver($0) }
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
#if os(iOS)
|
||||
// Monitor app state
|
||||
observers.append(
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.isInBackground = true
|
||||
self?.updatePowerMode()
|
||||
}
|
||||
)
|
||||
|
||||
observers.append(
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.willEnterForegroundNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.isInBackground = false
|
||||
self?.updatePowerMode()
|
||||
}
|
||||
)
|
||||
|
||||
// Monitor battery
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
|
||||
observers.append(
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.batteryLevelDidChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.updateBatteryStatus()
|
||||
}
|
||||
)
|
||||
|
||||
observers.append(
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.batteryStateDidChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.updateBatteryStatus()
|
||||
}
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func updateBatteryStatus() {
|
||||
#if os(iOS)
|
||||
batteryLevel = UIDevice.current.batteryLevel
|
||||
if batteryLevel < 0 {
|
||||
batteryLevel = 1.0 // Unknown battery level
|
||||
}
|
||||
|
||||
isCharging = UIDevice.current.batteryState == .charging ||
|
||||
UIDevice.current.batteryState == .full
|
||||
#elseif os(macOS)
|
||||
if let info = getMacOSBatteryInfo() {
|
||||
batteryLevel = info.level
|
||||
isCharging = info.isCharging
|
||||
}
|
||||
#endif
|
||||
|
||||
updatePowerMode()
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private func getMacOSBatteryInfo() -> (level: Float, isCharging: Bool)? {
|
||||
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||
|
||||
for source in sources {
|
||||
if let description = IOPSGetPowerSourceDescription(snapshot, source).takeUnretainedValue() as? [String: Any] {
|
||||
if let currentCapacity = description[kIOPSCurrentCapacityKey] as? Int,
|
||||
let maxCapacity = description[kIOPSMaxCapacityKey] as? Int {
|
||||
let level = Float(currentCapacity) / Float(maxCapacity)
|
||||
let isCharging = description[kIOPSPowerSourceStateKey] as? String == kIOPSACPowerValue
|
||||
return (level, isCharging)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
private func updatePowerMode() {
|
||||
// Determine optimal power mode based on:
|
||||
// 1. Battery level
|
||||
// 2. Charging status
|
||||
// 3. Background/foreground state
|
||||
|
||||
if isCharging {
|
||||
// When charging, use performance mode unless battery is critical
|
||||
currentPowerMode = batteryLevel < 0.1 ? .balanced : .performance
|
||||
} else if isInBackground {
|
||||
// In background, be less aggressive with power management
|
||||
// Don't force ultra-low power mode until battery is below 10%
|
||||
// Use balanced mode in background above 30% battery
|
||||
if batteryLevel < 0.1 {
|
||||
currentPowerMode = .ultraLowPower
|
||||
} else if batteryLevel < 0.3 {
|
||||
currentPowerMode = .powerSaver
|
||||
} else {
|
||||
currentPowerMode = .balanced // Use balanced mode above 30% in background
|
||||
}
|
||||
} else {
|
||||
// Foreground, not charging
|
||||
if batteryLevel < 0.1 {
|
||||
currentPowerMode = .ultraLowPower
|
||||
} else if batteryLevel < 0.3 {
|
||||
currentPowerMode = .powerSaver
|
||||
} else if batteryLevel < 0.6 {
|
||||
currentPowerMode = .balanced
|
||||
} else {
|
||||
currentPowerMode = .performance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manual power mode override
|
||||
func setPowerMode(_ mode: PowerMode) {
|
||||
currentPowerMode = mode
|
||||
}
|
||||
|
||||
// Get current scan parameters
|
||||
var scanParameters: (duration: TimeInterval, pause: TimeInterval) {
|
||||
return (currentPowerMode.scanDuration, currentPowerMode.scanPauseDuration)
|
||||
}
|
||||
|
||||
// Should we skip non-essential operations?
|
||||
var shouldSkipNonEssential: Bool {
|
||||
return currentPowerMode == .ultraLowPower ||
|
||||
(currentPowerMode == .powerSaver && isInBackground)
|
||||
}
|
||||
|
||||
// Should we reduce message frequency?
|
||||
var shouldThrottleMessages: Bool {
|
||||
return currentPowerMode == .powerSaver || currentPowerMode == .ultraLowPower
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
//
|
||||
// LRUCache.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Thread-safe LRU (Least Recently Used) cache implementation
|
||||
final class LRUCache<Key: Hashable, Value> {
|
||||
private class Node {
|
||||
var key: Key
|
||||
var value: Value
|
||||
var prev: Node?
|
||||
var next: Node?
|
||||
|
||||
init(key: Key, value: Value) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
private let maxSize: Int
|
||||
private var cache: [Key: Node] = [:]
|
||||
private var head: Node?
|
||||
private var tail: Node?
|
||||
private let queue = DispatchQueue(label: "bitchat.lrucache", attributes: .concurrent)
|
||||
|
||||
init(maxSize: Int) {
|
||||
self.maxSize = maxSize
|
||||
}
|
||||
|
||||
func set(_ key: Key, value: Value) {
|
||||
queue.sync(flags: .barrier) {
|
||||
if let node = cache[key] {
|
||||
// Update existing value and move to front
|
||||
node.value = value
|
||||
moveToFront(node)
|
||||
} else {
|
||||
// Add new node
|
||||
let newNode = Node(key: key, value: value)
|
||||
cache[key] = newNode
|
||||
addToFront(newNode)
|
||||
|
||||
// Remove oldest if over capacity
|
||||
if cache.count > maxSize {
|
||||
if let tailNode = tail {
|
||||
removeNode(tailNode)
|
||||
cache.removeValue(forKey: tailNode.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func get(_ key: Key) -> Value? {
|
||||
return queue.sync(flags: .barrier) {
|
||||
guard let node = cache[key] else { return nil }
|
||||
moveToFront(node)
|
||||
return node.value
|
||||
}
|
||||
}
|
||||
|
||||
func contains(_ key: Key) -> Bool {
|
||||
return queue.sync {
|
||||
return cache[key] != nil
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ key: Key) {
|
||||
queue.sync(flags: .barrier) {
|
||||
if let node = cache[key] {
|
||||
removeNode(node)
|
||||
cache.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
queue.sync(flags: .barrier) {
|
||||
cache.removeAll()
|
||||
head = nil
|
||||
tail = nil
|
||||
}
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return queue.sync {
|
||||
return cache.count
|
||||
}
|
||||
}
|
||||
|
||||
var keys: [Key] {
|
||||
return queue.sync {
|
||||
return Array(cache.keys)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func addToFront(_ node: Node) {
|
||||
node.next = head
|
||||
node.prev = nil
|
||||
|
||||
if let head = head {
|
||||
head.prev = node
|
||||
}
|
||||
|
||||
head = node
|
||||
|
||||
if tail == nil {
|
||||
tail = node
|
||||
}
|
||||
}
|
||||
|
||||
private func removeNode(_ node: Node) {
|
||||
if let prev = node.prev {
|
||||
prev.next = node.next
|
||||
} else {
|
||||
head = node.next
|
||||
}
|
||||
|
||||
if let next = node.next {
|
||||
next.prev = node.prev
|
||||
} else {
|
||||
tail = node.prev
|
||||
}
|
||||
|
||||
node.prev = nil
|
||||
node.next = nil
|
||||
}
|
||||
|
||||
private func moveToFront(_ node: Node) {
|
||||
guard node !== head else { return }
|
||||
removeNode(node)
|
||||
addToFront(node)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Bounded Set
|
||||
|
||||
/// Thread-safe set with maximum size using LRU eviction
|
||||
final class BoundedSet<Element: Hashable> {
|
||||
private let cache: LRUCache<Element, Bool>
|
||||
|
||||
init(maxSize: Int) {
|
||||
self.cache = LRUCache(maxSize: maxSize)
|
||||
}
|
||||
|
||||
func insert(_ element: Element) {
|
||||
cache.set(element, value: true)
|
||||
}
|
||||
|
||||
func contains(_ element: Element) -> Bool {
|
||||
return cache.get(element) != nil
|
||||
}
|
||||
|
||||
func remove(_ element: Element) {
|
||||
cache.remove(element)
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
cache.removeAll()
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return cache.count
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
//
|
||||
// OptimizedBloomFilter.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
/// Optimized Bloom filter using bit-packed storage and better hash functions
|
||||
struct OptimizedBloomFilter {
|
||||
private var bitArray: [UInt64]
|
||||
private let bitCount: Int
|
||||
private let hashCount: Int
|
||||
|
||||
// Statistics
|
||||
private(set) var insertCount: Int = 0
|
||||
|
||||
init(expectedItems: Int = 1000, falsePositiveRate: Double = 0.01) {
|
||||
// Calculate optimal bit count and hash count
|
||||
let m = Double(expectedItems) * abs(log(falsePositiveRate)) / (log(2) * log(2))
|
||||
self.bitCount = Int(max(64, m.rounded()))
|
||||
|
||||
let k = Double(bitCount) / Double(expectedItems) * log(2)
|
||||
self.hashCount = Int(max(1, min(10, k.rounded())))
|
||||
|
||||
// Initialize bit array (64 bits per UInt64)
|
||||
let arraySize = (bitCount + 63) / 64
|
||||
self.bitArray = Array(repeating: 0, count: arraySize)
|
||||
}
|
||||
|
||||
mutating func insert(_ item: String) {
|
||||
let hashes = generateHashes(item)
|
||||
|
||||
for i in 0..<hashCount {
|
||||
let bitIndex = hashes[i] % bitCount
|
||||
let arrayIndex = bitIndex / 64
|
||||
let bitOffset = bitIndex % 64
|
||||
|
||||
bitArray[arrayIndex] |= (1 << bitOffset)
|
||||
}
|
||||
|
||||
insertCount += 1
|
||||
}
|
||||
|
||||
func contains(_ item: String) -> Bool {
|
||||
let hashes = generateHashes(item)
|
||||
|
||||
for i in 0..<hashCount {
|
||||
let bitIndex = hashes[i] % bitCount
|
||||
let arrayIndex = bitIndex / 64
|
||||
let bitOffset = bitIndex % 64
|
||||
|
||||
if (bitArray[arrayIndex] & (1 << bitOffset)) == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
mutating func reset() {
|
||||
for i in 0..<bitArray.count {
|
||||
bitArray[i] = 0
|
||||
}
|
||||
insertCount = 0
|
||||
}
|
||||
|
||||
// Generate multiple hash values using double hashing technique
|
||||
private func generateHashes(_ item: String) -> [Int] {
|
||||
guard let data = item.data(using: .utf8) else {
|
||||
return Array(repeating: 0, count: hashCount)
|
||||
}
|
||||
|
||||
// Use SHA256 for high-quality hash values
|
||||
let hash = SHA256.hash(data: data)
|
||||
let hashBytes = Array(hash)
|
||||
|
||||
var hashes = [Int]()
|
||||
|
||||
// Extract multiple hash values from the SHA256 output
|
||||
for i in 0..<hashCount {
|
||||
let offset = (i * 4) % (hashBytes.count - 3)
|
||||
let value = Int(hashBytes[offset]) |
|
||||
(Int(hashBytes[offset + 1]) << 8) |
|
||||
(Int(hashBytes[offset + 2]) << 16) |
|
||||
(Int(hashBytes[offset + 3]) << 24)
|
||||
hashes.append(abs(value))
|
||||
}
|
||||
|
||||
return hashes
|
||||
}
|
||||
|
||||
// Calculate current false positive probability
|
||||
var estimatedFalsePositiveRate: Double {
|
||||
guard insertCount > 0 else { return 0 }
|
||||
|
||||
// Count set bits
|
||||
var setBits = 0
|
||||
for value in bitArray {
|
||||
setBits += value.nonzeroBitCount
|
||||
}
|
||||
|
||||
// Calculate probability: (1 - e^(-kn/m))^k
|
||||
let ratio = Double(hashCount * insertCount) / Double(bitCount)
|
||||
return pow(1 - exp(-ratio), Double(hashCount))
|
||||
}
|
||||
|
||||
// Get memory usage in bytes
|
||||
var memorySizeBytes: Int {
|
||||
return bitArray.count * 8
|
||||
}
|
||||
}
|
||||
|
||||
// Extension for adaptive Bloom filter that adjusts based on network size
|
||||
extension OptimizedBloomFilter {
|
||||
static func adaptive(for networkSize: Int) -> OptimizedBloomFilter {
|
||||
// Adjust parameters based on network size
|
||||
let expectedItems: Int
|
||||
let falsePositiveRate: Double
|
||||
|
||||
switch networkSize {
|
||||
case 0..<50:
|
||||
expectedItems = 500
|
||||
falsePositiveRate = 0.01
|
||||
case 50..<200:
|
||||
expectedItems = 2000
|
||||
falsePositiveRate = 0.02
|
||||
case 200..<500:
|
||||
expectedItems = 5000
|
||||
falsePositiveRate = 0.03
|
||||
default:
|
||||
expectedItems = 10000
|
||||
falsePositiveRate = 0.05
|
||||
}
|
||||
|
||||
return OptimizedBloomFilter(expectedItems: expectedItems, falsePositiveRate: falsePositiveRate)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user