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
+104
View File
@@ -0,0 +1,104 @@
//
// AutocompleteService.swift
// bitchat
//
// Handles autocomplete suggestions for mentions and commands
// This is free and unencumbered software released into the public domain.
//
import Foundation
/// Manages autocomplete functionality for chat
class AutocompleteService {
private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: [])
private let commandRegex = try? NSRegularExpression(pattern: "^/([a-z]*)$", options: [])
private let commands = [
"/msg", "/who", "/clear", "/help",
"/hug", "/slap", "/fav", "/unfav",
"/block", "/unblock"
]
/// Get autocomplete suggestions for current text
func getSuggestions(for text: String, peers: [String], cursorPosition: Int) -> (suggestions: [String], range: NSRange?) {
let textToPosition = String(text.prefix(cursorPosition))
// Check for mention autocomplete
if let (mentionSuggestions, mentionRange) = getMentionSuggestions(textToPosition, peers: peers) {
return (mentionSuggestions, mentionRange)
}
// Don't handle command autocomplete here - ContentView handles it with better UI
// if let (commandSuggestions, commandRange) = getCommandSuggestions(textToPosition) {
// return (commandSuggestions, commandRange)
// }
return ([], nil)
}
/// Apply selected suggestion to text
func applySuggestion(_ suggestion: String, to text: String, range: NSRange) -> String {
guard let textRange = Range(range, in: text) else { return text }
var replacement = suggestion
// Add space after command if it takes arguments
if suggestion.hasPrefix("/") && needsArgument(command: suggestion) {
replacement += " "
}
return text.replacingCharacters(in: textRange, with: replacement)
}
// MARK: - Private Methods
private func getMentionSuggestions(_ text: String, peers: [String]) -> ([String], NSRange)? {
guard let regex = mentionRegex else { return nil }
let nsText = text as NSString
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length))
guard let match = matches.last else { return nil }
let fullRange = match.range(at: 0)
let captureRange = match.range(at: 1)
let prefix = nsText.substring(with: captureRange).lowercased()
let suggestions = peers
.filter { $0.lowercased().hasPrefix(prefix) }
.sorted()
.prefix(5)
.map { "@\($0)" }
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
}
private func getCommandSuggestions(_ text: String) -> ([String], NSRange)? {
guard let regex = commandRegex else { return nil }
let nsText = text as NSString
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length))
guard let match = matches.last else { return nil }
let fullRange = match.range(at: 0)
let captureRange = match.range(at: 1)
let prefix = nsText.substring(with: captureRange).lowercased()
let suggestions = commands
.filter { $0.hasPrefix("/\(prefix)") }
.sorted()
.prefix(5)
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
}
private func needsArgument(command: String) -> Bool {
switch command {
case "/who", "/clear", "/help":
return false
default:
return true
}
}
}