Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions ai-docs/ai-migration-v9-to-v10.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ means changed. Details in the linked section.
| `useMessageContext().members` | `useStateStore(channel.state, (s) => ({ members: s.members }))` | §4 |
| `<ChannelList options={{ offset, limit }}>` | the `ChannelPaginator` owns paging: `requestOptions` takes neither; page size is `paginatorOptions.pageSize` | §R.6 |
| `<ChannelList filters={{ myField: x }}>` | `new ChannelPaginator({ client, filters: { 'custom.myField': x } })` | §R.6 |
| `channel.state.messages` / `channel.lastMessage()` | `channel.messagePaginator.state` (`items`) / `channel.messagePaginator.lastMessage` | §R.7 |
| `channel.state.messages` / `channel.lastMessage()` | `channel.messagePaginator.state` (`items`) / `channel.messagePaginator.latestMessage` | §R.7 |
| `client.activeChannels[cid]` | `client.channelManager.get(cid)` | §R.7 |
| `client.threads.state` `threads` / `client.threads.loadNextPage()` | `client.threads.paginator.state` `items` / `client.threads.paginator.toTail()` | §R.7 |
| `client.addDevice(token, provider)` / `getDevices()` / `removeDevice(id)` | `client.createDevice({ id: token, push_provider })` / `listDevices()` / `deleteDevice({ id })` | §R.8 |
Expand Down Expand Up @@ -3138,8 +3138,8 @@ v9's `<ChannelList>` props.
v9 → v10:

- `channel.state.messages` → `useStateStore(channel.messagePaginator.state, (s) => ({ items: s.items }))`.
- `channel.state.latestMessages` / `channel.lastMessage()` → `channel.messagePaginator.lastMessage` (reactive through
`messagePaginator.aggregateState`).
- `channel.state.latestMessages` / `channel.lastMessage()` → `channel.messagePaginator.latestMessage` (reactive through
`messagePaginator.aggregateState`; `null` when there is none).
- `channel.state.threads[id]` → `client.threads.get(id)?.messagePaginator`.
- `channel.state.findMessage(id)` → `channel.messagePaginator.getItem(id)`, or `client.messageStore` for any loaded
message.
Expand All @@ -3163,9 +3163,10 @@ v9 → v10:

Two request behaviours:

- **`channel.query(…)` no longer updates the message list.** It returns the page; only the latest-page paths
(`watch()`, `create()`) seed the list. Page with `channel.messagePaginator.toTail()` / `toHead()` instead of
`channel.query({ messages: { id_lt } })`.
- **`channel.query(…)` no longer updates the message list.** It returns the page; only `watch()` places the page it
fetches (`create()` places no messages). Page with `channel.messagePaginator.toTail()` / `toHead()` instead of
`channel.query({ messages: { id_lt } })`. `watch()` and the paginator's own page requests do not go through
`channel.query()`, so a spy on or an override of it does not see them; listen to `channels.queried` instead.
- **A request with `watch` or `presence` waits for a WebSocket connection id** — `channel.watch()`, `queryChannels*`,
`queryUsers({ payload: { presence } })`. v9 silently downgraded it to a non-watching request. With no socket open
or opening it throws `No connection id is available…`, so call `connectUser()` (or `openConnection()` after a
Expand Down
5 changes: 2 additions & 3 deletions ai-docs/v10-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ update. Each entry says what changed and how to migrate in one line, and links t
- **`§…`** → [`ai-migration-v9-to-v10.md`](./ai-migration-v9-to-v10.md), the agent migration ledger (Before/After code,
detection greps). `§R.x` sections were added by the audit that produced this file.
- **i18n** → [`i18n-v10-migration.md`](./i18n-v10-migration.md) and [`i18n-v10-key-map.json`](./i18n-v10-key-map.json).
- **LLC** → `stream-chat` v10's own list: `stream-chat-js` `ai-docs/v10-breaking-changes.md` (branch
`claude/sdk-v10-breaking-changes-f64baf`) and the `v9-to-v10-migration-guide-*.md` files on `release-v10`.
- **LLC** → `stream-chat` v10's own list: `stream-chat-js` `ai-docs/v10-breaking-changes.md` on `release-v10`.
- **#NNNN** → the `GetStream/stream-chat-react-native` pull request that made the change.

