mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 06:45:18 +00:00
Fix store-and-forward duplicate message delivery and timestamp ordering
- Add tracking to prevent sending cached messages multiple times to the same peer - Only cache messages for offline favorites, not online ones - Sort messages by timestamp when adding to ensure proper chronological order - Clean up tracking sets when peers disconnect to allow re-sending on reconnect - Cache our own messages when sending to offline favorites - Update favorite online notification text to "wanna get in there?"
This commit is contained in:
@@ -57,6 +57,8 @@ class BluetoothMeshService: NSObject {
|
||||
private let maxCachedMessages = 100 // For regular peers
|
||||
private let maxCachedMessagesForFavorites = 1000 // Much larger cache for favorites
|
||||
private var favoriteMessageQueue: [String: [StoredMessage]] = [:] // Per-favorite message queues
|
||||
private var deliveredMessages: Set<String> = [] // Track delivered message IDs to prevent duplicates
|
||||
private var cachedMessagesSentToPeer: Set<String> = [] // Track which peers have already received cached messages
|
||||
|
||||
// Battery and range optimizations
|
||||
private var scanDutyCycleTimer: Timer?
|
||||
@@ -596,6 +598,18 @@ class BluetoothMeshService: NSObject {
|
||||
|
||||
// Sending encrypted private message
|
||||
|
||||
// Check if recipient is offline and cache if they're a favorite
|
||||
if !self.activePeers.contains(recipientPeerID) {
|
||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
|
||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
||||
// Recipient is offline favorite, cache the message
|
||||
let messageID = "\(packet.timestamp)-\(self.myPeerID)"
|
||||
self.cacheMessage(packet, messageID: messageID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add random delay for timing obfuscation
|
||||
let delay = self.randomDelay()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
@@ -825,6 +839,12 @@ class BluetoothMeshService: NSObject {
|
||||
let cutoffTime = Date().addingTimeInterval(-messageCacheTimeout)
|
||||
// Only remove non-favorite messages that are older than timeout
|
||||
messageCache.removeAll { !$0.isForFavorite && $0.timestamp < cutoffTime }
|
||||
|
||||
// Clean up delivered messages set periodically (keep recent 1000 entries)
|
||||
if deliveredMessages.count > 1000 {
|
||||
// Clear older entries while keeping recent ones
|
||||
deliveredMessages.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
private func sendCachedMessages(to peerID: String) {
|
||||
@@ -835,6 +855,14 @@ class BluetoothMeshService: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we've already sent cached messages to this peer in this session
|
||||
if self.cachedMessagesSentToPeer.contains(peerID) {
|
||||
return // Already sent cached messages to this peer
|
||||
}
|
||||
|
||||
// Mark that we're sending cached messages to this peer
|
||||
self.cachedMessagesSentToPeer.insert(peerID)
|
||||
|
||||
// Clean up old messages first
|
||||
self.cleanupMessageCache()
|
||||
|
||||
@@ -842,7 +870,9 @@ class BluetoothMeshService: NSObject {
|
||||
|
||||
// First, check if this peer has any favorite messages waiting
|
||||
if let favoriteMessages = self.favoriteMessageQueue[peerID] {
|
||||
messagesToSend.append(contentsOf: favoriteMessages)
|
||||
// Filter out already delivered messages
|
||||
let undeliveredFavoriteMessages = favoriteMessages.filter { !self.deliveredMessages.contains($0.messageID) }
|
||||
messagesToSend.append(contentsOf: undeliveredFavoriteMessages)
|
||||
// Clear the favorite queue after adding to send list
|
||||
self.favoriteMessageQueue[peerID] = nil
|
||||
// Found favorite messages
|
||||
@@ -850,6 +880,10 @@ class BluetoothMeshService: NSObject {
|
||||
|
||||
// Filter regular cached messages for this specific recipient
|
||||
let recipientMessages = self.messageCache.filter { storedMessage in
|
||||
// Skip if already delivered
|
||||
if self.deliveredMessages.contains(storedMessage.messageID) {
|
||||
return false
|
||||
}
|
||||
// Check if this message is intended for this peer
|
||||
if let recipientID = storedMessage.packet.recipientID,
|
||||
let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
|
||||
@@ -859,8 +893,12 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
messagesToSend.append(contentsOf: recipientMessages)
|
||||
|
||||
// Collect message IDs that we're about to send
|
||||
// Sort messages by timestamp to ensure proper ordering
|
||||
messagesToSend.sort { $0.timestamp < $1.timestamp }
|
||||
|
||||
// Mark messages as delivered immediately to prevent duplicates
|
||||
let messageIDsToRemove = messagesToSend.map { $0.messageID }
|
||||
self.deliveredMessages.formUnion(messageIDsToRemove)
|
||||
|
||||
// Send cached messages with slight delay between each
|
||||
for (index, storedMessage) in messagesToSend.enumerated() {
|
||||
@@ -880,19 +918,21 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove sent messages after allowing time for delivery
|
||||
// Note: We remove optimistically since withoutResponse doesn't confirm delivery
|
||||
// Remove sent messages immediately
|
||||
if !messageIDsToRemove.isEmpty {
|
||||
let removalDelay = Double(messagesToSend.count) * 0.1 + 1.0 // Extra second for safety
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + removalDelay) { [weak self] in
|
||||
self?.messageQueue.async(flags: .barrier) {
|
||||
// Remove only the messages we sent to this specific peer
|
||||
self?.messageCache.removeAll { message in
|
||||
self.messageQueue.async(flags: .barrier) {
|
||||
// Remove only the messages we sent to this specific peer
|
||||
self.messageCache.removeAll { message in
|
||||
messageIDsToRemove.contains(message.messageID)
|
||||
}
|
||||
// Also remove from favorite queue if any
|
||||
if var favoriteQueue = self.favoriteMessageQueue[peerID] {
|
||||
favoriteQueue.removeAll { message in
|
||||
messageIDsToRemove.contains(message.messageID)
|
||||
}
|
||||
// Log removal
|
||||
// Removed \(messageIDsToRemove.count) delivered messages from cache
|
||||
self.favoriteMessageQueue[peerID] = favoriteQueue.isEmpty ? nil : favoriteQueue
|
||||
}
|
||||
// Removed \(messageIDsToRemove.count) delivered messages from cache
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1178,7 +1218,8 @@ class BluetoothMeshService: NSObject {
|
||||
if let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8),
|
||||
let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
|
||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
||||
// Only cache if recipient is a favorite AND is currently offline
|
||||
if (self.delegate?.isFavorite(fingerprint: fingerprint) ?? false) && !self.activePeers.contains(recipientIDString) {
|
||||
// Caching message for offline favorite
|
||||
self.cacheMessage(relayPacket, messageID: messageID)
|
||||
}
|
||||
@@ -1685,6 +1726,9 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
||||
announcedPeers.remove(peerID)
|
||||
announcedToPeers.remove(peerID)
|
||||
|
||||
// Clear cached messages tracking for this peer to allow re-sending if they reconnect
|
||||
cachedMessagesSentToPeer.remove(peerID)
|
||||
|
||||
// Only show disconnect if we have a resolved nickname
|
||||
peerNicknamesLock.lock()
|
||||
let nickname = peerNicknames[peerID]
|
||||
|
||||
@@ -76,7 +76,7 @@ class NotificationService {
|
||||
|
||||
func sendFavoriteOnlineNotification(nickname: String) {
|
||||
let title = "⭐ \(nickname) is online"
|
||||
let body = "Your favorite peer just joined the chat"
|
||||
let body = "wanna get in there?"
|
||||
let identifier = "favorite-online-\(UUID().uuidString)"
|
||||
|
||||
sendLocalNotification(title: title, body: body, identifier: identifier)
|
||||
|
||||
@@ -492,6 +492,8 @@ extension ChatViewModel: BitchatDelegate {
|
||||
privateChats[peerID] = []
|
||||
}
|
||||
privateChats[peerID]?.append(message)
|
||||
// Sort messages by timestamp to ensure proper ordering
|
||||
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
|
||||
|
||||
// Trigger UI update for private chats
|
||||
objectWillChange.send()
|
||||
@@ -510,6 +512,8 @@ extension ChatViewModel: BitchatDelegate {
|
||||
} else {
|
||||
// Regular public message
|
||||
messages.append(message)
|
||||
// Sort messages by timestamp to ensure proper ordering
|
||||
messages.sort { $0.timestamp < $1.timestamp }
|
||||
}
|
||||
|
||||
// Check if we're mentioned
|
||||
|
||||
Reference in New Issue
Block a user