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) }