Remove message batching system for simpler, faster UI updates (#377)

- Remove dual-timer batching architecture (100ms message timer, 50ms UI timer)
- Delete pending message queues and all batching infrastructure
- Simplify to direct message appends with duplicate detection
- Trust SwiftUI's built-in update optimization instead of manual batching
- Results in instant message display and 208 lines of code removed
- Eliminates timer management complexity and potential race conditions

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-01 11:38:53 +02:00
committed by GitHub
co-authored by jack
parent a6c2e751d2
commit b004bfa4aa
+54 -210
View File
@@ -31,7 +31,7 @@
/// ## Key Features
///
/// ### Message Management
/// - Batches incoming messages for performance (100ms window)
/// - Efficient message handling with duplicate detection
/// - Maintains separate public and private message queues
/// - Limits message history to prevent memory issues (1337 messages)
/// - Tracks delivery and read receipts
@@ -58,7 +58,7 @@
/// - `/help`: Show available commands
///
/// ## Performance Optimizations
/// - Message batching reduces UI updates
/// - SwiftUI automatically optimizes UI updates
/// - Caches expensive computations (encryption status)
/// - Debounces autocomplete suggestions
/// - Efficient peer list management
@@ -98,18 +98,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
@Published var allPeers: [BitchatPeer] = [] // Unified peer list including favorites
private var peerIndex: [String: BitchatPeer] = [:] // Quick lookup by peer ID
// MARK: - Message Batching Properties
// Message batching for performance
private var pendingMessages: [BitchatMessage] = []
private var pendingPrivateMessages: [String: [BitchatMessage]] = [:] // peerID -> messages
private var messageBatchTimer: Timer?
private let messageBatchInterval: TimeInterval = 0.1 // 100ms batching window
// UI update debouncing for performance
private var uiUpdateTimer: Timer?
private let uiUpdateInterval: TimeInterval = 0.05 // 50ms debounce window
private var pendingUIUpdate = false
// MARK: - Properties
@Published var nickname: String = "" {
didSet {
// Trim whitespace whenever nickname is set
@@ -234,7 +223,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
self?.peerIndex = Dictionary(uniqueKeysWithValues: peers.map { ($0.id, $0) })
// Schedule UI update if peers changed
if peers.count > 0 || self?.allPeers.count ?? 0 > 0 {
self?.scheduleUIUpdate()
// UI will update automatically
}
// Update private chat peer ID if needed when peers change
@@ -362,10 +351,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// MARK: - Deinitialization
deinit {
// Clean up timers
messageBatchTimer?.invalidate()
uiUpdateTimer?.invalidate()
// Force immediate save
userDefaults.synchronize()
}
@@ -642,7 +627,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
selectedPrivateChatPeer = currentPeerID
// Schedule UI update for encryption status change
scheduleUIUpdate()
// UI will update automatically
// Also refresh the peer list to update encryption status
Task { @MainActor in
@@ -651,7 +636,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
} else if selectedPrivateChatPeer == nil {
// Just set the peer ID if we don't have one
selectedPrivateChatPeer = currentPeerID
scheduleUIUpdate()
// UI will update automatically
}
// Clear unread messages for the current peer ID
@@ -816,7 +801,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
timestamp: Date(),
isRelay: false
)
addMessageToBatch(systemMessage)
addMessage(systemMessage)
}
}
@@ -1028,14 +1013,14 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
messages[index].deliveryStatus = DeliveryStatus.delivered(to: "nostr", at: Date())
// Schedule UI update for delivery status
scheduleUIUpdate()
// UI will update automatically
}
// Also update in private chats if it's a private message
for (peerID, chatMessages) in privateChats {
if let index = chatMessages.firstIndex(where: { $0.id == messageId }) {
privateChats[peerID]?[index].deliveryStatus = DeliveryStatus.delivered(to: "nostr", at: Date())
scheduleUIUpdate()
// UI will update automatically
break
}
}
@@ -1095,7 +1080,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
}
// Schedule UI refresh
scheduleUIUpdate()
// UI will update automatically
} else {
// Even if the chat isn't open, migrate any existing private chat data
if let messages = privateChats[oldPeerID] {
@@ -1152,7 +1137,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
)
// Add to message stream
addMessageToBatch(systemMessage)
addMessage(systemMessage)
// Update peer manager to refresh UI
peerManager?.updatePeers()
@@ -1223,24 +1208,16 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
timestamp: Date(),
isRelay: false
)
// System messages can be batched
addMessageToBatch(localNotification)
// Add system message
addMessage(localNotification)
}
}
@objc private func appWillResignActive() {
// Flush any pending messages when app goes to background
flushMessageBatchImmediately()
// Flush any pending UI updates
flushUIUpdateImmediately()
userDefaults.synchronize()
}
@objc func applicationWillTerminate() {
// Flush any pending messages immediately
flushMessageBatchImmediately()
// Force save any pending identity changes (verifications, favorites, etc)
SecureIdentityStateManager.shared.forceSave()
@@ -1255,11 +1232,6 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
}
@objc private func appWillTerminate() {
// Flush any pending messages immediately
flushMessageBatchImmediately()
// Flush any pending UI updates
flushUIUpdateImmediately()
userDefaults.synchronize()
}
@@ -1487,8 +1459,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// PANIC: Emergency data clearing for activist safety
@MainActor
func panicClearAllData() {
// Flush any pending messages immediately before clearing
flushMessageBatchImmediately()
// Messages are processed immediately - nothing to flush
// Clear all messages
messages.removeAll()
@@ -1551,7 +1522,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
userDefaults.synchronize()
// Force immediate UI update for panic mode
flushUIUpdateImmediately()
// UI updates immediately - no flushing needed
}
@@ -1619,7 +1590,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
nick.lowercased().hasPrefix(partial)
}
// Batch UI updates
// UI will update automatically
if !suggestions.isEmpty {
// Only update if suggestions changed
if autocompleteSuggestions != suggestions {
@@ -1981,8 +1952,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Invalidate cache when encryption status changes
invalidateEncryptionCache(for: peerID)
// Schedule UI update
scheduleUIUpdate()
// UI will update automatically via @Published properties
}
func getEncryptionStatus(for peerID: String) -> EncryptionStatus {
@@ -2081,7 +2051,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
}
// MARK: - Message Batching
// MARK: - Message Handling
private func trimMessagesIfNeeded() {
if messages.count > maxMessages {
@@ -2097,119 +2067,28 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
}
}
private func addMessageToBatch(_ message: BitchatMessage) {
pendingMessages.append(message)
scheduleBatchFlush()
}
// MARK: - Message Management
private func addPrivateMessageToBatch(_ message: BitchatMessage, for peerID: String) {
if pendingPrivateMessages[peerID] == nil {
pendingPrivateMessages[peerID] = []
}
pendingPrivateMessages[peerID]?.append(message)
scheduleBatchFlush()
}
private func scheduleBatchFlush() {
// Cancel existing timer
messageBatchTimer?.invalidate()
private func addMessage(_ message: BitchatMessage) {
// Check for duplicates
guard !messages.contains(where: { $0.id == message.id }) else { return }
// Schedule new flush
messageBatchTimer = Timer.scheduledTimer(withTimeInterval: messageBatchInterval, repeats: false) { [weak self] _ in
self?.flushMessageBatch()
messages.append(message)
messages.sort { $0.timestamp < $1.timestamp }
trimMessagesIfNeeded()
}
private func addPrivateMessage(_ message: BitchatMessage, for peerID: String) {
if privateChats[peerID] == nil {
privateChats[peerID] = []
}
}
private func flushMessageBatch() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Process pending public messages
if !self.pendingMessages.isEmpty {
let messagesToAdd = self.pendingMessages
self.pendingMessages.removeAll()
// Add all messages at once
self.messages.append(contentsOf: messagesToAdd)
// Sort once after batch addition
self.messages.sort { $0.timestamp < $1.timestamp }
// Trim once if needed
self.trimMessagesIfNeeded()
}
// Process pending private messages
if !self.pendingPrivateMessages.isEmpty {
let privateMessageBatches = self.pendingPrivateMessages
self.pendingPrivateMessages.removeAll()
for (peerID, messagesToAdd) in privateMessageBatches {
if self.privateChats[peerID] == nil {
self.privateChats[peerID] = []
}
// Add all messages for this peer at once
self.privateChats[peerID]?.append(contentsOf: messagesToAdd)
// Sort once after batch addition
self.privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
// Trim once if needed
self.trimPrivateChatMessagesIfNeeded(for: peerID)
}
}
// Single scheduled UI update for all changes
self.scheduleUIUpdate()
}
}
// Force immediate flush for high-priority messages
private func flushMessageBatchImmediately() {
messageBatchTimer?.invalidate()
flushMessageBatch()
}
// MARK: - UI Update Debouncing
// Schedule a debounced UI update
private func scheduleUIUpdate() {
// Ensure timer operations happen on main thread
if Thread.isMainThread {
guard !pendingUIUpdate else { return } // Already scheduled
pendingUIUpdate = true
// Cancel existing timer
uiUpdateTimer?.invalidate()
// Schedule new update
uiUpdateTimer = Timer.scheduledTimer(withTimeInterval: uiUpdateInterval, repeats: false) { [weak self] _ in
self?.flushUIUpdate()
}
} else {
DispatchQueue.main.async { [weak self] in
self?.scheduleUIUpdate()
}
}
}
// Execute the pending UI update
private func flushUIUpdate() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.pendingUIUpdate = false
self.objectWillChange.send()
}
}
// Force immediate UI update for critical user actions
private func flushUIUpdateImmediately() {
uiUpdateTimer?.invalidate()
pendingUIUpdate = false
objectWillChange.send()
// Check for duplicates
guard !(privateChats[peerID]?.contains(where: { $0.id == message.id }) ?? false) else { return }
privateChats[peerID]?.append(message)
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
trimPrivateChatMessagesIfNeeded(for: peerID)
}
// Update encryption status in appropriate places, not during view updates
@@ -2236,8 +2115,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Invalidate cache when encryption status changes
invalidateEncryptionCache(for: peerID)
// Schedule UI update
scheduleUIUpdate()
// UI will update automatically via @Published properties
}
// MARK: - Fingerprint Management
@@ -2364,7 +2242,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
self.invalidateEncryptionCache(for: peerID)
// Schedule UI update
self.scheduleUIUpdate()
// UI will update automatically
}
}
@@ -2378,7 +2256,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
self.invalidateEncryptionCache(for: peerID)
// Schedule UI update
self.scheduleUIUpdate()
// UI will update automatically
}
}
}
@@ -3039,8 +2917,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
)
}
// Use batching for private messages
addPrivateMessageToBatch(messageToStore, for: peerID)
// Add private message directly
addPrivateMessage(messageToStore, for: peerID)
// Debug logging
@@ -3052,47 +2930,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Update our private chat peer to the new ID
selectedPrivateChatPeer = peerID
// Schedule UI update when peer ID changes
scheduleUIUpdate()
// UI will update automatically
}
// Also check if we need to update the private chat peer for any reason
updatePrivateChatPeerIfNeeded()
// If we're currently viewing this chat, force immediate UI update
if selectedPrivateChatPeer == peerID {
// Immediately flush the batch for the current chat to ensure UI updates
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if let pendingMessages = self.pendingPrivateMessages[peerID], !pendingMessages.isEmpty {
// Process this peer's messages immediately
self.pendingPrivateMessages.removeValue(forKey: peerID)
if self.privateChats[peerID] == nil {
self.privateChats[peerID] = []
}
// Add messages and sort
self.privateChats[peerID]?.append(contentsOf: pendingMessages)
self.privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
// Remove duplicates
var seen = Set<String>()
self.privateChats[peerID] = self.privateChats[peerID]?.filter { msg in
if seen.contains(msg.id) {
return false
}
seen.insert(msg.id)
return true
}
// Trim if needed
self.trimPrivateChatMessagesIfNeeded(for: peerID)
// Schedule UI update
self.scheduleUIUpdate()
}
}
}
// No special handling needed - messages are added immediately
// Mark as unread if not currently viewing this chat
if selectedPrivateChatPeer != peerID {
@@ -3169,7 +3013,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
if finalMessage.sender != nickname && finalMessage.sender != "system" {
// Skip empty or whitespace-only messages
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
addMessageToBatch(finalMessage)
addMessage(finalMessage)
}
} else if finalMessage.sender != "system" {
// Our own message - check if we already have it (by ID and content)
@@ -3190,13 +3034,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// This is a message we sent from another device or it's missing locally
// Skip empty or whitespace-only messages
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
addMessageToBatch(finalMessage)
addMessage(finalMessage)
}
}
} else {
// System message - check for empty content before adding
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
addMessageToBatch(finalMessage)
addMessage(finalMessage)
}
}
}
@@ -3324,8 +3168,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
isRelay: false,
originalSender: nil
)
// Batch system messages
addMessageToBatch(systemMessage)
// Add system message
addMessage(systemMessage)
}
func didDisconnectFromPeer(_ peerID: String) {
@@ -3356,8 +3200,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
isRelay: false,
originalSender: nil
)
// Batch system messages
addMessageToBatch(systemMessage)
// Add system message
addMessage(systemMessage)
}
func didUpdatePeerList(_ peers: [String]) {
@@ -3375,13 +3219,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
}
// Schedule UI refresh to ensure offline favorites are shown
self.scheduleUIUpdate()
// UI will update automatically
// Update encryption status for all peers
self.updateEncryptionStatusForPeers()
// Schedule UI update for peer list change
self.scheduleUIUpdate()
// UI will update automatically
// Check if we need to update private chat peer after reconnection
if self.selectedPrivateChatFingerprint != nil {
@@ -3512,7 +3356,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.privateChats = updatedPrivateChats
self.scheduleUIUpdate()
// UI will update automatically
}
}