SIGN IN SIGN UP

fix: reconcile frozen chat keyboard padding (#1528)

## Problem

Same issue as #1493 (this PR adopts its implementation and tests —
credit to @jmeistrich, kept as co-author — and addresses the review
feedback there, since that PR looks stalled).

Keyboard handlers in `useChatKeyboard` return early while `freeze` is
true. If the keyboard closes during the frozen interval — e.g.
`@legendapp/list`'s `useKeyboardScrollToEnd` freezing across
`KeyboardController.dismiss()` + `scrollToEnd` (dismiss resolves on
`keyboardDidHide`, after the close animation) — the hook misses the
terminal keyboard height and keeps stale padding after `freeze` becomes
false. `onContentInsetChange` then stops at the previous keyboard
padding instead of continuing down to 0.

In a chat list the phantom padding hides under `max(blankSpace, padding
+ extraContentPadding)` until the anchored blank space shrinks below it
while a response streams in, then shows up as a keyboard-sized gap at
the end of the list. The next keyboard open/close cycle clears it, which
made it look intermittent.

## Solution

`useChatKeyboard` is untouched. A new self-contained `useFrozenPadding`
hook, wired at the `KeyboardChatScrollView` level alongside
`useExtraContentPadding`/`useEndVisible`, registers its own
`useKeyboardHandler` and tracks two private shared values:

- `lastHeight` — written in `onMove`/`onEnd` only, so it only ever holds
measured keyboard positions, never the `onStart` target.
- `targetKeyboardHeight` — the one-line `onStart` guard, needed for
`getEffectiveHeight` with a non-zero `offset`.

When `freeze` transitions from true to false, the hook recomputes the
effective padding from the latest observed keyboard height.

The documented bottom-sheet freeze use case is unaffected: if the
keyboard is still open at unfreeze, the recomputed effective height
equals the current padding and nothing changes. If unfreeze lands
mid-animation, the reconciled value is corrected within a frame by the
now-unfrozen `useChatKeyboard` handlers on the remaining
`onMove`/`onEnd` events.

### Review feedback addressed

- **[This PR's
review](https://github.com/kirillzyusko/react-native-keyboard-controller/pull/1528#discussion_r3519381040)**:
`currentHeight` is no longer reused — it stays purely the Fabric
commit-forcing mechanism and iOS never writes it. All `useChatKeyboard`
changes are reverted (early `freeze` returns restored on both
platforms); `useFrozenPadding` derives its own tracking values from its
own `useKeyboardHandler`.
- **[#1493 Android `onStart`
tracking](https://github.com/kirillzyusko/react-native-keyboard-controller/pull/1493#discussion_r3394715087)**:
the tracked height is never written in `onStart`, which carries the
*target* height and would interleave with per-frame `onMove` values
(336, 0, 80, 120, …).
- **[#1493 shared
hook](https://github.com/kirillzyusko/react-native-keyboard-controller/pull/1493#discussion_r3394731437)**:
iOS and Android share the single `useFrozenPadding` implementation.

## Test plan

- `yarn jest src/components/KeyboardChatScrollView --runInBand` — 194
passing, including a dedicated `useFrozenPadding` spec: keyboard close
while frozen, keyboard open while frozen with non-zero `offset`, no-op
when the keyboard didn't move, and no write on false→true transitions
- `yarn typescript`, `eslint --quiet` on the touched files — clean
- Verified manually in a chat app (iOS + Android, RN 0.85.3) via
patch-package on the previous revision of this fix: keyboard-open sends
no longer leave a keyboard-sized gap after long streamed responses. The
reconciliation math is unchanged in this revision; only where the
tracking lives moved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Jay Meistrich <github@jayz.us>
D
Daniel Suchý committed
95e2f5835e263aae88f9bd280853c11d294cfc13
Parent: fe777b4
Committed by GitHub <noreply@github.com> on 7/5/2026, 9:13:41 PM