Docs impact (which getstream.io pages change) is in [`v10-docs-impact.md`](./v10-docs-impact.md).
Expand Down Expand Up @@ -471,7 +470,7 @@ apps' own code.
- Message storage moved off `channel.state` (`messages`, `latestMessages`, `threads`, `pinnedMessages`,
`isUpToDate`, `findMessage`, `addMessageSorted`, `loadMessageIntoState`, …) → `channel.messagePaginator`,
`channel.pinnedMessagesPaginator`, `client.messageStore`, `client.threads.get(id)`.
- `channel.lastMessage()` → `channel.messagePaginator.lastMessage`.
- `channel.lastMessage()` → `channel.messagePaginator.latestMessage ?? undefined`.
- `channel.state.*Store` handles (pre-release) → `channel.state`; `mutedUsersStore` → `client.mutedUsersStore`. — §K.1 ·
§K.2
- `channel.disconnected` → `channel.pendingDisposal`; `<Channel>` keeps rendering a deleted channel. — §K.8 · P.15
Expand Down
2 changes: 1 addition & 1 deletion examples/ExpoChatGPTSample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"react-native-svg": "15.15.5",
"react-native-teleport": "^1.2.2",
"react-native-worklets": "0.13.0",
"stream-chat": "^10.0.0-rc.23",
"stream-chat": "^10.0.0-rc.24",
"stream-chat-expo": "workspace:^",
"stream-chat-react-native-core": "workspace:^",
"victory-native": "^42.0.1"
Expand Down
2 changes: 1 addition & 1 deletion examples/ExpoMessaging/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"react-native-teleport": "^1.2.2",
"react-native-web": "~0.21.4",
"react-native-worklets": "0.13.0",
"stream-chat": "^10.0.0-rc.23",
"stream-chat": "^10.0.0-rc.24",
"stream-chat-expo": "workspace:^",
"stream-chat-react-native-core": "workspace:^"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/SampleApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"react-native-teleport": "^1.2.2",
"react-native-video": "^6.19.3",
"react-native-worklets": "^0.13.0",
"stream-chat": "^10.0.0-rc.23",
"stream-chat": "^10.0.0-rc.24",
"stream-chat-react-native": "workspace:^",
"stream-chat-react-native-core": "workspace:^"
},
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^2.0.0",
"react-syntax-highlighter": "^16.1.0",
"stream-chat": "^10.0.0-rc.23",
"stream-chat": "^10.0.0-rc.24",
"use-sync-external-store": "^1.7.0",
"zod": "^4.1.12"
},
Expand Down
11 changes: 7 additions & 4 deletions package/src/__tests__/offline-support/offline-feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import { BetterSqlite } from '../../test-utils/BetterSqlite';
const ChannelPreviewComponent = ({ channel }: { channel: ChannelLLC }) => (
<View accessibilityLabel='list-item' testID={channel.cid}>
<Text>{channel.data?.custom?.name}</Text>
<Text>{channel.messagePaginator?.headItems?.[0]?.text}</Text>
<Text>{channel.messagePaginator?.anchoredHeadItems?.[0]?.text}</Text>
</View>
);

Expand Down Expand Up @@ -796,7 +796,8 @@ export const Generic = () => {
expect(matchingRows.length).toBe(1);
expect(matchingRows[0].hidden).toBeTruthy();
expect(matchingMessagesRows.length).toBe(
chatClient.channelManager.get(hiddenChannel.cid)?.messagePaginator.headItems.length,
chatClient.channelManager.get(hiddenChannel.cid)?.messagePaginator.anchoredHeadItems
.length,
);
});
});
Expand Down Expand Up @@ -831,7 +832,8 @@ export const Generic = () => {
expect(matchingRows.length).toBe(1);
expect(matchingRows[0].hidden).toBeTruthy();
expect(matchingMessagesRows.length).toBe(
chatClient.channelManager.get(hiddenChannel.cid)?.messagePaginator.headItems.length,
chatClient.channelManager.get(hiddenChannel.cid)?.messagePaginator.anchoredHeadItems
.length,
);
});

