Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
397a2a2
feat!: remove messageSetToAddToIfDoesNotExist from channel.query
MartinCupela Oct 13, 2026
66b544e
Merge branch 'release-v10' into martincupela/react-1075-get-rid-of-me…
MartinCupela Oct 13, 2026
96f9bfc
fix: mark the message paginator's head window correctly
MartinCupela Oct 13, 2026
4da840d
fix: read the newest messages from the head window, not the head-most…
MartinCupela Oct 13, 2026
6cc8c6b
docs: list channel.lastMessage as removed in the v9 to v10 guide
MartinCupela Oct 13, 2026
f33930a
fix: mark a fetched page holding the newest message as the head window
MartinCupela Oct 13, 2026
bf11aba
fix: keep the newest messages in the head window while another window…
MartinCupela Oct 13, 2026
2cb4ff4
fix: let watch() and create() place any page of messages through the …
MartinCupela Oct 14, 2026
e55f88e
fix: re-seed the pinned first page into the newest pinned window
MartinCupela Oct 14, 2026
6bba728
refactor!: remove the next/prev paginator aliases
MartinCupela Oct 14, 2026
77dadc1
fix!: mark the head window stale when a query reports newer messages
MartinCupela Oct 14, 2026
90a4a88
docs: describe latestMessage and queriedLatestMessageAt in CLAUDE.md
MartinCupela Oct 14, 2026
d9bef72
refactor!: run paginator queries through one pipeline and keep watch(…
MartinCupela Oct 19, 2026
1130fb9
fix: keep the reader's window across gaps and late pages; idle channe…
MartinCupela Oct 20, 2026
ee6e387
fix: keep the newest mark when a page joins a window to the newest wi…
MartinCupela Oct 20, 2026
3a88c9d
Merge remote-tracking branch 'origin/release-v10' into martincupela/r…
MartinCupela Oct 20, 2026
88aa5c4
fix: show the newest page after a reconnect gap and fix the RN review…
MartinCupela Oct 21, 2026
fb0e3f3
fix: judge whether a newest page connects by where it starts, not by …
MartinCupela Oct 21, 2026
de30d97
fix: keep an active older interval when a newest page arrives across …
MartinCupela Oct 22, 2026
a55365f
Merge branch 'release-v10' into martincupela/react-1075-get-rid-of-me…
MartinCupela Oct 22, 2026
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
4 changes: 2 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

116 changes: 61 additions & 55 deletions ai-docs/v10-breaking-changes.md

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions ai-docs/v10-followups.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,14 @@ Ordered by severity. The breaking dead-code removals and leftover decisions are

<a id="channel-query-messageset"></a>

### `channel.query()` keeps v9 message-set semantics in its signature
### `channel.query()` keeps v9 message-set semantics in its signature [SOLVED 397a2a2c]

`simplification` · confidence: high · pre-release: yes · channel

- **Where:** `src/channel.ts:1909-1913, 1931-1934`; JSDoc `:1901-1904`
- **What:** `messageSetToAddToIfDoesNotExist: MessageSetType` survives, but only `'latest'` has an effect (seeds the paginator); `'current'` and `'new'` are identical no-ops. The JSDoc still describes v9 message sets.
- **Suggested change:** Replace with `{ seedLatest?: boolean }` (or narrow to `'latest' | undefined`) and fix the JSDoc.
- **Resolution:** the parameter and `MessageSetType` are removed; `channel.query(options?, requestOptions?)` adds no messages, and the message paginator places the page `watch()` fetches ([channel-query](v10-breaking-changes.md#channel-query)).

<a id="poll-update-fields"></a>

Expand Down Expand Up @@ -694,7 +695,7 @@ Unused in `src` and not used downstream. Most are v10-only. The ones marked "v9
- **Where / What:** no src reader, but used downstream, so keep (or migrate the consumer first):
- `AttachmentManager.fileToLocalUploadAttachment` (deprecated): RN `examples/ExpoChatGPTSample/screens/ChatContent.tsx:121`.
- `AttachmentManager.uploadAttachment` (deprecated): runtime use in both UI SDKs. React `src/components/MediaRecorder/hooks/useMediaRecorder.ts:58`, `src/components/MessageComposer/AttachmentPreviewList/AttachmentPreviewList.tsx:99-141` (`handleRetry`), `VoiceRecordingPreviewSlot.tsx:39`; RN `package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx:353-503`, `package/src/components/MessageInput/hooks/useAudioRecorder.tsx:111`.
- `BasePaginator.next`, `hasNext`, `hasPrev` (deprecated shims): React `src/components/ChannelList/ChannelList.tsx:95` (`loadNextOnScrollToBottom={paginator.next}`), `src/components/InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx:66-67` (see [shims](#lo-paginator-shims)).
- `BasePaginator.next`, `hasNext`, `hasPrev` (deprecated shims): React `src/components/ChannelList/ChannelList.tsx:95` (`loadNextOnScrollToBottom={paginator.next}`), `src/components/InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx:66-67` (see [shims](#lo-paginator-shims)). Removed in 6bba7286, after React `master` moved to `toTail` / `hasMoreTail` / `hasMoreHead`.
- `TextComposer.upsertMentionedUser` (deprecated): React `MessageInput.test.tsx:1750-1778`.
- `MessageComposer.allowsPendingUploads`: React `examples/vite/src/Debug/ComposerStateInspector.tsx:140`. Now only an alias of `attachmentManager.config.pendingUploadsEnabled` (`src/messageComposer/messageComposer.ts:474-480`), still read by `hasSendableData`.
- `MessageReceiptsTracker.onMessageDelivered` / `onMessageRead`: RN tests (see [above](#dc-receipts-tracker)).
Expand Down Expand Up @@ -746,13 +747,14 @@ Unused in `src` and not used downstream. Most are v10-only. The ones marked "v9

<a id="lo-paginator-shims"></a>

#### Paginator and notification compat shims
#### Paginator and notification compat shims [PARTLY SOLVED 6bba7286]

`leftover` · confidence: high · pre-release: no · pagination / notifications

- **Where:** `src/pagination/paginators/BasePaginator.ts:237-249, 700-709, 3492-3529` (`next`, `prev`, `nextDebounced`, `prevDebounced`, `hasNext`, `hasPrev`, `PaginationQueryReturnValue.next/prev`); `src/notifications/types.ts:122-133`
- **What:** Deprecated shims shipped in the new major. React `master` still calls them at runtime on `ChannelPaginator`: `loadNextOnScrollToBottom={paginator.next}` (`src/components/ChannelList/ChannelList.tsx:95`) and `paginator.hasPrev` / `paginator.hasNext` (`src/components/InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx:66-67`, through `PaginatorLike`). `NotificationState` is deprecated, yet `NotificationManagerState` is declared as an alias of it, so the deprecated name is the canonical definition.
- **Suggested change:** Keep them for the whole v10 line ([keep](#keep-downstream)); removing them first requires migrating React's ChannelList / InfiniteScrollWithComponents to `toTail` / `hasMoreTail` / `hasMoreHead`, so remove in v11. Invert the alias so `NotificationState` points at `NotificationManagerState` (non-breaking, any time).
- **Resolution:** the paginator shims (`next`, `prev`, `nextDebounced`, `prevDebounced`, `hasNext`, `hasPrev`, `PaginationQueryReturnValue.next/prev`) are removed before 10.0.0 ([paginator navigation](v10-breaking-changes.md#paginator-navigation)); React `master` type-checks and passes against that build. The `NotificationState` alias is still open.

<a id="lo-composer-deprecations"></a>

Expand Down Expand Up @@ -1023,7 +1025,7 @@ Only places that survive the guide cleanup: JSDoc and code comments, README, CHA

- **Where / What:**
- `src/pagination/paginators/BasePaginator.ts:719-722`: `isInitialized` says "has not performed any query so far" but returns `true` once a query shape was recorded.
- `src/pagination/paginators/BasePaginator.ts:3260-3266`: the `next`/`prev` fallback "keeps non-migrated external subclasses working", but such subclasses reject in `getNextQueryShape()` (`:996-1000`) and, without `initialCursor`, their cursors are ignored (`:728-730`).
- `src/pagination/paginators/BasePaginator.ts:3260-3266`: the `next`/`prev` fallback "keeps non-migrated external subclasses working", but such subclasses reject in `getNextQueryShape()` (`:996-1000`) and, without `initialCursor`, their cursors are ignored (`:728-730`). [SOLVED 6bba7286: the fallback is removed.]
- `src/thread_manager.ts:85, 318, 480`, `src/pagination/paginators/ThreadPaginator.ts:77`: comments compare against "`master`" as the baseline, which goes stale once v10 is `master`.
- **Suggested change:** Fix the `isInitialized` doc; reword the fallback comment to "accepts legacy `next`/`prev` keys for subclasses that implement `getNextQueryShape` and set `initialCursor`" (or drop the fallback); say "v9" instead of `master`.

Expand Down Expand Up @@ -1124,9 +1126,9 @@ Only places that survive the guide cleanup: JSDoc and code comments, README, CHA
| Where | Gap | Suggested change |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `test/unit/utils.test.js:6` | `describe.skip('generateUUIDv4')` (skipped in v9 too); its TODO says it fails on newer Node, so the no-`crypto` fallback is never tested. | Stub `crypto` instead of skipping. |
| `test/unit/pagination/paginators/BasePaginator.test.ts:1200` | `it.todo('... whe updateState === false')`: the `updateState: false` path is untested ("whe" typo). | Write the test. |
| `test/unit/pagination/paginators/BasePaginator.test.ts:1200` | `it.todo('... whe updateState === false')`: the `updateState: false` path is untested ("whe" typo). [SOLVED #1926] | Write the test. |
| `test/unit/pagination/paginators/BasePaginator.test.ts:192` | Title "throws is the paginator does implement own getNextQueryShape" means "throws if ... does not implement". | Fix the title. |
| `test/unit/pagination/paginators/MessagePaginator.test.ts:1630` | `describe.todo('postQueryReconcile and deriveCursor for', () => {})`: neither path is tested. | Write the tests. |
| `test/unit/pagination/paginators/MessagePaginator.test.ts:1630` | `describe.todo('postQueryReconcile and deriveCursor for', () => {})` (now `placeQueryPage`): neither path is tested. | Write the tests. |
| `test/unit/channel.test.js:2618` | Empty body: `it('does not override the delivery information in the read status', () => {})` (also empty in v9). | Implement or delete. |
| `test/unit/channel.test.js:3369-3373` | Comment says "`client.search()` was removed - it forwarded to `client.search()`"; the removed method was `channel.search()`, and it did scope by `cid`. | Fix the comment. |
| `test/unit/messageDelivery/MessageDeliveryReporter.test.ts:475-480` | "still requests when no local unread count was configured" mocks `markRead`, hiding that the real `channel.markRead()` throws for `read_events: false` and `markReadViaReporter()` rejects. | Assert the real rejection. |
Expand Down
4 changes: 2 additions & 2 deletions specs/remove-legacy-channelstate-messages/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ paginators (owner-approved).
- Removed the dead `MessageSet` object-shape type from `types.ts` (public via `export * from './types'`
→ **breaking**) and the internal-only `DEFAULT_MESSAGE_SET_PAGINATION` from `constants.ts`
(not re-exported → non-breaking).
- **Kept** `MessageSetType` — still the type of `channel.query`'s `messageSetToAddToIfDoesNotExist`
param, which drives paginator seeding (`'latest'` seeds the first page; `'current'`/`'new'` do not).
- `MessageSetType` was later removed with `channel.query`'s `messageSetToAddToIfDoesNotExist` param
(REACT-1075): `watch()`/`create()` seed the first page, and `query()` never does.
- **Kept** `isLatestMessageSet` / `isLatestMessagesSet` — live flags on the `channels.queried` event
and the offline `upsertChannels` payload ("is this the latest page"), not the removed storage.
- **Kept** `binarySearchByDateEqualOrNearestGreater` (public util, now internally unused): removing it
Expand Down
8 changes: 4 additions & 4 deletions src/ChannelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,9 @@ export class ChannelManager extends WithSubscriptions {
* Removes every stored channel the app disconnected (`pendingDisposal`), whatever holds it, then
* every one that is neither watched nor held; lists drop them. Kept: watched channels
* (`watching`, or `wasWatching` until restored), active ones ({@link Channel.activate}), ones
* being loaded, and ones held in {@link ChannelManager.channelStore} (lists link theirs; threads,
* the composer cache and channel search hold theirs through claims,
* {@link EntityStore.addClaim}).
* being queried (`Channel.isQueryingChannel`), and ones held in
* {@link ChannelManager.channelStore} (lists link theirs; threads, the composer cache and channel
* search hold theirs through claims, {@link EntityStore.addClaim}).
*
* The SDK never calls this, as it cannot see channels an app keeps in its own state. Call it when
* the app is done with them (e.g. after `searchController.dispose()`), and keep a channel your
Expand Down Expand Up @@ -919,7 +919,7 @@ export class ChannelManager extends WithSubscriptions {
/**
* Removes a paginator: it leaves `state.paginators`, stops receiving WS-driven updates and gets
* its own `filterQueryResults` back. Its loaded items stay, and a query scheduled through
* `nextDebounced` is cancelled.
* `toTailDebounced` or `toHeadDebounced` is cancelled.
*
* @param paginatorOrId - The paginator to remove, or its ID.
* @returns The removed paginator, or `undefined` when this manager did not hold it.
Expand Down
14 changes: 11 additions & 3 deletions src/CooldownTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,17 @@ export class CooldownTimer extends WithSubscriptions {
.data ?? {}) as Partial<ChannelResponse>;
const canSkipCooldown = (own_capabilities ?? []).includes('skip-slow-mode');

const ownLatestMessageTimestamp = this.findOwnLatestMessageTimestamp({
messages: this.channel.messagePaginator.headItems,
});
const paginator = this.channel.messagePaginator;
// The newest messages: the window that reaches the newest message, and the live messages received
// while it was not loaded. Without that window an own message may sit in the part not loaded, so
// the timestamp already known is kept rather than cleared.
const ownLatestMessageTimestamp =
this.findOwnLatestMessageTimestamp({
messages: [...paginator.anchoredHeadItems, ...paginator.logicalHeadItems],
}) ??
(paginator.anchoredHeadItem !== undefined
? undefined
: this.ownLatestMessageTimestamp);

if (
cooldownConfigSeconds !== this.cooldownConfigSeconds ||
Expand Down
Loading
Loading