Skip to content

feat: add scheduled messages for v9 (CHA-5934) - #1951

Open
NikolaGoranNaumovski wants to merge 41 commits into
release-v9from
feat/cha-5934-scheduled-messages-api
Open

NikolaGoranNaumovski wants to merge 41 commits into
release-v9from
feat/cha-5934-scheduled-messages-api

Conversation

@NikolaGoranNaumovski

@NikolaGoranNaumovski NikolaGoranNaumovski commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

CLA

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

Description of the changes, What, Why and How?

Note

The backend (CHA-5924) shipped in chat v239.72.0, behind the per-app flag chat_scheduled_messages_enabled (off by default).

Scheduled messages for v9 ("send later"): the API contract, a state layer that keeps the user's scheduled messages in memory, and MessageComposer support for scheduling, editing and scheduling a failed message again.

This PR combines the three stacked v9 PRs into one: this one (API), #1915 (state layer, by nijeesh-stream) and #1956 (composer). It is up to date with release-v9. The v10 counterpart is #1990. React v14 builds on it in GetStream/stream-chat-react#3353.

Tickets

1. API

Sending

  • channel.sendMessage(message, { scheduled_at }) schedules the message. The response carries scheduled_message, and the message is not added to the channel (it arrives later through message.new).
  • A past or current scheduled_at sends the message now.
  • A scheduled send never goes through the offline queue.

Client methods

Method Endpoint
queryScheduledMessages({ filter?, sort?, limit?, next?, user_id? }) POST /scheduled_messages/query
updateScheduledMessage(id, { message?, scheduled_at? }) PATCH /scheduled_messages/{id}
deleteScheduledMessage(id) DELETE /scheduled_messages/{id}
sendScheduledMessageNow(id) POST /scheduled_messages/{id}/send_now

Channel queries: include_scheduled_messages on channel.query and client.queryChannels returns each channel's scheduled messages for the user.

Events: scheduled_message.created, .updated, .deleted, .sent and .failed in EVENT_MAP, with Event.scheduled_message typed. They are sent to the scheduling user only.

Errors: codes 114–124 in APIErrorCodes, none retryable.

Helpers

  • getScheduledMessageErrorReason(error) reads exception_fields.reason, falling back to the code.
  • isScheduledMessageNotFoundError(error) detects the plain 404 that update, delete and send-now return for a message that was already sent or deleted.
  • getScheduledMessageLimits(channel) reads the four scheduled_messages_* channel config fields, with defaults.

Types and constants: ScheduledMessageStatus, ScheduledMessageFailureReason, ScheduledMessageErrorReason, ScheduledMessageEventType, ScheduledMessageResponse and the request/response types.

2. State: client.scheduledMessages (ScheduledMessageManager)

It's built like client.reminders.

