Skip to content

fix: start the newest interval with the first message in a channel loaded empty - #2007

Merged
isekovanic merged 1 commit into
masterfrom
fix/first-message-in-empty-channel
Nov 9, 2026
Merged

isekovanic merged 1 commit into
masterfrom
fix/first-message-in-empty-channel

Conversation

@isekovanic

Copy link
Copy Markdown
Contributor

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

CLA

  • I have signed the Stream CLA (required).
  • Code changes are tested

Description of the changes, What, Why and How?

When a channel's newest page is loaded empty, the message paginator creates no interval. The first message ingested after that, a local send or a message.new event, went into __logical_head__ instead of starting the newest interval. isActiveIntervalAtHead then stayed false and jumpToTheLatestMessage() found no head interval, although the list held the newest message.

isNewNewestMessage() only treated a message as the newest when latestMessageAt was known. A channel that has never had a message has no last_message_at and no loaded message, so it returned false. A second message fixed the state, because it saw the first one's created_at and started the newest interval with both.

It now also treats a message as the newest when latestMessageAt is null and the newest page was loaded empty (items is empty and hasMoreHead is false), so ingestAsNewestInterval() starts the newest interval with it. A paginator that has loaded nothing yet keeps its behaviour, since its items is undefined.

In RN this showed as a "Failed to jump to the latest message" notification on the first message of the "Starting a conversation" UI cookbook. When watch() supersedes the provisional channel, <Channel> swaps to the stored instance, whose only message sat in the logical head. useJumpToLatestOnOpen read the list as not at head and called jumpToTheLatestMessage(), and the query came back empty because the server did not have the message yet.

Testing

  • New test in MessagePaginator.test.ts. A single own sending message after an empty newest page starts the newest interval and isActiveIntervalAtHead is true. It fails without the fix.
  • Checked with prepareExternalQuery().placePage([]), placeExternalQueryPage([], pageSize) and placeExternalNewestPage([], ...), for active and inactive channels and for received and sending messages. All of them put the first message in the logical head before the fix.
  • Ran the RN cookbook flow in a Jest repro with the fix applied. Without it the flow emits api:message:jumpToLatest:failed, with it there are no notifications.
  • yarn test, yarn run types, eslint and prettier pass.

Changelog

  • Fix the first message in a channel with no messages not being treated as the newest, which made jumpToTheLatestMessage() fail.

…aded empty

A channel whose newest page was loaded empty has no interval and no known
latest message time, so `isNewNewestMessage()` returned false and the first
message went into the logical head. The list then never read as at head,
and `jumpToTheLatestMessage()` failed while the server did not have the
message yet.
@isekovanic
isekovanic merged commit d31d789 into master Nov 9, 2026
7 checks passed
@isekovanic
isekovanic deleted the fix/first-message-in-empty-channel branch November 9, 2026 13:06
github-actions Bot pushed a commit that referenced this pull request Nov 9, 2026
## [10.0.1](v10.0.0...v10.0.1) (2026-11-09)

### Bug Fixes

* start the newest interval with the first message in a channel loaded empty ([#2007](#2007)) ([d31d789](d31d789))
@stream-ci-bot

Copy link
Copy Markdown

🎉 This PR is included in version 10.0.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

isekovanic added a commit to GetStream/stream-chat-react-native that referenced this pull request Nov 9, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants