Деdup: защита от дублей сообщений + Forward Picker UI parity
This commit is contained in:
@@ -176,10 +176,21 @@ enum MessageCrypto {
|
||||
var seen = Set<String>()
|
||||
return candidates.filter { seen.insert($0).inserted }
|
||||
}
|
||||
// Group key or plain password — try hex(utf8Bytes) first (Desktop parity:
|
||||
// Desktop encrypts group attachments with Buffer.from(groupKey).toString('hex')).
|
||||
// Group key or plain password — Desktop encrypts group attachments with
|
||||
// Buffer.from(groupKey).toString('hex') (hex of UTF-8 bytes).
|
||||
// If stored is already hex-encoded (128+ chars, all hex digits), use as-is
|
||||
// to avoid generating a 256-char double-hex garbage candidate.
|
||||
let isAlreadyHex = stored.count >= 128 && stored.allSatisfy { $0.isHexDigit }
|
||||
if isAlreadyHex {
|
||||
// Try hex as-is (Desktop) + un-hexed original (iOS/Android plain group key)
|
||||
let decoded = Data(hexString: stored)
|
||||
if let original = String(data: decoded, encoding: .utf8), !original.isEmpty {
|
||||
return [stored, original]
|
||||
}
|
||||
return [stored]
|
||||
}
|
||||
let hexVariant = Data(stored.utf8).map { String(format: "%02x", $0) }.joined()
|
||||
return hexVariant == stored ? [stored] : [hexVariant, stored]
|
||||
return [hexVariant, stored]
|
||||
}
|
||||
|
||||
// MARK: - Android-Compatible UTF-8 Decoder
|
||||
|
||||
@@ -1360,6 +1360,7 @@ final class SessionManager {
|
||||
self.syncRequestInFlight = false
|
||||
self.syncBatchInProgress = false
|
||||
self.pendingIncomingMessages.removeAll()
|
||||
self.enqueuedMessageIds.removeAll()
|
||||
self.isProcessingIncomingMessages = false
|
||||
|
||||
// Cancel stale retry timers from previous connection —
|
||||
@@ -1566,7 +1567,18 @@ final class SessionManager {
|
||||
DialogRepository.shared.updateDialogFromMessages(opponentKey: safeKey)
|
||||
}
|
||||
|
||||
/// Dedup set for the current processing queue — prevents the same messageId
|
||||
/// from being enqueued twice (real-time + sync overlap). Cleared after queue drains.
|
||||
private var enqueuedMessageIds: Set<String> = []
|
||||
|
||||
private func enqueueIncomingMessage(_ packet: PacketMessage) {
|
||||
// Queue-level dedup: if the same messageId is already pending, skip.
|
||||
// This catches the common real-time + sync overlap case without
|
||||
// burning CPU on duplicate crypto + DB upsert.
|
||||
let msgId = packet.messageId
|
||||
if !msgId.isEmpty && enqueuedMessageIds.contains(msgId) { return }
|
||||
if !msgId.isEmpty { enqueuedMessageIds.insert(msgId) }
|
||||
|
||||
pendingIncomingMessages.append(packet)
|
||||
guard !isProcessingIncomingMessages else { return }
|
||||
isProcessingIncomingMessages = true
|
||||
@@ -1595,6 +1607,7 @@ final class SessionManager {
|
||||
|
||||
if batching { MessageRepository.shared.endBatchUpdates() }
|
||||
isProcessingIncomingMessages = false
|
||||
enqueuedMessageIds.removeAll()
|
||||
signalQueueDrained()
|
||||
}
|
||||
|
||||
@@ -1612,6 +1625,16 @@ final class SessionManager {
|
||||
let opponentKey = context.dialogKey
|
||||
let isGroupDialog = context.kind == .group
|
||||
let wasKnownBefore = MessageRepository.shared.hasMessage(packet.messageId)
|
||||
|
||||
// Optimization: skip expensive crypto + upsert for incoming messages
|
||||
// already stored in DB. Only outgoing messages need re-processing
|
||||
// (sync may update delivery status from .waiting → .delivered).
|
||||
// This also eliminates any race-condition window between real-time
|
||||
// and sync delivery of the same messageId.
|
||||
if wasKnownBefore && !fromMe {
|
||||
return
|
||||
}
|
||||
|
||||
let groupKey: String? = {
|
||||
guard isGroupDialog, let currentPrivateKeyHex else { return nil }
|
||||
return GroupRepository.shared.groupKey(
|
||||
|
||||
Reference in New Issue
Block a user