Skip to content

Repro: composer inflates and collapses message list on iOS device unlock - #3802

Closed
amit-hinge wants to merge 1 commit into
GetStream:developfrom
amit-hinge:repro/ios-composer-inflation-on-device-unlock
Closed

amit-hinge wants to merge 1 commit into
GetStream:developfrom
amit-hinge:repro/ios-composer-inflation-on-device-unlock

Conversation

@amit-hinge

@amit-hinge amit-hinge commented Sep 14, 2026

Copy link
Copy Markdown

🎯 Goal

Not intended to be merged β€” this is a minimal reproduction to accompany the message-list flicker issue we reported in our shared support channel (follow-up for @isekovanic).

On a physical iOS device, locking the screen while the composer is focused causes the message list to collapse and snap back β€” a visible flicker. Our earlier theory (the connection.changed β†’ resyncChannel() reconnect path) was wrong; we instrumented that on device and it doesn't fire during most reproductions. The actual trigger is nested keyboard avoidance, which this PR isolates in SampleApp.

πŸ›  Implementation details

Two-line change to examples/SampleApp/src/screens/ChannelScreen.tsx: wrap <MessageComposer /> in an RN KeyboardAvoidingView with behavior="padding", nested inside Channel's own KeyboardCompatibleView (also padding mode). This mirrors the setup in our app, where the composer subtree carries its own KeyboardAvoidingView on iOS.

Mechanism:

  1. Locking the device while a text input is focused makes iOS emit a burst of keyboardWillShow/keyboardDidShow events with all endCoordinates zeroed (screenY: 0, height: 0, duration: 0) β€” typically 2–5 pairs within ~100ms.
  2. KeyboardCompatibleView is unaffected: _handleAppStateChange calls unsetKeyboardListeners() on background, so it isn't subscribed during that window.
  3. RN's KeyboardAvoidingView has no AppState handling, stays subscribed, and processes every bogus frame.
  4. Both components share the same guard, gated behind await AccessibilityInfo.prefersCrossFadeTransitions(). That setting is off by default β€” it is a sub-setting of Reduce Motion, only appearing under Settings β†’ Accessibility β†’ Motion once Reduce Motion is enabled β€” so for the vast majority of users the guard never fires and the bogus frame reaches the height math. With screenY = 0, relativeHeight collapses to frame.y + frame.height, i.e. the view's own bottom edge.
  5. That becomes paddingBottom, the view grows, onLayout reports the larger frame, and the next bogus event computes a larger padding. Compounding loop.

Measured in our app (composer height per event):

89 β†’ 217 β†’ 281 β†’ 345 β†’ 409 β†’ 473 β†’ 505 β†’ 537 β†’ 569 β†’ 601 β†’ 633 β†’ 665 β†’ 697   (+608px)

with the message list squeezed 366 β†’ 302 β†’ 238 β†’ 174 β†’ 110 β†’ 46 β†’ 0 in lockstep. The per-event increment (64px) exactly matched the KeyboardAvoidingView's own measured frame.height. The real keyboard event then snaps everything back.

Our fix (applied locally as a patch-package patch on React Native) was to make that guard unconditional:

// was: screenY === 0 && (await AccessibilityInfo.prefersCrossFadeTransitions())
if (Platform.OS === 'ios' && keyboardFrame.screenY === 0) {
  return 0;
}

Verified on device: guard fires, no composer growth, no flicker.

Possible SDK-side hardening, if you think it's worthwhile: apply the same unconditional rejection in KeyboardCompatibleView._relativeKeyboardHeight. It's currently shielded by unsetKeyboardListeners(), so this is defence-in-depth rather than a live bug β€” but a screenY of 0 is never a legitimate keyboard frame, and anyone nesting a KeyboardAvoidingView in their composer hits this. A docs note that nested padding-mode keyboard avoidance inside Channel is hazardous on iOS (not just Android) might also help.

🎨 UI Changes

No intentional UI change β€” the repro makes the message list visibly collapse and recover on unlock.

πŸ§ͺ Testing

  1. Run SampleApp on a physical iOS device β€” that is where we verified this repro. We have not tried this particular repro on the Simulator, but in our own app the same flicker only ever appeared on a physical device and never on the Simulator, so a real device may well be required.
  2. Open any channel, tap the composer so the keyboard is up.
  3. Lock the device, then unlock it.
  4. The message list collapses and snaps back.

Reproduced on iOS 26.6 with SampleApp defaults (MessageList, offline support enabled), so it is independent of MessageFlashList and of offline support. Originally found on stream-chat-expo 9.7.5 / RN 0.86.2.

β˜‘οΈ Checklist

  • I have signed the Stream CLA (required)
  • PR targets the develop branch
  • Documentation is updated
  • New code is tested in main example apps, including all possible scenarios
    • SampleApp iOS (physical device β€” reproduces)
    • SampleApp Android
    • Expo iOS and Android

Repro only β€” please don't merge.

Wraps MessageComposer in an RN KeyboardAvoidingView (behavior="padding"),
nested inside Channel's own KeyboardCompatibleView which is also in padding
mode. On a physical iOS device, locking the screen while the composer is
focused makes iOS emit a burst of keyboardWillShow/keyboardDidShow events with
all coordinates zeroed. KeyboardCompatibleView is unaffected because it
unsubscribes its keyboard listeners on AppState background, but RN's
KeyboardAvoidingView has no AppState handling and processes every bogus frame,
growing unboundedly and collapsing the message list until the real keyboard
event arrives.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants