Skip to content

Next Release - #3830

Merged
isekovanic merged 3 commits into
mainfrom
develop
Sep 25, 2026
Merged

isekovanic merged 3 commits into
mainfrom
develop

Conversation

@isekovanic

Copy link
Copy Markdown
Contributor

🎯 Goal

πŸ›  Implementation details

🎨 UI Changes

iOS
Before After
Android
Before After

πŸ§ͺ Testing

β˜‘οΈ 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 and Android
    • Expo iOS and Android

oliverlaz and others added 3 commits September 25, 2026 14:50
## 🎯 Goal

From iOS 27 every iPhone app is resizable β€” Split View, and the
fold/unfold of iPhone Duo (ships 2026-10-23). This makes the SDK and
SampleApp correct on those devices. No device detection is involved
anywhere: Duo is treated as a resizable window, not a device to branch
on.

Linear:
[RN-429](https://linear.app/stream/issue/RN-429/iphone-duo-ios-27-resizable-window-readiness)

**The two defects that matter:**

1. **Safe-area insets are asymmetric, and `top` is zero.** Measured on
device: `top: 0, bottom: 34, left: 0, right: 84`, on *both* Duo
displays. iOS 27 stacks the status bar, Dynamic Island, toolbars and tab
bars down a single vertical edge β€” an 84pt reserved column. Nothing in
the SDK or SampleApp read `insets.left` / `insets.right`, so channel-row
timestamps, the header compose button, unread badges and the
scroll-to-bottom button rendered underneath the system indicators.
2. **`inactive` was treated as `background`.** In Split View a *visible*
app sits `inactive`. `useAppStateListener` conflated the two, so
`useIsOnline` called `client.closeConnection()` and showed the offline
banner **while the user was reading the chat** (on by default via
`closeConnectionOnBackground`). Typing indicators stopped the same way,
and `KeyboardCompatibleView` had its own copy of the bug that dropped
the keyboard listeners.

## πŸ›  Implementation details

### Horizontal insets: the integrator wraps, the SDK insets only what a
wrapper cannot reach

An integrator wraps their screens in `<SafeAreaView edges={['left',
'right']}>`. `develop` has no horizontal inset handling at all, so any
integrator supporting landscape on a notched phone already does this β€”
and adding SDK padding on top would silently double-inset exactly them
on a minor upgrade. Leaving in-tree components alone is a no-op for
everyone else. A wrapper also narrows the whole subtree, so absolutely
positioned children (which ignore their container's padding) come along
for free.

The SDK insets itself only where a consumer's wrapper cannot reach:

- **React Native `Modal`s**, which render in their own native window β€”
`BottomSheetModal` (new `useHorizontalInsets`) and the channel details
`Modal`.
- **Image gallery header and footer**, hosted by `OverlayProvider` at
the app root. They use `SafeAreaView` `edges` rather than raw inset
values, so a nested instance contributes nothing when an ancestor has
already narrowed the subtree.
- `SafeAreaViewWrapper` now passes all four edges.

`useHorizontalInsets` keeps `left` and `right` separate (they differ on
iOS 27) and omits a side whose inset is 0, because a `paddingLeft: 0`
longhand would override a `padding` shorthand set by the component or a
consumer theme.

### Grid and tile sizing

A wrapper can supply layout, but not a number. New
`useWindowContentWidth` (window width minus the horizontal insets) sizes
the channel-details media grid and its skeleton, the attachment picker
grid and tiles, and the image gallery grid. It degrades safely: an
unwrapped app gets slightly small tiles, where plain window width would
overflow for anyone who wraps. Grids stay at 3 columns. `ImageGrid`
remounts on a column-count change, since `FlatList` rejects `numColumns`
changes on a mounted list.

### App state

`useAppStateListener` and `KeyboardCompatibleView` now treat only a real
`background` transition as backgrounding, each callback firing once per
transition. This also stops the connection dropping during Control
Center and the app switcher. The previous test *codified* the bug and
was rewritten.

### Smaller fixes

- **`StreamShimmerView`** reads `traitCollection.displayScale` instead
of a once-cached `UIScreen.main.scale`, and per-view
`window.windowScene.activationState` instead of the app-wide
`applicationState`.
- **`MessageItemView`** reads the window, not the screen. `screen` β‰ 
`window` in both directions: an 834pt iPad screen behind a 417pt window,
and the Duo's 466pt outer screen behind its 951pt inner window.
- **`useScreenDimensions`** is marked `@deprecated` in favour of
`useViewport` β€” same API, measured against the window. Behaviour
unchanged; removal in the next major.

### SampleApp

Every screen wraps itself in `<SafeAreaView edges={['left', 'right']}>`,
with the header inside the wrapper. The hand-rolled `insets.left/right`
padding in `ScreenHeader` and on the channel-list search bar is gone.
`BottomTabs` keeps its own horizontal padding, because it is the
navigator's tab bar and renders outside every screen. The toast is sized
as a percentage of its container instead of the window.

### Measured geometry, since the published figures are wrong

| Device / mode | window (pt) | screen (pt) | insets t/b/l/r |
| --- | --- | --- | --- |
| Duo, outer display, folded | 466 Γ— 678 @3x | 466 Γ— 678 @3x | 0 / 34 /
0 / 84 |
| Duo, inner display, unfolded, landscape | **951 Γ— 669 @3x** | **466 Γ—
678 @3x** | 0 / 34 / 0 / 84 |
| iPad Pro 11", iPhone-only app in a compatibility window | **417** Γ—
1210 @2x | **834** Γ— 1210 @2x | 32 / 20 / 0 / 0 |

The inner display is 669 Γ— 951pt, not the widely cited 626 Γ— 890: 669 Γ—
3 = 2007 and 951 Γ— 3 = 2853, matching the framebuffer exactly.

## 🎨 UI Changes

On the Duo, nothing the app draws enters the 84pt reserved column any
more, on either display. Previously the row timestamps, the header
compose button, the unread badges and the scroll-to-bottom button
(406–446pt) all sat inside it. Folded, content now ends at the 382pt
boundary (466 βˆ’ 84). Unfolded, it ends at 867pt (951 βˆ’ 84) or before.

On phones without horizontal insets in portrait there is no visible
change. In landscape, content now clears the Dynamic Island or camera
cutout on whichever side it is.

## πŸ§ͺ Testing

| Gate | Result |
| --- | --- |
| `yarn lint` | exit 0, zero warnings |
| `cd package && yarn test:typecheck` | exit 0 |
| `yarn build` | succeeds (the strict type gate) |
| `yarn test:unit` | 185 suites passed (2 skipped), 1506 tests passed
(14 skipped), 12 snapshots |

**On device.** The same 13-screen tour on every device: channel list,
channel, attachment picker, channel details, Photos & Videos, gallery,
gallery grid, Threads, Mentions, Drafts, Reminders, Pinned Messages and
Files. Each screen was captured, and the inset regions were measured
from pixels.

| Device | Orientations | Result |
| --- | --- | --- |
| iPhone Duo (iOS 27.1), outer display | portrait | reserved column
clear on every screen except the six headers below (captured before
their fix) |
| iPhone Duo, inner display (unfolded) | landscape | reserved column
clear on every screen; live unfold re-lays out without a relaunch |
| iPhone 18 Pro Max (iOS 27.0) | portrait + landscape | 62pt side strips
clear; portrait pixel-identical to the previous verified head, apart
from content |
| Pixel 10 emulator (Android 17, gesture nav) | portrait + both
landscape rotations | 54dp cutout strip clear on whichever side it is;
no double padding on the other side |
| Pixel 10 Pro Fold emulator (Android 17) | unfolded + folded, 3
orientations each | cover-screen cutout strip clear; media grid and
attachment picker re-lay out on fold and are pixel-identical after
unfolding again |

In every run the only thing inside an inset region is the gallery grid's
dimmed backdrop, which is intended.

This device pass caught one regression from the inset simplification:
six SampleApp screens (Mentions, Drafts, Reminders, Photos & Videos,
Pinned Messages, Files) rendered their header outside the new wrapper.
The Duo's compose button went under the system controls, and titles
centred on the full window. It is fixed in `fix(SampleApp): wrap screen
headers in the horizontal safe area` and re-verified on the Duo inner
display, the Pro Max and both Android emulators. The Duo outer display
has not been re-captured since the fix; it has the same root cause.

**Not verified:** the Duo inner display in portrait (the simulator pins
the unfolded scene to landscape); Android 3-button navigation, where the
nav bar sits on a side in landscape; the toast, thread detail, message
actions, polls and the keyboard; Expo.

## Known trade-offs and follow-ups

- **The media and picker grids are 3 columns.** On the Duo's inner
display and on the Pixel Fold unfolded, the middle column crosses the
fold. Apple's Duo guidance asks for an even column count, but develop
has 3 columns too, so this is not a regression.
- **Rotating the media grid keeps the pixel offset, not the row.**
Landscape's larger tiles land you several rows earlier. `develop`
behaves the same way.
- **ExpoMessaging does not launch on iOS 27.** Expo SDK 57's prebuild
template predates the scene lifecycle. This needs SDK 58 or a config
plugin, and is out of scope here.
- **Two-pane layout on the Duo's inner display.** Apple's HIG presents
this as the main adaptation for that display. For us it means
`ChannelList` + `Channel` side by side, which needs a product decision.
- **Native chrome in SampleApp.** Verified empirically: iOS relocates
real native bars into the reserved column. SampleApp forgoes that by
using `headerShown: false` and a custom JS tab bar.
- **Unrelated, spotted in passing:** the Drafts list renders a literal
`{{count}} Files` (`useMessagePreviewText.tsx:105`).

## β˜‘οΈ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] PR targets the `develop` branch
- [x] Documentation is updated (JSDoc on the new hooks, `@deprecated` on
`useScreenDimensions`)
- [ ] New code is tested in main example apps, including all possible
scenarios
- [x] SampleApp iOS β€” iPhone Duo (iOS 27.1, both displays) and iPhone 18
Pro Max (iOS 27.0), portrait and landscape
- [x] SampleApp Android β€” Pixel 10 and Pixel 10 Pro Fold emulators
(Android 17), portrait and both landscape directions, folded and
unfolded
- [ ] Expo iOS and Android β€” untouched; `ExpoMessaging` does not launch
on iOS 27 at all (see above)

---------

Co-authored-by: Ivan Sekovanikj <ivan.sekovanikj@getstream.io>
…cribe (#3827)

## 🎯 Goal

A message or draft with no text and no attachments rendered a bogus
attachment summary as its preview. It showed up in the SampleApp Drafts
list as a literal `{{count}} Files` during the iPhone Duo device pass
(RN-429). The same hook also feeds the channel-list last-message
preview, the thread list, the reply preview and the SampleApp Reminders
list.

The easiest way to hit it: in a channel, long-press a message β†’
**Reply**, type nothing, and go back. `stream-chat` saves a draft that
has only a `quoted_message_id`, so the Drafts tab shows `{{count}}
Files`.

Linear:
[RN-432](https://linear.app/stream/issue/RN-432/message-preview-shows-0-files-0-audios-for-messages-with-nothing-to)
Β· Same fix on `V10`: #3826

## πŸ›  Implementation details

`useMessagePreviewText` had no exit for "nothing to describe", so it
fell through to its attachment wording:

| `message.attachments` | before | after |
| --- | --- | --- |
| absent | literal `{{count}} Files`: `count` is undefined, so no
`_one`/`_other` key matches | `''` |
| `[]` | "0 Audios": `onlyAudio` is `0 === 0` | `''` |

It now returns an empty preview once the text check has passed and there
are no attachments. The sibling `useMessagePreviewIcon` already returns
`null` in that case, and it guards `onlyAudio` with `audios?.length &&`
for exactly this reason.

Deleted messages, polls, locations, giphys and text all return before
the new check, so they are unaffected. Mixed attachments still read "N
Files".

## 🎨 UI Changes

The rows that showed `{{count}} Files` or "0 Audios" now show an empty
subtitle, the same as their icon, which was already absent. No other
visual change.

## πŸ§ͺ Testing

New
`package/src/hooks/messagePreview/__tests__/useMessagePreviewText.test.tsx`:

- **Regression:** an absent `attachments` and an empty one both return
`''`. Both failed before the fix.
- **Unchanged behaviour:** mixed attachments give "2 Files", audio-only
gives "2 Audios", and text wins over attachments. The counted cases use
`waitFor`, because translations load asynchronously in tests.

| Gate | Result |
| --- | --- |
| `yarn lint` | exit 0 |
| `cd package && yarn test:typecheck` | exit 0 |
| `yarn build` | succeeds |
| `yarn test:unit` | 186 suites, 1511 passed (14 skipped), 12 snapshots
|

Not checked in the running app. The change is covered by the hook tests.

## β˜‘οΈ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] PR targets the `develop` branch
- [x] Documentation is updated β€” not needed, no API change
- [ ] New code is tested in main example apps, including all possible
scenarios
  - [ ] SampleApp iOS and Android
  - [ ] Expo iOS and Android
## 🎯 Goal

Should resolve [this GH
issue](#3824).

Linear ticket:
https://linear.app/stream/issue/RN-430/messagecomposer-reserves-72dp-of-extra-bottom-space-on-first-mount-of

## πŸ›  Implementation details

<!-- Provide a description of the implementation -->

## 🎨 UI Changes

<!-- Add relevant screenshots -->

<details>
<summary>iOS</summary>


<table>
    <thead>
        <tr>
            <td>Before</td>
            <td>After</td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                <!--<img src="" /> -->
            </td>
            <td>
                <!--<img src="" /> -->
            </td>
        </tr>
    </tbody>
</table>
</details>


<details>
<summary>Android</summary>

<table>
    <thead>
        <tr>
            <td>Before</td>
            <td>After</td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                <!--<img src="" /> -->
            </td>
            <td>
                <!--<img src="" /> -->
            </td>
        </tr>
    </tbody>
</table>
</details>

## πŸ§ͺ Testing

<!-- Explain how this change can be tested (or why it can't be tested)
-->

## β˜‘οΈ Checklist

- [ ] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [ ] PR targets the `develop` branch
- [ ] Documentation is updated
- [ ] New code is tested in main example apps, including all possible
scenarios
  - [ ] SampleApp iOS and Android
  - [ ] Expo iOS and Android
@github-actions

Copy link
Copy Markdown

Next releases

v9.9.2

9.9.2 (2026-09-25)

Bug Fixes

  • core: show an empty message preview when there is nothing to describe (#3827) (1abca98), closes #3826
  • introduce prop to prevent attachment picker rendering (#3829) (31bfe9c)
  • iPhone Duo / iOS 27 resizable-window readiness (#3818) (c51967a)

sampleapp@v4.15.2

4.15.2 (2026-09-25)

Bug Fixes

  • iPhone Duo / iOS 27 resizable-window readiness (#3818) (c51967a)

@Stream-SDK-Bot

Copy link
Copy Markdown
Contributor

SDK Size

title develop branch diff status
js_bundle_size 2033 KB 2033 KB 0 B 🟒

@isekovanic
isekovanic merged commit 7350137 into main Sep 25, 2026
18 checks passed
@stream-ci-bot

Copy link
Copy Markdown
Contributor

πŸŽ‰ This PR is included in version 9.9.2 πŸŽ‰

The release is available on:

Your semantic-release bot πŸ“¦πŸš€

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants