mattermost-mobile-upstream/patches/react-native-keyboard-controller+1.21.10.patch
yasser khan 3e3b763619
Fix iOS 26 stuck keyboard and channel intro spinner (#10256)
* fix(ios): resign KeyboardGestureArea input before its screen leaves the window

react-native-keyboard-controller postpones resignFirstResponder by one frame for
KeyboardGestureArea inputs. On iOS 26 UIKit asks the input to resign while it removes a
popped screen; a frame later the input is off the window, the resign fails and the keyboard
stays open. Backport of upstream kirillzyusko/react-native-keyboard-controller#1678.

* fix(channel): re-read loading state after the Intro subscribes

The Intro reads the post-list loading flag at render but subscribes to its
change event only in an effect. A load that finishes in between is missed,
and the Intro stays on its spinner instead of showing the channel intro.

* fix(e2e): scroll the iOS sidebar's outer list to reach the last channel

On iOS the categories list grows to its full height inside the sidebar's outer list, so
only the outer list scrolls. Give it a testID and scroll it from the page object.

* fix(e2e): clear the search box before each hashtag search
2026-10-19 16:55:01 +05:30

86 lines
4.3 KiB
Diff

diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt
index 8bffcae..cdb9a3a 100644
--- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt
+++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt
@@ -54,7 +54,7 @@ class KeyboardControllerModuleImpl(
view.clearFocus()
}
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !animated) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !animated && !controller.isInsetAnimationInProgress()) {
controller.startControlRequest(view) { insetsController ->
insetsController.finish(false)
diff --git a/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift b/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift
index 32ae6de..ace9817 100644
--- a/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift
+++ b/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift
@@ -9,6 +9,7 @@ import Foundation
import UIKit
private var pendingBecomeResponder: TextInput?
+private weak var pendingResignResponder: UIResponder?
private var originalResignFirstResponder: IMP?
private var originalBecomeFirstResponder: IMP?
@@ -69,9 +70,13 @@ extension UIResponder {
}
KeyboardAreaExtender.shared.hide()
+ pendingResignResponder = self
// Postpone execution of the original resignFirstResponder
DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) {
+ if pendingResignResponder === self {
+ pendingResignResponder = nil
+ }
NotificationCenter.default.post(
name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": false]
)
@@ -86,6 +91,45 @@ extension UIResponder {
let implementation = imp_implementationWithBlock(swizzledResignImplementation)
method_setImplementation(originalResignMethod, implementation)
+
+ swizzleWillMoveToWindow(originalResignSelector)
+ }
+
+ /// If an input with a postponed resign is about to leave its window (native-stack pop on iOS 26+,
+ /// unmount, etc.), then the postponed resign will fail, because input will not be attached to the
+ /// window anymore, and window will keep a stale first responder. So we resign it immediately while
+ /// input is still attached.
+ private static func swizzleWillMoveToWindow(_ resignSelector: Selector) {
+ let selector = #selector(UIView.willMove(toWindow:))
+
+ for cls in [UITextView.self, UITextField.self] as [AnyClass] {
+ guard let method = class_getInstanceMethod(cls, selector) else { continue }
+
+ // may be inherited from `UIView` - we add own method to the class in this case,
+ // so the change doesn't affect all views
+ let originalImplementation = method_getImplementation(method)
+ typealias Function = @convention(c) (AnyObject, Selector, UIWindow?) -> Void
+ let castOriginalImplementation = unsafeBitCast(originalImplementation, to: Function.self)
+
+ let swizzledImplementation: @convention(block) (UIView, UIWindow?) -> Void = { view, newWindow in
+ if newWindow !== view.window, pendingResignResponder === view {
+ pendingResignResponder = nil
+ NotificationCenter.default.post(
+ name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": false]
+ )
+ (view as? TextInput)?.inputAccessoryView = nil
+ KeyboardAreaExtender.shared.remove()
+ _ = view.callOriginalResignFirstResponder(resignSelector)
+ }
+
+ castOriginalImplementation(view, selector, newWindow)
+ }
+
+ let implementation = imp_implementationWithBlock(swizzledImplementation)
+ if !class_addMethod(cls, selector, implementation, method_getTypeEncoding(method)) {
+ method_setImplementation(method, implementation)
+ }
+ }
}
private func callOriginalResignFirstResponder(_ selector: Selector) -> Bool {