Skip to content

Next Release - #3970

Merged
isekovanic merged 8 commits into
mainfrom
develop
Nov 10, 2026
Merged

isekovanic merged 8 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

isekovanic and others added 7 commits November 9, 2026 14:57
…3953)

Ticket: https://linear.app/stream/issue/REACT-1537

## 🎯 Goal

Make the SampleApp start new conversations the way the v10 "Starting a
conversation" UI cookbook does, so the sample exercises the path
integrators follow.

It also bumps `stream-chat` to 10.0.1, which has the fix for the
first-message error (GetStream/stream-chat-js#2007).

| Commit | Change |
| --- | --- |
|
[`39a2be3b0`](39a2be3b0)
| SampleApp new-conversation screen follows the UI cookbook |
|
[`caec3a002`](caec3a002)
| `stream-chat` bumped to 10.0.1 |

Its new-conversation screen had a custom send button that composed the
message, called `channel.watch()` and the raw `channel.sendMessage()`,
then replaced the screen with `ChannelScreen`. The message never showed
as sending, and the cookbook's flow was never run in the sample, which
is how the first-message error fixed in GetStream/stream-chat-js#2007
went unnoticed.

## 🛠 Implementation details

- `utils/startConversation.ts` holds the cookbook's two helpers.
`startLocalConversation()` gets the channel from its members and places
an empty first page while it's provisional, so the list shows its empty
state. `createOnFirstSend()` is a `sendMessageRequest` handler: for a
provisional channel it calls `Channel.watch()`, which creates the
channel, and sends on the instance `watch()` resolves with. Every other
channel sends through the default request.
- `useChatClient` registers the handler in its existing
`client.config.set()` call, where the client is created.
- `NewDirectMessagingScreen` builds its draft with
`startLocalConversation()`, uses the default `MessageComposer`, and
passes `overrideOwnCapabilities` (`sendMessage` and `sendLinks`) only
while the channel is provisional. It stays open after the first send,
and `<Channel>` moves to the created channel, or to the one that
superseded it.
- Once the first message creates a conversation started on this screen,
`NewConversationHeader` replaces the recipient picker with the channel
header. An existing conversation keeps the picker, so its recipients can
still be changed.
- `ChannelHeader` is exported from `ChannelScreen` with two optional
props: `onBack`, so the new-conversation screen still resets its user
search on back, and `inSafeArea`, since that screen already applies the
top inset. `ChannelScreen` passes neither and is unchanged.
- `NewDirectMessagingSendButton` is removed.

The other example apps don't start conversations from members
(ExpoChatGPTSample creates its channels with an id), so they need no
change.

`stream-chat` goes from `^10.0.0` to `^10.0.1` in the SDK and every
example app, as in #3951. 10.0.1 is 10.0.0 plus
GetStream/stream-chat-js#2007: the first message in a channel whose
newest page was loaded empty now starts the newest interval, so
`jumpToTheLatestMessage()` no longer fails right after the first send of
a new conversation.

## 🎨 UI Changes

The new-conversation screen keeps the conversation open after the first
message and swaps the recipient picker for the channel header. Before,
it navigated to `ChannelScreen`.

## 🧪 Testing

Tested in SampleApp on iOS (debug build, iPhone 17 Pro simulator),
starting new conversations with throwaway users. Each first send went
through the superseded-channel path from the cookbook report.

- With `stream-chat` 10.0.0-rc.34, a new direct conversation sends from
the same screen, and `stream-chat` emits
`api:message:jumpToLatest:failed`, the error the cookbook report
describes.
- With a local build of GetStream/stream-chat-js#2007 (the code 10.0.1
ships), the same flow emits no notifications and the channel the screen
moves to is at the newest messages.
- A new group of three: the empty state shows before the first message
and the attach button is hidden while the channel is provisional. After
the send, the channel header replaces the picker ("3 members, 1
online"), back returns to the channel list, and "New Chat" opens empty
again. The created channels show in the channel list, and
`ChannelScreen`'s header is unchanged.
- On 10.0.1: the full unit suite, `yarn test:typecheck`, SampleApp `tsc`
and `yarn lint` pass.

## ☑️ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] 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
…expo-av is installed (#3954)

Ticket: https://linear.app/stream/issue/REACT-1538

Stacked on #3953.

## 🎯 Goal

Stop `stream-chat-expo` from logging "install the expo-av package" in
apps that have `expo-audio` installed. Audio works in those apps, so the
log is wrong, and it points to a package Expo has deprecated.

## 🛠 Implementation details

`AudioVideo.ts` logged whenever `require('expo-av')` failed, without
checking for `expo-audio`. `Audio.ts` and `Sound.ts` use `expo-audio`
first and fall back to `expo-av` only when it's missing.

- The log is removed from `AudioVideo.ts`.
- `Sound.ts` already loads both packages, so it now logs once, only when
neither is installed, and names `expo-audio`, for playback and voice
recording.

The `expo-av` fallback stays. `stream-chat-expo` v10 supports Expo 53
and 54, where `expo-av` still works, so an app that never moved to
`expo-audio` keeps its audio without a log. The same log has been on
`develop` since v7, so this isn't a v10 regression.

## 🎨 UI Changes

None.

## 🧪 Testing

- New `Sound.test.ts`, with the optional packages mocked. With only
`expo-audio` installed there's no log, with only `expo-av` there's no
log, and with neither there's one log naming `expo-audio`. Without the
fix, the first and third cases fail.
- The expo-package typecheck, `yarn test:typecheck`, `prettier` and
`yarn lint` pass.

## ☑️ Checklist

- [x] 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
Ticket: https://linear.app/stream/issue/RN-449

## 🎯 Goal

Port the native iOS and Android e2e suites to the V10 Detox suite in
`examples/E2ETestApp`, and bring its CI up to the native SDKs: per-test
impact analysis on pull requests, a nightly run that builds the coverage
map, and Allure TestOps reporting.

## 🛠 Implementation details

**Tests**

- 19 suites, 255 tests, ported from the Android compose and iOS UIKit
suites (the union of both, with the native names and expectations).
Every test links an Allure case in project 3.
- When the SDK disagrees with the native expectation, the test keeps the
native expectation and is skipped with `it.skip` and a comment that
links a ticket. That covers 72 tests, tracked in RN-489 to RN-506 and
RN-510. With those skipped, 183 tests run.
- Robots, pages and support code grew to cover the suites: offline
toggling (Android), the media library, JWT asserts, and Android system
dialogs.

**E2E app**

- Offline support is on (`@op-engineering/op-sqlite`, `<Chat
enableOfflineSupport>`). The database is wiped at login, so every test
starts clean.
- Added a JWT test screen, plus the channel details and thread screens
the suites need.
- No `setupCommandUIMiddlewares` call: since stream-chat `10.0.0-rc.31`
(#3919), the client installs the command middlewares itself.

**SDK**

- Additive `testID`s only (channel preview, reactions, link previews,
unread indicators, swipe actions, and a few others). There is no
behaviour change. One snapshot changed because `ScrollToBottomButton`
got a new `testID`.

**Impact analysis (`e2e/README.md`, "Impact analysis")**

- `E2E_COVERAGE=1` builds add a small Babel plugin
(`e2e/coverage/babelPlugin.js`) that records which files run code. It
isn't istanbul, because worklets capture istanbul's counters and then
throw on the UI runtime.
- After each test, the app reports its coverage to a collector in the
test process, which writes one lcov file per test.
- `build_e2e_coverage_map` and `select_e2e_tests` use
`fastlane-plugin-stream_actions` (bumped from 0.4.14 to 0.4.19).
- A PR runs only the tests its diff can reach. Changes to the harness,
native code, dependencies or lanes still run everything.
- The `Check E2E Required` job now also selects the tests, and skips e2e
when none are selected.
- Android runs in batches, like the native Android SDK: `Build Android`
builds the APKs once, `Launch Allure TestOps (Android)` opens one
launch, and `Test Android` runs up to 8 batches (one per selected test
when fewer than 8 are selected), each taking a round-robin share of the
selection.
- A new nightly workflow, `e2e-test-cron.yml`, runs everything on
Android with the coverage build and uploads the `e2e-coverage-map`
artifact. It also runs every test on iOS. Until its first run on the
default branch, every PR runs all tests.

**Allure**

- The Allure lanes moved into `examples/SampleApp/fastlane/Allurefile`,
the same layout as stream-chat-swift. A new `allure_testcase` lane
creates test cases.
- Allure uses the `ALLURE_TOKEN` repository secret.

**Mock server**

- The suite runs the mock with `api_version=v2&timestamps=ns`. That mode
comes from GetStream/stream-chat-test-mock-server#98, which is now
merged into `main`.

## 🎨 UI Changes

None. The SDK changes are `testID`s only.

## 🧪 Testing

- Full Android run (debug build, emulator): 255 tests, 182 passed, 70
skipped and 3 failed. All 3 passed when rerun:
  - A mock server start timeout, which is a harness issue.
  - RN-489 (the channel actions sheet sometimes opens empty).
- RN-506 (after jumping to a quoted message, the list stays on the
jumped page when the user sends).
- Impact analysis:
- Coverage collection checked on debug and release builds (17 of 17
tests collected).
- A map built from those tests gave sensible selections from changed
files.
  - A selection file ran exactly the selected tests.
- First batched CI run (Android release build, 8 batches): batches 0 to
3 passed. Of the rest:
- The two long-thread jump tests hung in release builds only: the open
thread keeps enqueuing JS timers, so the app never becomes idle.
Reproduced locally, ticketed as RN-510, and skipped.
- The multiple-votes poll test pressed Back to hide a keyboard the CI
emulator never showed, which closed the sheet. It now presses Back only
when the keyboard is up.
  - One batch lost its emulator mid-run (runner issue, no test failure).
- `yarn lint`, E2ETestApp `typecheck` and the SDK unit tests pass.
- iOS isn't tuned yet. On PRs its e2e job runs on dispatch only; the
nightly runs it.

## ☑️ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [ ] PR targets the `develop` branch (targets `V10`: the suite needs
the v10 SDK)
- [x] Documentation is updated (`examples/E2ETestApp/e2e/README.md`)
- [ ] New code is tested in main example apps, including all possible
scenarios (e2e app only, no SampleApp/Expo changes)
…3961)

Ticket: https://linear.app/stream/issue/RN-519

## 🎯 Goal

Design fix: the number next to the clustered reactions should be the
total number of reactions on the message, shown whenever it is greater
than 1. It was showing the number of reaction types beyond the first
four, and only when there were more than four types.

## 🛠 Implementation details

- `ReactionListClustered` sums the `count` of every reaction type and
shows the badge when the total is above 1.
- The badge's accessibility label now reads "N reaction(s)", using the
`message.reactionList.total.accessibilityLabel` key.

## 🧪 Testing

- New `ReactionListClustered.test.tsx`: the total across types, a single
type reacted twice, the accessibility label, and no badge for a single
reaction.
Ticket: https://linear.app/stream/issue/RN-520

## 🎯 Goal

The first nightly e2e run
([42739845823](https://github.com/GetStream/stream-chat-react-native/actions/runs/42739845823))
failed on both platforms and nothing reached Slack:

- **Android:** a temporary Gradle Plugin Portal outage
(`foojay-resolver-convention` not found) broke the build. The
coverage-map step then failed too, because no tests had run.
- **iOS:** the full suite had never run in CI before. 77 tests failed,
and with `--retries 2` the job went over the 180-minute timeout and was
cancelled, so no report was uploaded. Most of the time went to the iOS
photo-library permission prompt, which blocked every later `launchApp`.
- **Slack:** `e2e-test-cron.yml` had no notification job.

The nightly also tested one OS version per platform, and ran the Android
coverage pass as a single job that had never finished. The Flutter
nightly covers several versions and splits coverage into batches, so
this PR does the same here.

## 🛠 Implementation details

**Nightly matrix, as in the Flutter nightly**

| | Before | After |
|---|---|---|
| iOS | 26.2 (iPhone 17 Pro) | 26.2 (iPhone 17 Pro) and 17.5 (iPhone 15
Pro) |
| Android | API 36 with coverage, one job | API 36 with coverage in 4
batches, plus API 34, 31 and 28 without coverage |

- iOS 17.5 isn't on the runner image. The new `setup-ios-runtime` action
installs it with `ipsw` (`fastlane install_runtime`,
`bin/install_ios_runtime.sh`, both copied from Flutter) and creates the
simulator. If that simulator is missing, the job fails instead of
letting Detox pick another iOS version.
- Android builds two sets of APKs once, one instrumented for coverage
and one plain, and every test job downloads the set it needs.
- After its tests, each coverage batch writes `failed_tests_<n>.json`:
every test in the batch that didn't pass, including tests that never
ran.
- A new `coverage_map` job merges the batches and runs
`build_e2e_coverage_map batch_count:4`. Failed tests keep their previous
coverage. If a batch is missing, the job fails and the previous map
stays in use.
- The emulator now uses `-gpu swangle_indirect`, as PR runs do (RN-513).

**Workflows**
- Android `Build app` (nightly and PR) retries once after 60s.
- The iOS test step has `timeout-minutes: 120`, so it fails (and
reports) instead of being cancelled with the job.
- A new `slack` job posts to `SLACK_WEBHOOK_NIGHTLY_CHECKS` when a
scheduled run fails or is cancelled. It covers every nightly job.

**iOS harness**, where Detox runs without synchronization
- `IOSSimulator.grantIOSPermissions()` grants photos and camera through
`applesimutils`, then restarts `tccd`. `simctl privacy grant` writes a
TCC row that iOS 18 Photos ignores, so the system prompt still appeared.
- `waitToSettleAndClick`: waits for the element to appear, then 1s,
before tapping. Used for sheets, swipe actions, back, reaction pill,
Paste, the attachment picker and polls.
- After a message-menu action, wait until the backdrop is gone. If the
menu doesn't open, retry the long-press.
- `isAnyDisplayed`: iOS reports off-screen list cells as matches, so a
check passes if any match is visible. The scroll loops use it, and
scroll steps are smaller on iOS and settle before each check.
- Visibility checks became existence checks where iOS Fabric mounts the
container with no content of its own. Assertions that checked once now
wait.
- Three offline tests use `disableInternetConnection`, which only exists
on Android, so they are now `itOnAndroid`.

None of the failures were SDK bugs.

## 🧪 Testing

- Full iOS suite on a fresh simulator (iPhone 16 Pro, iOS 18.5): **178
passed, 77 skipped, 0 failed, no retries, about 26 min**. Before this
change, CI got 84 passing and 77 failing, then the job was cancelled
after 3h.
- I didn't run Android locally. The Android code paths barely change, so
this PR's e2e run covers them.
- The nightly on this branch:
[42874414367](https://github.com/GetStream/stream-chat-react-native/actions/runs/42874414367).
- I ran `e2e_failed_tests` and `build_e2e_coverage_map` locally on real
coverage and Allure results. They built the map with the batches
complete and stopped when a batch was missing; a test that never ran was
listed as failed.
- eslint, prettier, actionlint and `tsc -p e2e` are clean.

Before the next nightly, the `SLACK_WEBHOOK_NIGHTLY_CHECKS` repo secret
has to be added.

## ☑️ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] 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
Ticket: https://linear.app/stream/issue/RN-522

## 🎯 Goal

Editing a message with a video attachment creates a crash. The video
upload preview reads the duration off `localMetadata.file`, which
doesn't exist for attachments restored from a sent message or a draft.

## 🛠 Implementation details

- Guard the `localMetadata.file` read in `VideoAttachmentUploadPreview`.
Already sent videos now render without the duration pill and nothing
else changes.

## 🧪 Testing

- Added a regression test that puts the composer into the editing state
with a video attachment.
Ticket: https://linear.app/stream/issue/RN-520

## 🎯 Goal

Follow-up to #3965, which moved the nightly onto Flutter's OS matrix.
Its first run on the branch
([42874414367](https://github.com/GetStream/stream-chat-react-native/actions/runs/42874414367))
showed:

- **Android:** every version passed and the coverage map was built.
- **iOS 26.2:** hit the 120-minute test-step limit. One pass on a
`macos-15` runner takes about 105 minutes, and that pass had 27
failures.
- **iOS 17.5**, which the suite had never run on, had its own failures.

## 🛠 Implementation details

**iOS 26 and later**
- System alert buttons match `label + button traits` twice: a wrapper
(`_UIInterfaceActionCustomViewRepresentationView`) and the action view
(`_UIAlertControllerActionView`), with the same frame. Detox then fails
on multiple matches, which caused 15 of the 27 failures (Delete, Block,
Leave, Flag). Alert buttons are now tapped at `index: 0`. I checked the
matches on a local iOS 27 simulator.

**iOS 17**
- Detox never counts the menu's transparent `message-overlay-backdrop`
as visible. `closeContextMenu` now waits for the backdrop to exist, taps
a point on it away from the menu, and taps again until it is gone. This
fixed 4 moderation tests.
- The channel hidden under an open thread is still reported. On iOS,
`assertMessages` counts only matches on screen.
- Taps during animations: the "N Replies" label and the poll's
multiple-votes switch now use `waitToSettleAndClick`. Message-list
swipes retry while Detox reports the list as not hittable (a thread
still sliding in).

**One SDK bug:** [RN-521](https://linear.app/stream/issue/RN-521). On
iOS 17, "scroll to bottom" leaves the newest message's footer under the
composer. `test_reloadsSkippedMessages_whenScrolledToTheBottom` is
skipped on iOS below 18 through the new `itExceptOnOldIOS`. That helper
reads the simulator's runtime, and the Fastfile's test-id parser
recognizes it.

**Timeout:** the iOS nightly job gets 360 minutes and its test step 300,
as in the Flutter nightly.

## 🧪 Testing

- iOS 17.5, iPhone 15 Pro:
  - Before the fixes, the full suite had 22 failures.
  - After the first round, a full run had 174 passed and 3 failed.
  - Those 3 were then fixed, and each passed twice when re-run.
- The alert matcher was checked on iOS 27.1 with a probe test.
- Nightly on this branch:
[42902107500](https://github.com/GetStream/stream-chat-react-native/actions/runs/42902107500).
- Android API 36 coverage batch 0 failed when the emulator died mid-run
(RN-513). Its tests keep their previous coverage in the map.
- eslint, prettier, actionlint and `tsc -p e2e` are clean.

## ☑️ Checklist

- [x] I have signed the [Stream
CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform)
(required)
- [x] 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
@isekovanic
isekovanic requested a review from oliverlaz November 10, 2026 22:20
@github-actions

Copy link
Copy Markdown

Next releases

v10.0.1

10.0.1 (2026-11-10)

Bug Fixes

sampleapp@v5.0.1

5.0.1 (2026-11-10)

Bug Fixes

@Stream-SDK-Bot

Copy link
Copy Markdown
Contributor

SDK Size

title develop branch diff status
js_bundle_size 2027 KB 2027 KB 0 B 🟢
ai_components_size 163 KB 163 KB 0 B 🟢

@isekovanic
isekovanic merged commit 3ea8a7f into main Nov 10, 2026
31 of 32 checks passed
@stream-ci-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 10.0.1 🎉

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.

4 participants