From e3866c9da688f67b6bde9fc980e6cbbad5b13e62 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 24 Aug 2025 10:41:45 +0200 Subject: [PATCH] perf(chat): conditional animations via isBatchingPublic; late-arrival binary insert; flush public buffer on channel switch; prewarm formatting on flush --- bitchat/ViewModels/ChatViewModel.swift | 41 ++++++++++++++++++++++++-- bitchat/Views/ContentView.swift | 2 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 32995966..4cab57fd 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -147,6 +147,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { private let contentBucketCapacity: Double = 3 private let contentBucketRefill: Double = 0.5 // tokens per second + @MainActor private func normalizedSenderKey(for message: BitchatMessage) -> String { if let spid = message.senderPeerID { if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") { @@ -377,6 +378,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate { private var publicBuffer: [BitchatMessage] = [] private var publicBufferTimer: Timer? = nil private let publicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching + @Published private(set) var isBatchingPublic: Bool = false + private let lateInsertThreshold: TimeInterval = 15.0 // Track sent read receipts to avoid duplicates (persisted across launches) // Note: Persistence happens automatically in didSet, no lifecycle observers needed @@ -1262,6 +1265,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate { #if os(iOS) @MainActor private func switchLocationChannel(to channel: ChannelID) { + // Flush pending public buffer to avoid cross-channel bleed + publicBufferTimer?.invalidate(); publicBufferTimer = nil + publicBuffer.removeAll(keepingCapacity: false) activeChannel = channel // Reset deduplication set and optionally hydrate timeline for mesh processedNostrEvents.removeAll() @@ -5221,10 +5227,41 @@ class ChatViewModel: ObservableObject, BitchatDelegate { publicBuffer.removeAll(keepingCapacity: true) guard !added.isEmpty else { return } - // Rough chronological order: sort the batch by timestamp before appending + // Indicate batching for conditional UI animations + isBatchingPublic = true + // Rough chronological order: sort the batch by timestamp before inserting added.sort { $0.timestamp < $1.timestamp } - messages.append(contentsOf: added) + // Insert late arrivals into approximate position; append recent ones + let lastTs = messages.last?.timestamp ?? .distantPast + for m in added { + if m.timestamp < lastTs.addingTimeInterval(-lateInsertThreshold) { + let idx = insertionIndexByTimestamp(m.timestamp) + if idx >= messages.count { messages.append(m) } else { messages.insert(m, at: idx) } + } else { + messages.append(m) + } + } trimMessagesIfNeeded() + // Prewarm formatting for new messages (light scheme) + for m in added { + _ = self.formatMessageAsText(m, colorScheme: .light) + } + // Reset batching flag on next runloop to allow brief UI transaction window + DispatchQueue.main.async { [weak self] in self?.isBatchingPublic = false } + } + + private func insertionIndexByTimestamp(_ ts: Date) -> Int { + var low = 0 + var high = messages.count + while low < high { + let mid = (low + high) / 2 + if messages[mid].timestamp < ts { + low = mid + 1 + } else { + high = mid + } + } + return low } /// Check for mentions and send notifications diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 5bb0b324..18204383 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -451,7 +451,7 @@ struct ContentView: View { .padding(.vertical, 2) } } - .transaction { $0.disablesAnimations = true } + .transaction { tx in if viewModel.isBatchingPublic { tx.disablesAnimations = true } } .padding(.vertical, 4) } .background(backgroundColor)