Expand All @@ -858,7 +860,8 @@ export const Generic = () => {
expect(matchingRows.length).toBe(1);
expect(matchingRows[0].hidden).toBeFalsy();
expect(matchingMessagesRows.length).toBe(
chatClient.channelManager.get(hiddenChannel.cid)?.messagePaginator.headItems.length,
chatClient.channelManager.get(hiddenChannel.cid)?.messagePaginator.anchoredHeadItems
.length,
);
});
});
Expand Down
36 changes: 19 additions & 17 deletions package/src/__tests__/offline-support/optimistic-update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export const OptimisticUpdates = () => {
// is intentionally NOT queued. Force the offline case. See the delete-message variant for detail.
it('pending task should exist if sendReaction request fails', async () => {
const reaction = generateReaction();
const targetMessage = channel.messagePaginator.headItems[0];
const targetMessage = channel.messagePaginator.anchoredHeadItems[0];

render(
<Chat client={chatClient} enableOfflineSupport>
Expand Down Expand Up @@ -415,7 +415,7 @@ export const OptimisticUpdates = () => {

it('pending task should be cleared if sendReaction request is successful', async () => {
const reaction = generateReaction();
const targetMessage = channel.messagePaginator.headItems[0];
const targetMessage = channel.messagePaginator.anchoredHeadItems[0];

render(
<Chat client={chatClient} enableOfflineSupport>
Expand Down Expand Up @@ -516,7 +516,7 @@ export const OptimisticUpdates = () => {
// is intentionally NOT queued. Force the offline case. See the delete-message variant for detail.
it('pending task should exist if deleteReaction request fails', async () => {
const reaction = generateReaction();
const targetMessage = channel.messagePaginator.headItems[0];
const targetMessage = channel.messagePaginator.anchoredHeadItems[0];

render(
<Chat client={chatClient} enableOfflineSupport>
Expand Down Expand Up @@ -549,7 +549,7 @@ export const OptimisticUpdates = () => {

it('pending task should be cleared if deleteReaction request is successful', async () => {
const reaction = generateReaction();
const targetMessage = channel.messagePaginator.headItems[0];
const targetMessage = channel.messagePaginator.anchoredHeadItems[0];

render(
<Chat client={chatClient} enableOfflineSupport>
Expand All @@ -576,7 +576,7 @@ export const OptimisticUpdates = () => {

describe('edit message', () => {
it('should keep the optimistic edit in state and DB if the LLC queues the edit', async () => {
const message = channel.messagePaginator.headItems[0];
const message = channel.messagePaginator.anchoredHeadItems[0];
const editedText = 'edited while offline';

// Registered declaratively — the `<Channel doUpdateMessageRequest>` prop is gone. The LLC
Expand Down Expand Up @@ -666,7 +666,7 @@ export const OptimisticUpdates = () => {
});

it('should keep the optimistic edit if the request fails', async () => {
const message = channel.messagePaginator.headItems[0];
const message = channel.messagePaginator.anchoredHeadItems[0];
const editedText = 'should stay optimistic';

// Registered declaratively — the `<Channel doUpdateMessageRequest>` prop is gone. The LLC
Expand Down Expand Up @@ -738,7 +738,7 @@ export const OptimisticUpdates = () => {
// failed message carries no message_text_updated_at. generateMessage always sets one, so strip
// it here; the test then verifies the optimistic edit path does not add one back.
const { message_text_updated_at: _stripped, ...headItem } = channel.messagePaginator
.headItems[0] as LocalMessage & { message_text_updated_at?: Date };
.anchoredHeadItems[0] as LocalMessage & { message_text_updated_at?: Date };
const message = headItem as LocalMessage;
const optimisticStateSpy = jest.fn();

Expand Down Expand Up @@ -795,7 +795,7 @@ export const OptimisticUpdates = () => {
});

it('should keep the optimistic edit for attachment updates without auto-queueing', async () => {
const message = channel.messagePaginator.headItems[0];
const message = channel.messagePaginator.anchoredHeadItems[0];
const editedText = 'edited attachment message';
const localUri = 'file://edited-attachment.png';
const editedAttachments = [
Expand Down Expand Up @@ -899,7 +899,7 @@ export const OptimisticUpdates = () => {
// failing until the default optimistic offline update/delete lands — the red is the signal.
describe('default offline update/delete (no custom handler)', () => {
it('should optimistically apply + persist an offline edit without a custom doUpdateMessageRequest', async () => {
const message = channel.messagePaginator.headItems[0];
const message = channel.messagePaginator.anchoredHeadItems[0];
const editedText = 'edited offline via default path';

render(
Expand Down Expand Up @@ -953,7 +953,7 @@ export const OptimisticUpdates = () => {
});

it('should optimistically apply + persist an offline delete without a custom doDeleteMessageRequest', async () => {
const message = channel.messagePaginator.headItems[0];
const message = channel.messagePaginator.anchoredHeadItems[0];

render(
<Chat client={chatClient} enableOfflineSupport>
Expand Down Expand Up @@ -1121,7 +1121,7 @@ export const OptimisticUpdates = () => {

describe('optimistic edit without offline support', () => {
it('keeps the optimistic edit AND surfaces the failure when there is no offline DB', async () => {
const message = channel.messagePaginator.headItems[0];
const message = channel.messagePaginator.anchoredHeadItems[0];
const editedText = 'edited with no offline support';

chatClient.config.set({
Expand Down Expand Up @@ -1180,7 +1180,7 @@ export const OptimisticUpdates = () => {
jest.setTimeout(20_000);

it('pending task should be executed after connection is recovered', async () => {
const message = channel.messagePaginator.headItems[0];
const message = channel.messagePaginator.anchoredHeadItems[0];
const reaction = generateReaction();

render(
Expand Down Expand Up @@ -1291,7 +1291,7 @@ export const OptimisticUpdates = () => {
it('should not re-add a failed local message after reconnect when its pending send task was resolved', async () => {
const localMessage = generateMessage({
// The channel paginator only ingests messages whose cid matches (matchesFilter), so the
// optimistic message must carry the channel cid to appear in the paginator/headItems.
// optimistic message must carry the channel cid to appear in the paginator/anchoredHeadItems.
cid: channel.cid,
status: MessageStatusTypes.SENDING,
text: 'offline resend',
Expand Down Expand Up @@ -1344,7 +1344,9 @@ export const OptimisticUpdates = () => {
});

expect(
channel.messagePaginator.headItems.some((message) => message.id === localMessage.id),
channel.messagePaginator.anchoredHeadItems.some(
(message) => message.id === localMessage.id,
),
).toBe(true);

jest
Expand Down Expand Up @@ -1380,7 +1382,7 @@ export const OptimisticUpdates = () => {
});

await waitFor(() => {
const matchingMessages = channel.messagePaginator.headItems.filter(
const matchingMessages = channel.messagePaginator.anchoredHeadItems.filter(
(message) => message.text === localMessage.text,
);

Expand All @@ -1399,7 +1401,7 @@ export const OptimisticUpdates = () => {
it('should re-add a failed local message after reconnect when fresh state still does not contain it', async () => {
const localMessage = generateMessage({
// The channel paginator only ingests messages whose cid matches (matchesFilter), so the
// optimistic message must carry the channel cid to appear in the paginator/headItems.
// optimistic message must carry the channel cid to appear in the paginator/anchoredHeadItems.
cid: channel.cid,
status: MessageStatusTypes.SENDING,
text: 'offline resend unresolved',
Expand Down Expand Up @@ -1476,7 +1478,7 @@ export const OptimisticUpdates = () => {
});

await waitFor(() => {
const matchingMessages = channel.messagePaginator.headItems.filter(
const matchingMessages = channel.messagePaginator.anchoredHeadItems.filter(
(message) => message.id === localMessage.id,
);

Expand Down
6 changes: 3 additions & 3 deletions package/src/components/Channel/__tests__/Channel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,8 @@ describe('Channel', () => {
}),
);

it('should call the channel query method to load more messages', async () => {
const channelQuerySpy = jest.spyOn(channel, 'query');
it('should request the channel state to load more messages', async () => {
const channelQuerySpy = jest.spyOn(channel, 'requestChannelState');

const newMessages = [generateMessage()];

Expand Down Expand Up @@ -1008,7 +1008,7 @@ describe('Channel initial load useEffect', () => {
// the 10 originals + 10 failed remain.
await waitFor(() => {
expect(reloadSpy).toHaveBeenCalled();
expect(channel.messagePaginator.headItems.length).toBe(20);
expect(channel.messagePaginator.anchoredHeadItems.length).toBe(20);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const ChannelPreviewComponent = ({ channel }: { channel: ChannelType }) => {
return (
<View accessibilityLabel='list-item' testID={channel.id}>
<Text>{name}</Text>
<Text>{channel.messagePaginator.headItems[0]?.text}</Text>
<Text>{channel.messagePaginator.latestMessage?.text}</Text>
</View>
);
};
Expand Down Expand Up @@ -513,7 +513,7 @@ describe('ChannelList', () => {
describe('message.new', () => {
// The message must carry the target channel's `cid`: v10's `MessageIntervalPaginator` filters
// ingested messages by `cid` (`buildMatchFilters`), so a message without one is dropped and never
// reaches the channel's `messagePaginator.headItems` that the preview reads.
// reaches the channel's `messagePaginator.anchoredHeadItems` that the preview reads.
const sendNewMessageOnChannel3 = () => {
const newMessage = generateMessage({
cid: testChannel3.channel.cid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type LastMessageType = LocalMessage | MessageResponse;
// The latest message comes from the paginator's `aggregateState`, which stays current when a new
// message lands in the head interval while an older window is active.
const lastMessageSelector = (state: MessagePaginatorAggregateState) => ({
lastMessage: state.lastMessage ?? undefined,
lastMessage: state.latestMessage ?? undefined,
});

export const useChannelPreviewData = (channel: Channel, client: StreamChat) => {
Expand Down
2 changes: 1 addition & 1 deletion package/src/components/MessageList/MessageFlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
}

if (paginator.hasMoreHead) {
latestNonCurrentMessageBeforeUpdateRef.current = paginator.lastMessage ?? undefined;
latestNonCurrentMessageBeforeUpdateRef.current = paginator.latestMessage ?? undefined;
setAutoscrollToRecent(false);
setScrollToBottomButtonVisible(true);
return;
Expand Down
2 changes: 1 addition & 1 deletion package/src/components/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
}

if (paginator.hasMoreHead) {
latestNonCurrentMessageBeforeUpdateRef.current = paginator.lastMessage ?? undefined;
latestNonCurrentMessageBeforeUpdateRef.current = paginator.latestMessage ?? undefined;
setAutoscrollToRecent(false);
setScrollToBottomButtonVisible(true);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,8 @@ describe('MessageList pagination', () => {
),
});
channel.messagePaginator.state.partialNext({
// an older window than the newest messages, so "go to latest" has to jump
hasMoreHead: true,
items: Array.from({ length: 10 }, (_, i) => generateMessage({ id: String(i) })),
});

Expand Down
6 changes: 3 additions & 3 deletions package/src/components/MessageList/hooks/useMarkRead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ export const useMarkRead = (channel: Channel) => {
// with unreads keeps its unread separator until the user catches up.
const { updateChannelUnreadState = true } = options ?? {};
if (updateChannelUnreadState) {
const { lastMessage, lastMessageAt, unreadStateSnapshot } = channel.messagePaginator;
const { latestMessage, latestMessageAt, unreadStateSnapshot } = channel.messagePaginator;
const previous = unreadStateSnapshot.getLatestValue();
unreadStateSnapshot.next({
firstUnreadMessageId: null,
lastReadAt: lastMessageAt ?? nowNs(),
lastReadMessageId: lastMessage?.id ?? previous.lastReadMessageId,
lastReadAt: latestMessageAt ?? nowNs(),
lastReadMessageId: latestMessage?.id ?? previous.lastReadMessageId,
unreadCount: 0,
});
}
Expand Down
Loading
Loading