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
249 changes: 222 additions & 27 deletions ai-docs/ai-migration-v9-to-v10.md

Large diffs are not rendered by default.

15 changes: 5 additions & 10 deletions examples/SampleApp/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,16 +241,11 @@ const App = () => {
if (!chatClient) {
return;
}
chatClient.setMessageComposerSetupFunction(({ composer }) => {
composer.updateConfig({
drafts: {
enabled: true,
},
linkPreviews: {
enabled: true,
},
});

// Only behaviour lives here. `setMessageComposerSetupFunction` is deprecated in favour of
// `config.setSetupFunction`, and the plain values this used to set (`drafts`, `linkPreviews`) moved
// to the declarative `client.config.set()` call in `useChatClient`, next to where the client is
// created. A setup function is re-run on every configuration cycle, which is what middleware needs.
chatClient.config.setSetupFunction('messageComposer', ({ composer }) => {
setupCommandUIMiddlewares(composer);

composer.textComposer.middlewareExecutor.insert({
Expand Down
4 changes: 2 additions & 2 deletions examples/SampleApp/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2857,7 +2857,7 @@ PODS:
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- stream-chat-react-native (9.7.6):
- stream-chat-react-native (9.8.0):
- hermes-engine
- RCTRequired
- RCTTypeSafety
Expand Down Expand Up @@ -3399,7 +3399,7 @@ SPEC CHECKSUMS:
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
stream-chat-react-native: ea3499916019c499ecb745be61e7ecf82f0fd999
stream-chat-react-native: 2b46e4b09bf63ab1e8ef12e8e6ff59fe785f747a
Teleport: c56b30b08bd20d10da1efb0f21c6bc50c269ee6a
Yoga: 542a30dafe5b0f5f1d9f185ea7b2a3811ac54801

Expand Down
15 changes: 15 additions & 0 deletions examples/SampleApp/src/hooks/useChatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ export const useChatClient = () => {
timeout: 6000,
// logger: (type, msg) => console.log(type, msg)
});

// Declarative configuration for the instances the SDK builds for us. Registered here, at the
// client-creation site, rather than from a component effect: some configuration is read once when
// an instance is constructed, and channels are constructed by `client.channel()` β€” which can run
// before a component effect flushes. Behaviour that cannot be expressed as a value (middleware)
// stays in the `messageComposer` setup function in `App.tsx`.
//
// `linkPreviews.enabled` is deliberately not set: it defaults to `true` in v10 and is ANDed with
// the channel type's `url_enrichment`, so the server has the final say either way.
client.config.set({
messageComposer: {
drafts: { enabled: true },
},
});

setChatClient(client);

const user = {
Expand Down
176 changes: 176 additions & 0 deletions package/src/__tests__/instanceConfiguration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React from 'react';

import { renderHook } from '@testing-library/react-native';

import { ChannelPaginator } from 'stream-chat';
import type { Channel, StreamChat } from 'stream-chat';

import { useChannelRequestHandlers } from '../components/Channel/hooks/useChannelRequestHandlers';
import { useMarkRead } from '../components/MessageList/hooks/useMarkRead';
import { ChatProvider } from '../contexts/chatContext/ChatContext';
import { initiateClientWithChannels } from '../mock-builders/api/initiateClientWithChannels';

/**
* Contract tests for the SDK's use of the LLC instance-configuration API (`client.config`).
*
* These exist because the migration to it broke things that had no coverage, and the failures were all
* silent β€” a raw server flag still reads fine, a `Readonly` config still compiles for a nested write, a
* spread copy of a channel still looks like a channel. Each test below pins one invariant that, if
* someone reverts it, produces working-looking code with the wrong behaviour.
*
* They deliberately do NOT re-test the LLC. `stream-chat` has its own suite for resolution order and
* server authority; what is asserted here is that *this* SDK reads the resolved value rather than the
* server's half, and that its own writes land where it thinks they do.
*/

const seedServerConfig = (
client: StreamChat,
channel: Channel,
config: Record<string, unknown>,
) => {
client.channelServerConfigsStore.partialNext({
configs: { ...client.channelServerConfigs, [channel.cid]: config as never },
});
};

const chatWrapper =
(client: StreamChat) =>
({ children }: { children: React.ReactNode }) => (
<ChatProvider value={{ client } as never}>{children}</ChatProvider>
);

describe('instance configuration contract', () => {
describe('resolved configuration, not the raw server flag', () => {
it('honours a client-side readEvents opt-out even when the server allows read events', async () => {
const {
channels: [channel],
client,
} = await initiateClientWithChannels();
seedServerConfig(client, channel, { name: 'messaging', read_events: true });
client.config.set({ channel: { readEvents: { enabled: false } } });

const throttledMarkRead = jest.spyOn(client.messageDeliveryReporter, 'throttledMarkRead');

const { result } = renderHook(() => useMarkRead(channel), {
wrapper: chatWrapper(client),
});
result.current();

// Reading `channel.serverConfig?.read_events` here would report `true` and report the read.
expect(channel.config.readEvents.enabled).toBe(false);
expect(throttledMarkRead).not.toHaveBeenCalled();
});

it('reports the read when both the server and the client allow it', async () => {
const {
channels: [channel],
client,
} = await initiateClientWithChannels();
seedServerConfig(client, channel, { name: 'messaging', read_events: true });

const throttledMarkRead = jest.spyOn(client.messageDeliveryReporter, 'throttledMarkRead');

const { result } = renderHook(() => useMarkRead(channel), {
wrapper: chatWrapper(client),
});
result.current();

expect(throttledMarkRead).toHaveBeenCalledWith(channel);
});

it('resolves the composer poll gate from configuration rather than the server flag alone', async () => {
const {
channels: [channel],
client,
} = await initiateClientWithChannels();
seedServerConfig(client, channel, { name: 'messaging', polls: true });
client.config.set({ messageComposer: { polls: { enabled: false } } });
// A composer only re-derives on a configuration or server-config change once it has registered
// subscriptions β€” which is what `MessageInput` does. Without this it keeps what it resolved at
// construction, and the assertion below would pass or fail for the wrong reason.
channel.messageComposer.registerSubscriptions();

// `Channel` gates its poll UI on this, so a consumer reading `serverConfig?.polls` would offer
// poll creation the composer has already refused.
expect(channel.serverConfig?.polls).toBe(true);
expect(channel.messageComposer.config.polls.enabled).toBe(false);
});

it('caps the composer text limit by the channel type max_message_length', async () => {
const {
channels: [channel],
client,
} = await initiateClientWithChannels();
seedServerConfig(client, channel, { name: 'messaging', max_message_length: 120 });
channel.messageComposer.registerSubscriptions();

// AutoCompleteInput reads `text.maxLengthOnSend` for the input's maxLength; the LLC applies the
// server value as an upper bound, so this is where the old `getConfig()?.max_message_length` went.
expect(channel.messageComposer.config.text.maxLengthOnSend).toBe(120);
});
});

describe('paginator configuration is written through updateConfig', () => {
it('persists lockItemOrder and doRequest on the channel-list paginator', async () => {
const { client } = await initiateClientWithChannels();
const paginator = new ChannelPaginator({ client, id: 'channels:test' });
const doRequest = jest.fn();

// `usePaginatedChannels` used to assign into `paginator.config` directly. That object is
// `Readonly` now, so the assignment is a compile error β€” and a runtime no-op for nested writes.
paginator.updateConfig({ lockItemOrder: true });
paginator.updateConfig({ doRequest });

expect(paginator.config.lockItemOrder).toBe(true);
expect(paginator.config.doRequest).toBe(doRequest);
});

it('keeps those writes across an unrelated client.config.set', async () => {
const { client } = await initiateClientWithChannels();
const paginator = new ChannelPaginator({ client, id: 'channels:test-2' });
paginator.updateConfig({ lockItemOrder: true });

// There is no `channelPaginator` configuration key, so nothing re-derives this paginator and an
// imperative patch survives. If a key is ever added, this breaks β€” and it should, because
// `retainPatches` is off and the patch would then be dropped on the next derivation.
client.config.set({ messageOperations: { failedSendCacheMaxSize: 42 } });

expect(paginator.config.lockItemOrder).toBe(true);
});
});

describe('channel.configState is a prototype getter', () => {
it('does not throw for a spread copy of a channel, which no longer carries it', async () => {
const {
channels: [channel],
client,
} = await initiateClientWithChannels();

// `configState` moved from an own field to a getter on `Channel.prototype`, so `{...channel}`
// silently drops it. Tests and integrator code both make such copies; this is the crash that
// took out `Channel.test.tsx` during the migration.
const spreadCopy = { ...channel } as Channel;
expect(spreadCopy.configState).toBeUndefined();

expect(() =>
renderHook(() => useChannelRequestHandlers({ channel: spreadCopy }), {
wrapper: chatWrapper(client),
}),
).not.toThrow();
});
});

describe('the mock builder seeds server configuration where the LLC reads it', () => {
it('makes serverConfig readable and folds it into the resolved config', async () => {
const {
channels: [channel],
} = await initiateClientWithChannels();

// `getConfig()` is gone and `serverConfig` is a getter over the client's cid-keyed store, so
// `jest.spyOn(channel, 'getConfig')` cannot stand in for it. Every test that depends on server
// configuration depends on this write working.
expect(channel.serverConfig).toBeDefined();
expect(channel.config.readEvents.enabled).toBe(channel.serverConfig?.read_events !== false);
});
});
});
Loading
Loading