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:
jack
2025-08-17 01:51:54 +02:00
committed by GitHub
co-authored by jack callebtc
parent 47b0829685
commit 845ffc601b
38 changed files with 6423 additions and 12246 deletions
-222
View File
@@ -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
}
}
-171
View File
@@ -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
}
}
-141
View File
@@ -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)
}
}