From a2de309a0f8a286ff74a5ba5a5566073171b902f Mon Sep 17 00:00:00 2001 From: senseiGai Date: Mon, 30 Mar 2026 23:08:52 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81:=20=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B2=D0=BE=D1=81?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8=D0=B0=D1=82=D1=83=D1=80?= =?UTF-8?q?=D1=8B=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=B2=D0=B0=D0=B9=D0=BF-?= =?UTF-8?q?=D0=B1=D1=8D=D0=BA=20=D0=B8=20=D1=81=D0=BA=D0=B8=D0=BF=20read?= =?UTF-8?q?=20receipt=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=BC=D0=BD=D1=8B=D1=85=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Rosetta/Core/Services/SessionManager.swift | 7 ++++ .../Chats/ChatDetail/ComposerView.swift | 9 ++++ .../Chats/ChatDetail/NativeMessageList.swift | 41 ++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Rosetta/Core/Services/SessionManager.swift b/Rosetta/Core/Services/SessionManager.swift index 5019e95..4ce335f 100644 --- a/Rosetta/Core/Services/SessionManager.swift +++ b/Rosetta/Core/Services/SessionManager.swift @@ -560,6 +560,12 @@ final class SessionManager { for item in encryptedAttachments { if item.original.type == .image, let image = UIImage(data: item.original.data) { AttachmentCache.shared.saveImage(image, forAttachmentId: item.original.id) + } else if item.original.type == .file { + AttachmentCache.shared.saveFile( + item.original.data, + forAttachmentId: item.original.id, + fileName: item.original.fileName ?? "file" + ) } } @@ -998,6 +1004,7 @@ final class SessionManager { let connState = ProtocolManager.shared.connectionState guard normalized != currentPublicKey, !normalized.isEmpty, + !SystemAccounts.isSystemAccount(normalized), let hash = privateKeyHash, connState == .authenticated else { diff --git a/Rosetta/Features/Chats/ChatDetail/ComposerView.swift b/Rosetta/Features/Chats/ChatDetail/ComposerView.swift index 38d5824..2ab107a 100644 --- a/Rosetta/Features/Chats/ChatDetail/ComposerView.swift +++ b/Rosetta/Features/Chats/ChatDetail/ComposerView.swift @@ -29,6 +29,11 @@ final class ComposerView: UIView, UITextViewDelegate { private(set) var currentHeight: CGFloat = 0 + /// When true, blocks becomeFirstResponder via textViewShouldBeginEditing. + /// Set by NativeMessageListController during swipe-back to prevent UIKit + /// from auto-restoring first responder on transition cancellation. + var isFocusBlocked = false + // MARK: - Subviews // Attach button (glass circle, 42×42) @@ -495,6 +500,10 @@ final class ComposerView: UIView, UITextViewDelegate { // MARK: - UITextViewDelegate + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + !isFocusBlocked + } + func textViewDidBeginEditing(_ textView: UITextView) { delegate?.composerFocusDidChange(self, isFocused: true) } diff --git a/Rosetta/Features/Chats/ChatDetail/NativeMessageList.swift b/Rosetta/Features/Chats/ChatDetail/NativeMessageList.swift index 9bdbbc6..8d4efe7 100644 --- a/Rosetta/Features/Chats/ChatDetail/NativeMessageList.swift +++ b/Rosetta/Features/Chats/ChatDetail/NativeMessageList.swift @@ -81,6 +81,10 @@ final class NativeMessageListController: UIViewController { private var composerBottomConstraint: NSLayoutConstraint? private var composerHeightConstraint: NSLayoutConstraint? private var isKeyboardAnimating = false + private(set) var isDisappearing = false + /// Blocks programmatic re-focus after swipe-back dismisses keyboard. + /// Cleared only when isInputFocused becomes true (user tap or reply action). + fileprivate(set) var keyboardBlockedUntilUserTap = false private var currentKeyboardHeight: CGFloat = 0 // MARK: - Scroll-to-Bottom Button @@ -147,7 +151,26 @@ final class NativeMessageListController: UIViewController { ) } - // Phase 7: No viewWillAppear/viewWillDisappear — CADisplayLink removed entirely. + // Dismiss keyboard on swipe-back. Let keyboardWillChangeFrame update insets + // normally so content slides down, but skip offset compensation (isDisappearing flag). + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + isDisappearing = true + keyboardBlockedUntilUserTap = true + // Block UIKit first-responder restoration at the UITextView level. + composerView?.isFocusBlocked = true + view.endEditing(true) + onComposerFocusChange?(false) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + isDisappearing = false + // UIKit's first-responder restoration has already been blocked + // by isFocusBlocked during the transition. Safe to unblock now + // for future user taps. + composerView?.isFocusBlocked = false + } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -849,8 +872,10 @@ final class NativeMessageListController: UIViewController { let delta = newInsetTop - oldInsetTop // Capture offset BEFORE animation — UIKit may auto-clamp after inset change. + // Skip compensation during swipe-back — let content slide down naturally. let oldOffset = collectionView.contentOffset.y - let shouldCompensate = abs(delta) > 0.5 + let shouldCompensate = !isDisappearing + && abs(delta) > 0.5 && !collectionView.isDragging && !collectionView.isDecelerating @@ -1204,6 +1229,18 @@ struct NativeMessageListView: UIViewControllerRepresentable { } #endif composer.setReply(senderName: replySenderName, previewText: replyPreviewText) + + // After swipe-back dismisses keyboard, block programmatic re-focus + // until user taps text field OR SwiftUI sets isInputFocused=true (reply). + if controller.keyboardBlockedUntilUserTap { + if isInputFocused { + controller.keyboardBlockedUntilUserTap = false + // Fall through to setFocused below + } else { + return // Don't touch focus while blocked + } + } + composer.setFocused(isInputFocused) }