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
2 changes: 1 addition & 1 deletion examples/tutorial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"emoji-mart": "^5.6.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "^9.50.2",
"stream-chat": "10.0.0-rc.1",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/src/3-channel-list/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const user: User = {
image: `https://getstream.io/random_png/?name=${userName}`,
};

const sort: ChannelSort = { last_message_at: -1 };
const sort: ChannelSort = [{ direction: -1, field: 'last_message_at' }];
const filters: ChannelFilters = {
type: 'messaging',
members: { $in: [userId] },
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"modern-normalize": "^3.0.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "^9.50.2",
"stream-chat": "10.0.0-rc.1",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
Expand Down
15 changes: 12 additions & 3 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'react';
import type {
ChannelFilters,
ChannelPaginatorRequestOptions,
ChannelSort,
LocalMessage,
TextComposerMiddleware,
Expand Down Expand Up @@ -119,13 +120,19 @@ if (!apiKey) {
throw new Error('VITE_STREAM_API_KEY is not defined');
}

const options: ChannelOptions = {
// v10: the paginator takes query options as `requestOptions`, which omits `offset`/`limit` —
// page size is a paginator concern and is passed via `paginatorOptions.pageSize` instead.
const CHANNELS_PAGE_SIZE = 10;

const requestOptions: ChannelPaginatorRequestOptions = {
presence: true,
state: true,
limit: 10,
};

const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
const sort: ChannelSort = [
{ direction: -1, field: 'last_message_at' },
{ direction: -1, field: 'updated_at' },
];

// @ts-expect-error ai_generated isn't on LocalMessage's public type yet
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;
Expand Down Expand Up @@ -378,6 +385,8 @@ const App = () => {
client: chatClient,
filters: { ...filters, archived: false, muted: false },
id: 'channels:default',
paginatorOptions: { pageSize: CHANNELS_PAGE_SIZE },
requestOptions,
sort,
});
const archived = new ChannelPaginator({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type {
Channel,
ChannelMemberResponse,
Event,
MessageResponseBase,
MessageResponse,
ReactionResponse,
StreamChat,
Event as StreamChatEvent,
UserResponse,
} from 'stream-chat';

Expand All @@ -16,13 +16,16 @@ import {
import type { SimulationState, SimulationUser } from './types';

type UnknownRecord = Record<string, unknown>;
type EventPayload = Omit<
Partial<Event>,
'channel' | 'member' | 'message' | 'reaction' | 'user'
> & {
/**
* The simulator assembles arbitrary WS payloads as loose JSON, so this is intentionally an open
* record rather than being derived from `Event`. In v10 `Event` is a discriminated union, and
* `Omit<Partial<Event>, …>` distributes over it — which drops the fields common to every member
* (`created_at`, `channel_member_count`, `message_id`, …) and makes them unassignable here.
*/
type EventPayload = UnknownRecord & {
channel?: Partial<WebSocketEventTemplateContext['channel']>;
member?: ChannelMemberResponse;
message?: Partial<MessageResponseBase>;
message?: Partial<MessageResponse>;
reaction?: ReactionResponse;
user?: UserResponse;
};
Expand Down Expand Up @@ -86,15 +89,17 @@ const buildReactionState = ({
}: {
reaction: ReactionResponse;
}): Pick<
MessageResponseBase,
MessageResponse,
'latest_reactions' | 'reaction_counts' | 'reaction_groups' | 'reaction_scores'
> => {
const reactionType = getId(reaction.type) ?? 'love';
const reactionScore =
typeof reaction.score === 'number' && Number.isFinite(reaction.score)
? reaction.score
: 1;
const reactionTimestamp = getId(reaction.created_at) ?? new Date().toISOString();
const reactionTimestamp = reaction.created_at
? new Date(reaction.created_at)
: new Date();

return {
latest_reactions: [reaction],
Expand Down Expand Up @@ -330,7 +335,8 @@ export const createInitialSimulationState = ({
});
});

const channelMessages = channel?.state.messages ?? [];
// Messages are owned by the LLC paginator; `channel.state.messages` was removed in v15.
const channelMessages = channel?.messagePaginator.state.getLatestValue().items ?? [];

channelMessages.forEach((message) => {
const messageObject = asJsonObject(message);
Expand Down Expand Up @@ -383,12 +389,12 @@ export const buildFreshWebSocketEventPayload = ({
created_at: freshContext.createdAt,
message: {
...baseMessage,
created_at: freshContext.createdAt,
created_at: new Date(freshContext.createdAt),
html: `<p>${text}</p>\n`,
id: messageId,
member,
text,
updated_at: freshContext.createdAt,
updated_at: new Date(freshContext.createdAt),
user,
},
message_id: messageId,
Expand All @@ -406,10 +412,14 @@ export const buildFreshWebSocketEventPayload = ({
const reactionScore = eventType === 'reaction.updated' ? 2 : 1;
const reaction = {
...baseReaction,
created_at: freshContext.createdAt,
// `dispatchEvent` receives an already-parsed `Event`, so timestamps are `Date`s here
// (only the raw wire format uses ISO strings).
created_at: new Date(freshContext.createdAt),
// v10 requires `custom` on reaction responses.
custom: {},
message_id: messageId,
type: reactionType,
updated_at: freshContext.createdAt,
updated_at: new Date(freshContext.createdAt),
user,
user_id: user.id,
score: reactionScore,
Expand All @@ -427,7 +437,7 @@ export const buildFreshWebSocketEventPayload = ({
...baseMessage,
id: messageId,
member,
updated_at: freshContext.createdAt,
updated_at: new Date(freshContext.createdAt),
user,
...buildReactionState({ reaction }),
},
Expand Down Expand Up @@ -462,7 +472,7 @@ export const buildFreshWebSocketEventPayload = ({
...baseMessage,
id: messageId,
member,
updated_at: freshContext.createdAt,
updated_at: new Date(freshContext.createdAt),
user,
},
user,
Expand Down Expand Up @@ -494,7 +504,7 @@ export const trackSimulationStateFromPayload = ({
simulationState,
templateContext,
}: {
payload: Event;
payload: EventPayload;
simulationState: SimulationState;
templateContext: WebSocketEventTemplateContext;
}) => {
Expand Down Expand Up @@ -555,12 +565,15 @@ export const emitWebSocketEventPayload = ({
simulationState: SimulationState;
templateContext: WebSocketEventTemplateContext;
}) => {
const emittedPayload = {
const emittedPayload: EventPayload = {
...payload,
type: eventType,
} as Event;
};

client.dispatchEvent(emittedPayload);
// Assert only at the LLC boundary: `Event` is a discriminated union that a generic payload
// builder cannot satisfy structurally. (`StreamChatEvent` is aliased on import because the
// bare name `Event` would resolve to the DOM global.)
client.dispatchEvent(emittedPayload as StreamChatEvent);

trackSimulationStateFromPayload({
payload: emittedPayload,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,19 @@ type BuildChannelSeedContext = Omit<WebSocketEventTemplateContext, 'channel'> &
channel: Partial<DebugChannelResponse>;
};

const createFallbackUser = (id: string, createdAt: string): DebugUserResponse => ({
const createFallbackUser = (id: string, createdAt: Date): DebugUserResponse => ({
banned: false,
blocked_user_ids: [],
created_at: createdAt,
// v10 requires `custom` on user responses.
custom: {},
id,
invisible: false,
language: '',
last_active: createdAt,
name: id,
online: true,
role: 'user',
shadow_banned: false,
teams: [],
updated_at: createdAt,
});
Expand All @@ -140,13 +141,16 @@ const getUserId = (user: DebugUserResponse) =>
typeof user.id === 'string' ? user.id : 'debug-user';

const createMember = (user: DebugUserResponse): ChannelMemberResponse => {
const createdAt =
typeof user.created_at === 'string' ? user.created_at : new Date().toISOString();
// `user.created_at` is typed as `Date` in v10, but this builder also receives raw event/JSON
// payloads where it may still be a string — normalize either form to a `Date`.
const createdAt = user.created_at ? new Date(user.created_at) : new Date();

return {
banned: false,
channel_role: 'channel_member',
created_at: createdAt,
// v10 requires `custom` on member responses.
custom: {},
notifications_muted: false,
role: 'member',
shadow_banned: false,
Expand Down Expand Up @@ -174,12 +178,16 @@ const buildChannel = (
context: BuildChannelSeedContext,
overrides: JsonObject = {},
): DebugChannelResponse => {
const createdAt = context.createdAt;
// `context.createdAt` stays an ISO string (event payloads carry strings), but the
// `ChannelResponse`/config timestamps below are typed as `Date` in v10.
const createdAt = new Date(context.createdAt);

return {
cid: context.cid,
config: {
automod: 'disabled',
// v10 requires `automod_behavior` alongside `automod`.
automod_behavior: 'flag',
blocklist_behavior: 'flag',
commands: [
{
Expand Down Expand Up @@ -220,7 +228,6 @@ const buildChannel = (
delivery_events: true,
mark_messages_pending: false,
max_message_length: 5000,
message_retention: 'infinite',
mutes: true,
name: context.channelType,
polls: true,
Expand All @@ -242,6 +249,9 @@ const buildChannel = (
},
created_at: createdAt,
created_by: context.actor,
// v10 requires `custom` on channel responses; the demo's `name` is a custom field, but
// `DebugChannelResponse` keeps it top-level for the simulator's own payload shaping.
custom: {},
disabled: false,
frozen: false,
hidden: false,
Expand Down Expand Up @@ -758,11 +768,14 @@ export const createWebSocketEventTemplateContext = ({
channel?: Channel;
client: StreamChat;
}): WebSocketEventTemplateContext => {
const createdAt = new Date().toISOString();
// Kept as an ISO string on the context (event payloads carry string timestamps), with the `Date`
// form on hand for the response-shaped builders that v10 types as `Date`.
const createdAtDate = new Date();
const createdAt = createdAtDate.toISOString();
const actorUser =
client.user && typeof client.user === 'object'
? ({ ...client.user } as DebugUserResponse)
: createFallbackUser('debug-user', createdAt);
: createFallbackUser('debug-user', createdAtDate);
const actorId = typeof actorUser.id === 'string' ? actorUser.id : 'debug-user';

const members = channel ? Object.values(channel.state.members) : [];
Expand All @@ -779,7 +792,7 @@ export const createWebSocketEventTemplateContext = ({
const otherUser =
otherMemberFromChannel?.user && typeof otherMemberFromChannel.user === 'object'
? ({ ...otherMemberFromChannel.user } as DebugUserResponse)
: createFallbackUser('debug-other-user', createdAt);
: createFallbackUser('debug-other-user', createdAtDate);
const otherMember = otherMemberFromChannel
? ({ ...otherMemberFromChannel } as ChannelMemberResponse)
: createMember(otherUser);
Expand Down
Loading