State: scheduledMessages (a map of the user's scheduled / sending / failed messages), isLoaded, isLoading, isDisabled (error 114), lastQueryError and next.

Selectors: getFromState(id), getForChannel(cid) (outside threads), getForThread(parentId) and getCountForChannel(cid). Lists are sorted by scheduled_at.

Loading: load() (up to 100) and loadMore(). Overlapping loads are cancelled so only the latest applies.

Events (opt-in through registerSubscriptions())

  • created, updated and failed upsert.
  • sent and deleted remove.
  • connection.recovered reloads.

Actions: updateScheduledMessage, deleteScheduledMessage and sendScheduledMessageNow update the store after the request. A 404 removes the item and rethrows.

Filled from elsewhere: a scheduled sendMessage response, channel queries with include_scheduled_messages, and a reset on disconnectUser().

Ordering rules

  • The newest updated_at wins.
  • Sent and canceled messages are final until disconnectUser().
  • A message stored while a snapshot request is in flight is kept.

3. Composer

Scheduling: schedule(at) runs the normal compose() pipeline with sendOptions.scheduled_at.

  • It waits for pending uploads when the composer sends with uploads in progress.
  • It clears the composer and returns the stored ScheduledMessageResponse.
  • A failure keeps the composition and adds an api:scheduled-message:create:failed notification.

What can't be scheduled: the scheduled-message composition middleware blocks slash commands, polls and shared locations with a validation:scheduled-message:blocked warning.

Editing: editScheduledMessage, updateScheduledMessage and cancelEditScheduledMessage, with the editedScheduledMessage state.

  • The payload takes every composer-owned field from the new composition.
  • It keeps the stored message's custom fields and parent_id.

Scheduling a failed message again: rescheduleFailedMessage and cancelRescheduleFailedMessage, with the failedScheduledMessage state. Scheduling it uses a new id and then deletes the failed message.

Changes made elsewhere: if the message being edited is sent, deleted or fails, the edit ends with a validation:scheduled-message:composition:ended warning. Drafts are paused during a scheduled composition.

Additive changes

  • compose({ sendOptions }) and initState({ stateOverrides }).
  • The composer state gains editedScheduledMessage and failedScheduledMessage.

Tests

  • Unit tests for:
    • each client method;
    • error codes and the reason, not-found and limits helpers;
    • include_scheduled_messages and sendMessage with scheduled_at (including the offline queue);
    • the manager: selectors, loading, events, ordering, hydration and 404 handling;
    • the composer: scheduling, blocking, editing, scheduling a failed message again, changes made elsewhere, and drafts.
  • On the combined branch: yarn types, yarn lint and yarn build pass, and yarn test passes (3370 passed, 1 skipped).
  • The stream-chat-react v14 PR (#3353) passes types, lint, all 3718 tests, translation validation and build against this branch's build.
  • Live check against an app with the feature enabled:
    • schedule, edit, send now and delete;
    • blocked commands and polls;
    • a message that failed because its channel was frozen, then scheduled again;
    • an edit that ended when another session sent the message.

End-to-end

Run against the scheduled messages test app with this branch's build: 42/42 checks pass.

  • Schedule and store: scheduling through sendMessage, with the rounded scheduled_at; nothing added to the channel; another session of the same user gets the events.
  • Past time: a past time sends the message now.
  • Thread replies: getForThread returns them, and getForChannel leaves them out.
  • Loading: load(), queryScheduledMessages filtered by cid, and hydration through include_scheduled_messages.
  • Changes: update, reschedule, send now and delete, each reaching other sessions; a second delete is a not-found error.
  • Errors: "too far" (116) and the per-channel limit (117).
  • Composer: scheduling, blocking a slash command, editing then saving with the earlier composition restored, and cancel.
  • Failure: a message in a frozen channel fails with channel_frozen while another one sends on time; rescheduleFailedMessage + schedule() creates a new message and deletes the failed one.
  • Disconnect: disconnectUser() resets the store.

The same flows in the React v14 UI (GetStream/stream-chat-react#3353) with this branch's build:

scheduled-messages-v9-v14.mp4

Backend behaviour found in end-to-end testing

  • A scheduled_at less than 60 seconds ahead is not rejected with 115 (scheduled_at_too_soon). The server moves it to the next whole minute at least 60 seconds out. In practice 115 can't be triggered with a future time.
  • queryScheduledMessages requires filter.status to be an array (the typed ScheduledMessageFilters already says so).

Changelog

  • Add scheduled messages: scheduled_at on channel.sendMessage, client.queryScheduledMessages / updateScheduledMessage / deleteScheduledMessage / sendScheduledMessageNow, include_scheduled_messages on channel queries, the scheduled_message.* events, and error codes 114–124.
  • Add getScheduledMessageErrorReason, isScheduledMessageNotFoundError and getScheduledMessageLimits.
  • Add client.scheduledMessages (ScheduledMessageManager), kept in sync from events and channel queries.
  • Add MessageComposer.schedule, editing of scheduled messages, scheduling a failed message again, and the scheduled-message composition middleware.

🤖 Generated with Claude Code

nijeesh-stream and others added 10 commits October 15, 2026 00:15
Add scheduled_at to sendMessage options, the update/delete/send_now/query
scheduled message endpoints, the scheduled_message.* events and a
ScheduledMessageManager (client.scheduledMessages) that keeps the current
user's scheduled messages in sync from the query, channel queries with
include_scheduled_messages, and events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Messages awaiting a send retry report status sending, pending: false is
accepted with scheduled_at, and the scheduled send response hydrates
mentioned_users.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ignore an upsert older (by updated_at) than the stored version, so the
  send-now response or its updated event cannot revert a failed message
  to scheduled.
- load() and channel query hydration no longer undo changes that arrived
  while they were in flight.
- Drop responses to requests made before disconnectUser(), so a new user
  never sees the previous user's scheduled messages.
- getForChannel accepts parentId: null for messages outside threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pass include_scheduled_messages through queryChannels and replace each
returned channel's entries in the scheduled messages store from its
scheduled_messages field. An absent field leaves the store untouched so
the app can fall back to queryScheduledMessages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add getScheduledMessageErrorReason(), which reads exception_fields.reason
and falls back to the error code, and register codes 123
(ScheduledMessageAlreadySentError) and 124 (ScheduledMessageCanceledError).

Type the scheduled_messages_* channel config fields and add
getScheduledMessageLimits() with DEFAULT_SCHEDULED_MESSAGE_LIMITS, so a
missing field falls back to the backend default.

Address review feedback:
- load and loadMore share one query helper and snapshot token; loadMore
  updates the store once per page
- one subscribeToEvents helper serves both event subscriptions
- order by Date.parse, tie-break with localeCompare
- ScheduledMessageSort is { scheduled_at: 1 }
- drop the ScheduledMessageId alias and the comments this PR added
Add getScheduledMessageErrorReason(), which reads exception_fields.reason
from an ErrorFromResponse and falls back to the error code. Register
codes 123 (ScheduledMessageAlreadySentError) and 124
(ScheduledMessageCanceledError), and make 122 non-retryable.

Type the scheduled_messages_* channel config fields and add
getScheduledMessageLimits() with DEFAULT_SCHEDULED_MESSAGE_LIMITS, so a
missing field falls back to the backend default.

Export ScheduledMessageStatus, ScheduledMessageFailureReason,
ScheduledMessageErrorReason and ScheduledMessageEventType as const
objects with same-named types.

Rework ScheduledMessageManager:
- set isDisabled when load fails with code 114; a successful load
  clears it
- getForChannel returns channel-level messages only; add getForThread
  and getCountForChannel
- drop the snapshot token (epoch/seq), hasMore and
  upsertResponseToState; keep the updated_at check and tombstones
- load and loadMore share one query helper; loadMore updates the store
  once per page
- one subscribeToEvents helper for both event groups
- sort by Date.parse, tie-break with localeCompare
- ScheduledMessageSort is { scheduled_at: 1 }
- name every inline type, order members private-first and drop the
  comments this PR added
…ypes.ts

Collect the client-side types (manager state, options, event, handlers,
ScheduledMessageAPIError and ScheduledMessageLimits) in the folder's own
types.ts, like notifications/ and search/. src/types.ts keeps the API
shapes. Every type is still exported from the package root.
Add the scheduled message types and const objects
(ScheduledMessageStatus, ScheduledMessageFailureReason,
ScheduledMessageErrorReason, ScheduledMessageEventType), the
scheduled_at and include_scheduled_messages options, the
scheduled_messages_* channel config fields and
APIErrorResponse.exception_fields.

Add the queryScheduledMessages, updateScheduledMessage,
deleteScheduledMessage and sendScheduledMessageNow client methods, the
scheduled_message.* events, and error codes 114-124. Only 122 is
not marked retryable either: a conflict should be reloaded, not resent.

Add getScheduledMessageErrorReason(), which reads
exception_fields.reason and falls back to the error code, and
getScheduledMessageLimits() with the backend defaults.

channel.sendMessage with scheduled_at skips the offline queue, so a
replayed scheduled send can't land in the channel as a sent message.
…4-scheduled-messages

Update the scheduled message manager and API tests to the merged API layer.
@NikolaGoranNaumovski
NikolaGoranNaumovski requested review from MartinCupela, isekovanic and oliverlaz and removed request for isekovanic October 28, 2026 08:49
Add MessageComposer.schedule(at). It waits for pending uploads, composes
the message and sends it with scheduled_at. On success it deletes the
draft and clears the composer.

Add an edit mode for scheduled messages: editScheduledMessage,
updateScheduledMessage and cancelEditScheduledMessage. While a scheduled
message is being edited, drafts are neither created nor deleted. When the
edit ends, the composer goes back to what it held before, or to the newest
draft if the draft changed during the edit.
- Disable commands while a scheduled message is edited through
  getCommandDisabledReason, and share the command lookup of
  compositionValidation as getComposedCommand.
- Check the composition before scheduling or updating: commands, polls and
  shared locations are rejected with a validation:scheduled-message:blocked
  warning carrying the reason (ScheduledMessageBlockReason). Polls skipped
  validation through the poll-only middleware before.
- Wait for uploads only when sending with pending uploads; the default
  attachments middleware already refuses while uploads run.
- Fill the composer through initState when editing a scheduled message.
- Build the update payload's server fields on RESERVED_UPDATED_MESSAGE_FIELDS.
- Let clear() and the composer subscriptions delete the draft after
  scheduling, as after a normal send.

Refs: REACT-1385
Replace the partial offline DB casts with the MockOfflineDB fixture and drop
inline comments from the scheduled message tests.

Refs: REACT-1385
… a predicate

applySnapshot only ever received two predicates: one matching every
scheduled message (load) and one matching a single channel's cid
(hydrateChannelState). Replace the predicate parameter with an optional
cid and move the check into a module-level isInSnapshotScope helper.
…manager

applySnapshot only ever received two predicates: one matching every
scheduled message (load) and one matching a single channel's cid
(hydrateChannelState). Replace the predicate parameter with an optional
cid and move the check into a module-level isInSnapshotScope helper.

Subscribe to scheduled message events with the SDK's EventHandler type
through dedicated handleUpsertEvent and handleRemoveEvent handlers.

Remove the ScheduledMessagePredicate and ScheduledMessageHandler types.
Update, delete and send-now on a scheduled message that was already sent
or deleted return a plain 404 (code 16) rather than an error reason, so
getScheduledMessageErrorReason cannot classify it. Expose a helper that
detects it, so the state layer can drop the stale item and UIs can say
"Already sent".
…r has it

updateScheduledMessage, deleteScheduledMessage and sendScheduledMessageNow
remove the item from the store when the request fails with a 404, then
rethrow. This happens when the message was sent or deleted elsewhere and
the matching event never reached this client; the item would otherwise
stay listed until the next reload.
- Move the slash command / poll / shared location check into a composition
  middleware (stream-io/message-composer-middleware/scheduled-message),
  registered before poll-only so poll compositions reach it. It runs when
  sendOptions.scheduled_at is set or a scheduled message is being edited,
  and keeps the validation:scheduled-message:blocked warning and reasons.
- compose() accepts { sendOptions }, and schedule() passes scheduled_at
  through the chain instead of adding it after composing.
- Drop getComposedCommand and restore the validation middleware.
- Move the mention and quoted-message lookups for the scheduled edit out of
  MessageComposer into module helpers.
load() had no concurrency guard: a reload on connection.recovered and a
load started by the app could resolve out of order, and the first one to
finish set isLoading to false while the other was still running.

load() now runs through withCancellation with a tag per manager. A newer
load aborts the older one: the older result is not applied, its state
changes are skipped, and it resolves to undefined. Loads run one after the
other, so the latest snapshot always wins.
- queryChannels no longer hydrates scheduled messages for channels listed in
  stateOptions.skipInitialization, matching how their other state is left
  alone
- a snapshot that leaves the store as it was no longer emits a new
  scheduledMessages map, so subscribers are not notified for nothing
SCHEDULED_MESSAGES_LOAD_LIMIT, ScheduledMessageMap and
ScheduledMessageQueryResultHandler are only used inside
ScheduledMessageManager, so they are no longer exported.
ScheduledMessageManagerState.scheduledMessages is typed as
Map<string, ScheduledMessageResponse> directly.
Nothing ended a scheduled message edit when the message was sent at its
time, sent now or deleted on another device, or failed: saving then failed
with "already sent", and the payload was built from a stale stored copy.

MessageComposer now listens for scheduled_message events while subscribed:
- sent, deleted or failed for the message being edited ends the edit,
  restores the earlier composition and adds a
  validation:scheduled-message:composition:ended warning with the reason
  (ScheduledMessageCompositionEndReason: sent, deleted, failed)
- deleted for a failed message being scheduled again ends that too
- a newer scheduled_message.updated refreshes the stored copy the update
  payload is built from
A channel query or queryChannels response with a draft (for example after
a reconnect) re-initialized the composer from that draft, which silently
dropped a scheduled message edit or a failed message being scheduled again
and left a stale restore point.

While a scheduled message is in the composer, initStateFromChannelResponse
now only updates the restore point with the newer draft, the same way the
draft.updated event does.
With pending uploads, schedule() waits for them before composing. If the
user cancelled scheduling a failed message again during that wait, the
restored draft was scheduled and the failed message was still deleted.

After the wait, schedule() now stops when an edit started or the failed
message being scheduled again changed.
- the composer-owned fields of a scheduled message are listed once; the
  draft is built from that list instead of repeating it
- getCurrentCommand in commandUtils replaces the lookup duplicated in the
  validation and scheduled message middleware
- the default middleware chain creates one CommandSearchSource and passes
  it to both middleware
initState accepts stateOverrides that are merged into the new composer
state. Editing a scheduled message or scheduling a failed one again now
initializes the composition with its scheduled message, draftId, quoted
message and show_in_channel in one update, instead of writing the
scheduled state before initState, having initState reset it, and writing
it again.
createPoll, sendLocation, schedule and updateScheduledMessage reported
failures with the same addError block. They now use one private
notifyApiError(message, type, error); the notifications are unchanged.
ScheduledMessageEditRestore and ScheduledMessageCompositionState are only
used by private MessageComposer members, so they are no longer exported.
@NikolaGoranNaumovski
NikolaGoranNaumovski force-pushed the feat/cha-5934-scheduled-messages-api branch from 1679755 to b4a398e Compare November 3, 2026 09:07
@MartinCupela
MartinCupela changed the base branch from master to release-v9 November 9, 2026 10:23
Comment thread src/types.ts
typeof ScheduledMessageStatus.Canceled | typeof ScheduledMessageStatus.Sent
>;

export const ScheduledMessageFailureReason = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could just have a union type for the strings or even just declare the union inline in ScheduledMessageResponse.failure_reason?

Comment thread src/types.ts
next?: string;
};

export const ScheduledMessageStatus = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could ScheduledMessageStatus be removed and only inline union of strings declared in ScheduledMessageResponse.status?

@NikolaGoranNaumovski NikolaGoranNaumovski changed the title feat: add scheduled messages API feat: add scheduled messages for v9 (CHA-5934) Nov 9, 2026
Comment thread src/types.ts

export type QueryScheduledMessagesOptions = Omit<Pager, 'prev'> & {
filter?: ScheduledMessageFilters;
sort?: ScheduledMessageSort | ScheduledMessageSort[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to include key sort if ScheduledMessageSort allows only asc sorting by scheduled_at?

Comment thread src/types.ts
longitude?: number;
};

export type ScheduledMessageContent = CustomMessageData & {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to declare a new type here if we have MessageBase?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be called ScheduledMessageContentResponse as in v10 it has that name?

Comment thread src/types.ts
export type ScheduledMessageCompositionEndReason =
(typeof ScheduledMessageCompositionEndReason)[keyof typeof ScheduledMessageCompositionEndReason];

export const ScheduledMessageEventType = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep this in EVENT_MAP only? Is there added value to extract them here?

Comment thread src/types.ts
export type ScheduledMessageFailureReason =
(typeof ScheduledMessageFailureReason)[keyof typeof ScheduledMessageFailureReason];

export const ScheduledMessageErrorReason = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this variable could be private to the module where getScheduledMessageErrorReason resides. Integrators will probably not need to have access to this const. I am proposing this to reduce the exported surface.

Comment thread src/types.ts
export type ScheduledMessageErrorReason =
(typeof ScheduledMessageErrorReason)[keyof typeof ScheduledMessageErrorReason];

export const ScheduledMessageBlockReason = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could reside where getScheduleBlockReason resides and be private to the module (not exported).

Comment thread src/types.ts
export type ScheduledMessageBlockReason =
(typeof ScheduledMessageBlockReason)[keyof typeof ScheduledMessageBlockReason];

export const ScheduledMessageCompositionEndReason = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be a union type declared inline?

Comment thread src/types.ts
next?: string;
};

export const ScheduledMessageStatus = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that in general, we should use different names for types and variables - I would apply that to all the types / variables declared in this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is that you never know what you import as an integrator or at least your DX is lowered by having to distinguish btw importing the type and var.

Comment thread src/types.ts
typeof ScheduledMessageStatus.Canceled | typeof ScheduledMessageStatus.Sent
>;

export const ScheduledMessageFailureReason = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this variable? Can we just have a union type and remove type ScheduledMessageFailureReason? I would apply it to all the analogies in this file / PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we may not need ScheduledMessageFailureReason neither. Again, maybe just inline union declaration is enough.

NikolaGoranNaumovski added a commit that referenced this pull request Nov 10, 2026
…onse (#2010)

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

## Summary

The `Type test` check fails on every PR into `release-v9` (for example
#2000, #2002 and #1951). There are two causes, and this PR fixes both.

### 1. `dist` is never built before the type tests

`test/typescript` loads the built bundle (`dist/cjs/index.node.js`). It
used to be built by the root `prepare` script during `yarn install`.
Since the Yarn 4 migration (#1742), `yarn install` no longer runs
`prepare` for the project itself, so the job fails with:

```
Error: Cannot find module '../../dist/cjs/index.node.js'
```

The weekly `Scheduled tests` workflow kept passing because it already
has its own `yarn build` step. This PR adds the same step to
`.github/workflows/type.yml`.

### 2. `two_way_block_effect_enabled` is missing from the app settings
response type

Once the build runs, every API call in the type test succeeds, but
compiling the generated responses fails. The API now returns
`two_way_block_effect_enabled` from `getAppSettings()`, which came with
two-way blocking (CHA-6283):

```
test/typescript/data.ts(72,79536): error TS2353: Object literal may only specify known properties,
and '"two_way_block_effect_enabled"' does not exist in type '{ ... }'
```

This PR adds it to `AppSettingsAPIResponse['app']` as an optional
boolean, the same way #1887 added
`before_message_send_hook_system_messages`. It is a type-only change
with no runtime effect. The update request type (`AppSettings`) is
unchanged.

## Changes

- `.github/workflows/type.yml`: add a `Build` step (`yarn build`) before
`yarn test-types`.
- `src/types.ts`: add `two_way_block_effect_enabled?: boolean` to
`AppSettingsAPIResponse`.

## Notes

- The `ci:` commit does not trigger a release. The `fix(types):` commit
releases a v9 patch.
- v10 (`master`) needs neither change, because its type tests were
removed in #1870.
- The type test runs against a shared live app. One run on #2000 hit
`block list with name "FWord" already exists`, most likely because
another run was using the same app at the same moment. Rerunning the
failed job passed.

## Test plan

- [x] `yarn types`
- [x] `yarn lint`
- [x] `yarn test`: 3242 passed, 1 skipped
- [x] `yarn build`, and `dist/cjs/index.node.js` loads
- [x] The same two commits are green in CI on #2000, including `Type
test`
- [ ] `Type test` passes on this PR

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants