Skip to content
Open
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
3e898be
feat: support scheduled messages (CHA-5934)
nijeesh-stream Oct 14, 2026
cac2635
docs: align scheduled message docs with backend changes
nijeesh-stream Oct 15, 2026
8296921
fix: keep scheduled message store ordered across responses and events
nijeesh-stream Oct 15, 2026
5e783e0
feat: hydrate scheduled messages from queryChannels
nijeesh-stream Oct 20, 2026
d0a4400
Merge branch 'master' into feat/cha-5934-scheduled-messages
NikolaGoranNaumovski Oct 27, 2026
131e55f
feat: add scheduled message error reasons and channel limits
NikolaGoranNaumovski Oct 27, 2026
dddf4d9
feat: add scheduled message error reasons, limits and selectors
NikolaGoranNaumovski Oct 27, 2026
1828064
refactor: move the scheduled message SDK types to scheduledMessages/t…
NikolaGoranNaumovski Oct 27, 2026
c32c7c6
Merge branch 'feat/cha-5934-scheduled-messages-api' into feat/cha-593…
NikolaGoranNaumovski Oct 27, 2026
82ef9a3
refactor(scheduledMessages): scope snapshots by channel id instead of…
NikolaGoranNaumovski Oct 29, 2026
7703188
refactor(scheduledMessages): drop single-use callback types from the …
NikolaGoranNaumovski Oct 29, 2026
0ff6f3f
Merge branch 'feat/cha-5934-scheduled-messages-api' into feat/cha-593…
NikolaGoranNaumovski Nov 2, 2026
3c9fc11
fix: drop a scheduled message from the store when the server no longe…
NikolaGoranNaumovski Nov 2, 2026
e9cee89
Merge branch 'feat/cha-5934-scheduled-messages-api' into feat/cha-593…
NikolaGoranNaumovski Nov 2, 2026
5739197
test: drop a leftover comment from the scheduled message manager tests
NikolaGoranNaumovski Nov 2, 2026
5521753
Merge branch 'feat/cha-5934-scheduled-messages-api' into feat/cha-593…
NikolaGoranNaumovski Nov 2, 2026
aa96957
fix: keep scheduled messages stored while a snapshot is loading
NikolaGoranNaumovski Nov 2, 2026
b459014
fix: let only the latest scheduled message load update the store
NikolaGoranNaumovski Nov 2, 2026
04d42e3
refactor: skip unchanged scheduled message snapshots
NikolaGoranNaumovski Nov 2, 2026
070e050
refactor: keep internal scheduled message manager types private
NikolaGoranNaumovski Nov 2, 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
24 changes: 19 additions & 5 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,24 @@ export class Channel {
*
* @return {Promise<SendMessageAPIResponse>} The Server Response
*/
async _sendMessage(rawMessage: Message, options?: SendMessageOptions) {
async _sendMessage(rawMessage: Message, options: SendMessageOptions = {}) {
const client = this.getClient();
const message = sanitizeOutgoingAttachments({
client,
message: rawMessage,
});

return await client.post<SendMessageAPIResponse>(this._channelURL() + '/message', {
message,
...options,
});
const response = await client.post<SendMessageAPIResponse>(
`${this._channelURL()}/message`,
{
message,
...options,
Comment thread
NikolaGoranNaumovski marked this conversation as resolved.
Resolved
},
);
if (response.scheduled_message) {
client.scheduledMessages.upsertToState(response.scheduled_message);
}
return response;
}

async sendMessage(message: Message, options?: SendMessageOptions) {
Expand Down Expand Up @@ -1575,6 +1582,8 @@ export class Channel {
// Make sure we wait for the connect promise if there is a pending one
await this.getClient().wsPromise;

const scheduledMessagesVersion = this.getClient().scheduledMessages.version;

const createdById =
options.created_by?.id ??
options.created_by_id ??
Expand Down Expand Up @@ -1660,6 +1669,11 @@ export class Channel {

this.getClient().polls.hydratePollCache(state.messages, true);
this.getClient().reminders.hydrateState(state.messages);
this.getClient().scheduledMessages.hydrateChannelState(
this.cid,
state.scheduled_messages,
scheduledMessagesVersion,
);

this.messageComposer.initStateFromChannelResponse(state);

Expand Down
19 changes: 17 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ import { ChannelManager } from './channel_manager';
import { MessageDeliveryReporter } from './messageDelivery';
import { NotificationManager } from './notifications';
import { ReminderManager } from './reminders';
import { ScheduledMessageManager } from './scheduledMessages';
import { StateStore } from './store';
import type { MessageComposer } from './messageComposer';
import type { AbstractOfflineDB } from './offline-support';
Expand Down Expand Up @@ -354,6 +355,7 @@ export class StreamChat {
offlineDb?: AbstractOfflineDB;
notifications: NotificationManager;
reminders: ReminderManager;
scheduledMessages: ScheduledMessageManager;
anonymous: boolean;
persistUserOnConnectionFailure?: boolean;
axiosInstance: AxiosInstance;
Expand Down Expand Up @@ -590,6 +592,7 @@ export class StreamChat {
this.threads = new ThreadManager({ client: this });
this.polls = new PollManager({ client: this });
this.reminders = new ReminderManager({ client: this });
this.scheduledMessages = new ScheduledMessageManager({ client: this });
this.messageDeliveryReporter = new MessageDeliveryReporter({ client: this });
this.messageComposerCache = new FixedSizeQueueCache<string, MessageComposer>(64);
}
Expand Down Expand Up @@ -1139,6 +1142,7 @@ export class StreamChat {
this.state = new ClientState({ client: this });
// reset thread manager
this.threads.resetState();
this.scheduledMessages.resetState();
this.uploadManager.reset();
this.messageComposerCache.clear();

Expand Down Expand Up @@ -2264,14 +2268,15 @@ export class StreamChat {
options: ChannelOptions = {},
stateOptions: ChannelStateOptions = {},
): Promise<Channel[] | QueryChannelsResponseWithChannels> {
const scheduledMessagesVersion = this.scheduledMessages.version;
const queryChannelsResponse = await this.queryChannelsRequestWithResponse(
filterConditions,
sort,
options,
// stateOptions is never serialized, so it carries the abort signal
{ signal: stateOptions.signal },
);
const channels = queryChannelsResponse.channels;
const channels = queryChannelsResponse.channels ?? [];

this.dispatchEvent({
type: 'channels.queried',
Expand All @@ -2280,13 +2285,23 @@ export class StreamChat {
isLatestMessageSet: true,
},
});
if (channels?.length && this.offlineDb?.upsertChannels) {
if (channels.length && this.offlineDb?.upsertChannels) {
await this.offlineDb.upsertChannels({
channels,
isLatestMessagesSet: true,
});
}

const { skipInitialization } = stateOptions;
for (const channelState of channels) {
if (skipInitialization?.includes(channelState.channel.id)) continue;

this.scheduledMessages.hydrateChannelState(
channelState.channel.cid,
channelState.scheduled_messages,
scheduledMessagesVersion,
);
}
const hydratedChannels = this.hydrateActiveChannels(channels, stateOptions, options);

if (stateOptions.withResponse) {
Expand Down
Loading
Loading