From 078e2d4febcfa8daec51252595249e7fa92ad824 Mon Sep 17 00:00:00 2001 From: senseiGai Date: Fri, 3 Apr 2026 19:18:56 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=B5dup:=20=D0=B7=D0=B0=D1=89=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BE=D1=82=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20+=20Forward=20Picker=20UI=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Rosetta/Core/Crypto/MessageCrypto.swift | 17 +++++++++++++--- Rosetta/Core/Services/SessionManager.swift | 23 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Rosetta/Core/Crypto/MessageCrypto.swift b/Rosetta/Core/Crypto/MessageCrypto.swift index 9f5b6c3..3d7dd0f 100644 --- a/Rosetta/Core/Crypto/MessageCrypto.swift +++ b/Rosetta/Core/Crypto/MessageCrypto.swift @@ -176,10 +176,21 @@ enum MessageCrypto { var seen = Set() 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 diff --git a/Rosetta/Core/Services/SessionManager.swift b/Rosetta/Core/Services/SessionManager.swift index 32c21cc..1effb9b 100644 --- a/Rosetta/Core/Services/SessionManager.swift +++ b/Rosetta/Core/Services/SessionManager.swift @@ -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 = [] + 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(