diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx
index bdf11f91c..7abfd4087 100644
--- a/examples/vite/src/App.tsx
+++ b/examples/vite/src/App.tsx
@@ -68,6 +68,7 @@ import {
SegmentedReactionsList,
} from './CustomMessageUi';
import { ConfigurableMessageActions } from './CustomMessageActions';
+import { InlineEditableMessage } from './InlineEditMessage';
import { SidebarToggle } from './Sidebar/SidebarToggle.tsx';
import { CommandModeAttachmentSelector } from './CommandModeAttachmentSelector.tsx';
@@ -424,6 +425,7 @@ const App = () => {
HeaderStartContent: SidebarToggle,
MessageActions: ConfigurableMessageActions,
AttachmentSelector: CommandModeAttachmentSelector,
+ Message: InlineEditableMessage,
...messageUiOverrides,
}}
>
diff --git a/examples/vite/src/AppSettings/state.ts b/examples/vite/src/AppSettings/state.ts
index 9cd77495d..7281da3b1 100644
--- a/examples/vite/src/AppSettings/state.ts
+++ b/examples/vite/src/AppSettings/state.ts
@@ -25,6 +25,7 @@ export type MessageActionsSettingsState = {
delete: {
enableOptionConfiguration: boolean;
};
+ inlineEdit: boolean;
markOwnUnread: boolean;
viewMessageInfo: boolean;
};
@@ -121,6 +122,7 @@ const defaultAppSettingsState: AppSettingsState = {
delete: {
enableOptionConfiguration: false,
},
+ inlineEdit: false,
markOwnUnread: false,
viewMessageInfo: false,
},
diff --git a/examples/vite/src/AppSettings/tabs/MessageActions/MessageActionsTab.tsx b/examples/vite/src/AppSettings/tabs/MessageActions/MessageActionsTab.tsx
index 8f76d0876..3d0d8bb20 100644
--- a/examples/vite/src/AppSettings/tabs/MessageActions/MessageActionsTab.tsx
+++ b/examples/vite/src/AppSettings/tabs/MessageActions/MessageActionsTab.tsx
@@ -90,6 +90,33 @@ export const MessageActionsTab = ({ close }: MessageActionsTabProps) => {
title='Show JSON viewer action in the message actions menu'
/>
+
+
+
+ Enable inline message editing
+
+
+ appSettingsStore.partialNext({
+ messageActions: {
+ ...messageActions,
+ customMessageActions: {
+ ...customMessageActions,
+ inlineEdit: event.target.checked,
+ },
+ },
+ })
+ }
+ title='Add an "Edit inline" action that swaps the message bubble for a MessageComposer in place'
+ />
+
+ Adds an “Edit inline” action that replaces the
+ message with a MessageComposer scoped to that message via
+ MessageComposerControllerProvider.
+
+
);
diff --git a/examples/vite/src/InlineEditMessage/InlineEditMessage.scss b/examples/vite/src/InlineEditMessage/InlineEditMessage.scss
new file mode 100644
index 000000000..89d7bd928
--- /dev/null
+++ b/examples/vite/src/InlineEditMessage/InlineEditMessage.scss
@@ -0,0 +1,22 @@
+.app__inline-edit-message {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 0.5rem 0;
+ width: 100%;
+}
+
+.app__inline-edit-message__cancel {
+ align-self: flex-end;
+ background: transparent;
+ border: 1px solid var(--str-chat__secondary-surface-color, #dbdde1);
+ border-radius: 999px;
+ color: var(--str-chat__text-color, inherit);
+ cursor: pointer;
+ font-size: 0.85rem;
+ padding: 0.25rem 0.75rem;
+
+ &:hover {
+ background: var(--str-chat__secondary-surface-color, #f7f7f8);
+ }
+}
diff --git a/examples/vite/src/InlineEditMessage/InlineEditMessage.tsx b/examples/vite/src/InlineEditMessage/InlineEditMessage.tsx
new file mode 100644
index 000000000..615324075
--- /dev/null
+++ b/examples/vite/src/InlineEditMessage/InlineEditMessage.tsx
@@ -0,0 +1,183 @@
+import {
+ type ComponentProps,
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { MessageComposer as MessageComposerController } from 'stream-chat';
+import type { MessageComposerState } from 'stream-chat';
+import { useChannelStateContext } from 'stream-chat-react';
+import {
+ ContextMenuButton,
+ defaultMessageActionSet,
+ MessageUI as DefaultMessageUI,
+ IconEdit,
+ MessageActions,
+ type MessageActionSetItem,
+ MessageComposer,
+ MessageComposerControllerProvider,
+ type MessageUIComponentProps,
+ useChatContext,
+ useComponentContext,
+ useContextMenuContext,
+ useMessageContext,
+ useStateStore,
+ useTranslationContext,
+ WithComponents,
+} from 'stream-chat-react';
+
+import { useAppSettingsSelector } from '../AppSettings';
+
+type InlineEditContextValue = {
+ isEditing: boolean;
+ startEditing: () => void;
+ stopEditing: () => void;
+};
+
+const InlineEditContext = createContext(undefined);
+
+const useInlineEditContext = () => {
+ const value = useContext(InlineEditContext);
+ if (!value) {
+ throw new Error('useInlineEditContext must be used within an InlineEditableMessage');
+ }
+ return value;
+};
+
+const InlineEditAction = () => {
+ const { closeMenu } = useContextMenuContext();
+ const { startEditing } = useInlineEditContext();
+ const { t } = useTranslationContext();
+
+ return (
+ {
+ startEditing();
+ closeMenu();
+ }}
+ >
+ {t('Edit inline')}
+
+ );
+};
+
+const inlineEditActionSetItem: MessageActionSetItem = {
+ Component: InlineEditAction,
+ placement: 'dropdown',
+ type: 'editInline',
+};
+
+const insertInlineEditAction = (
+ actionSet: MessageActionSetItem[],
+): MessageActionSetItem[] => {
+ const editIndex = actionSet.findIndex((item) => 'type' in item && item.type === 'edit');
+
+ if (editIndex < 0) return [...actionSet, inlineEditActionSetItem];
+
+ return [
+ ...actionSet.slice(0, editIndex),
+ inlineEditActionSetItem,
+ ...actionSet.slice(editIndex),
+ ];
+};
+
+const InlineEditComposer = ({ onExit }: { onExit: () => void }) => {
+ const { t } = useTranslationContext();
+
+ return (
+
+
+
+
+ );
+};
+
+const selector = (state: MessageComposerState) => ({
+ editing: state.editedMessage != null,
+});
+
+export const InlineEditableMessage = (props: MessageUIComponentProps) => {
+ const { client } = useChatContext();
+ const { channel } = useChannelStateContext();
+ const { message } = useMessageContext();
+ const inlineEditEnabled = useAppSettingsSelector(
+ (state) => state.messageActions.customMessageActions,
+ ).inlineEdit;
+
+ const { MessageActions: OuterMessageActions = MessageActions } = useComponentContext();
+
+ const [editingComposer] = useState(
+ () =>
+ new MessageComposerController({
+ compositionContext: channel,
+ client,
+ config: { drafts: { enabled: false } },
+ }),
+ );
+
+ const { editing } = useStateStore(editingComposer.state, selector);
+
+ // If the setting is turned off mid-edit, abandon the in-progress edit so the
+ // message doesn't stay stuck in composer view with no way to submit it.
+ useEffect(() => {
+ if (!inlineEditEnabled && editing) editingComposer.clear();
+ }, [editing, editingComposer, inlineEditEnabled]);
+
+ const startEditing = useCallback(() => {
+ editingComposer.initState({ composition: message });
+ }, [editingComposer, message]);
+ const stopEditing = useCallback(() => {
+ editingComposer.clear();
+ }, [editingComposer]);
+
+ const contextValue = useMemo(
+ () => ({ isEditing: editing, startEditing, stopEditing }),
+ [editing, startEditing, stopEditing],
+ );
+
+ const MessageActionsWithInlineEdit = useMemo(() => {
+ const Component = (actionsProps: ComponentProps) => {
+ const messageActionSet = useMemo(
+ () =>
+ insertInlineEditAction(
+ actionsProps.messageActionSet ?? defaultMessageActionSet,
+ ),
+ [actionsProps.messageActionSet],
+ );
+
+ return (
+
+ );
+ };
+ Component.displayName = 'MessageActionsWithInlineEdit';
+ return Component;
+ }, [OuterMessageActions]);
+
+ if (!inlineEditEnabled) {
+ return ;
+ }
+
+ if (editing) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/examples/vite/src/InlineEditMessage/index.ts b/examples/vite/src/InlineEditMessage/index.ts
new file mode 100644
index 000000000..32f21bf26
--- /dev/null
+++ b/examples/vite/src/InlineEditMessage/index.ts
@@ -0,0 +1 @@
+export { InlineEditableMessage } from './InlineEditMessage';
diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss
index 922a276e3..a7bbf5723 100644
--- a/examples/vite/src/index.scss
+++ b/examples/vite/src/index.scss
@@ -9,6 +9,7 @@
@import url('./AppSettings/AppSettings.scss') layer(stream-app-overrides);
@import url('./CustomMessageActions/CustomMessageActions.scss')
layer(stream-app-overrides);
+@import url('./InlineEditMessage/InlineEditMessage.scss') layer(stream-app-overrides);
@import url('./SystemNotification/SystemNotification.scss') layer(stream-app-overrides);
@import url('./AccessibilityNavigation/ReturnToSkipNavigation.scss')
layer(stream-app-overrides);
diff --git a/src/components/MessageComposer/MessageComposer.tsx b/src/components/MessageComposer/MessageComposer.tsx
index 165239fe0..b0e1df6a0 100644
--- a/src/components/MessageComposer/MessageComposer.tsx
+++ b/src/components/MessageComposer/MessageComposer.tsx
@@ -1,5 +1,5 @@
import type { PropsWithChildren } from 'react';
-import React, { useEffect } from 'react';
+import React, { useContext, useEffect } from 'react';
import { MessageComposerUI as DefaultMessageComposerUI } from './MessageComposerUI';
import { useMessageComposerController } from './hooks';
@@ -11,11 +11,34 @@ import { MessageComposerContextProvider } from '../../context/MessageComposerCon
import { DialogManagerProvider } from '../../context';
import { useStableId } from '../UtilityComponents/useStableId';
-import type { LocalMessage, Message, SendMessageOptions } from 'stream-chat';
+import type {
+ LocalMessage,
+ Message,
+ MessageComposer as MessageComposerController,
+ SendMessageOptions,
+} from 'stream-chat';
import type { CustomAudioRecordingConfig } from '../MediaRecorder';
import { useRegisterDropHandlers } from './WithDragAndDropUpload';
+const MessageComposerControllerContext = React.createContext<
+ MessageComposerController | undefined
+>(undefined);
+
+export const MessageComposerControllerProvider = ({
+ children,
+ messageComposerController,
+}: PropsWithChildren<{
+ messageComposerController?: MessageComposerController;
+}>) => (
+
+ {children}
+
+);
+
+export const useMessageComposerControllerContext = () =>
+ useContext(MessageComposerControllerContext);
+
export type EmojiSearchIndexResult = {
id: string;
name: string;
@@ -79,6 +102,10 @@ export type MessageComposerProps = {
* ```
*/
shouldSubmit?: (event: React.KeyboardEvent) => boolean;
+ /**
+ * When set to `true` disables clearing established state of the MessageComposerController upon component unmount.
+ */
+ preventClearingOnUnmount?: boolean;
};
const MessageComposerProvider = (props: PropsWithChildren) => {
@@ -99,9 +126,15 @@ const MessageComposerProvider = (props: PropsWithChildren)
// for a disconnected channel
if (messageComposer.channel.disconnected) return;
- messageComposer.createDraft().finally(() => messageComposer.clear());
+ const promise = messageComposer.config.drafts.enabled
+ ? messageComposer.createDraft().catch(console.error)
+ : Promise.resolve();
+
+ if (props.preventClearingOnUnmount) return;
+
+ promise.finally(() => messageComposer.clear());
},
- [messageComposer],
+ [messageComposer, props.preventClearingOnUnmount],
);
useEffect(() => {
diff --git a/src/components/MessageComposer/hooks/__tests__/useMessageComposerController.test.tsx b/src/components/MessageComposer/hooks/__tests__/useMessageComposerController.test.tsx
new file mode 100644
index 000000000..f9fef9876
--- /dev/null
+++ b/src/components/MessageComposer/hooks/__tests__/useMessageComposerController.test.tsx
@@ -0,0 +1,281 @@
+import React from 'react';
+import type { PropsWithChildren } from 'react';
+import { act, renderHook, type RenderHookResult } from '@testing-library/react';
+import { fromPartial } from '@total-typescript/shoehorn';
+import {
+ type Channel,
+ type LocalMessage,
+ MessageComposer as MessageComposerController,
+ type StreamChat,
+ type Thread,
+} from 'stream-chat';
+
+import { useMessageComposerController } from '../useMessageComposerController';
+import { Chat } from '../../../Chat';
+import { Channel as ChannelComponent } from '../../../Channel';
+import { LegacyThreadContext } from '../../../Thread/LegacyThreadContext';
+import { ThreadContext } from '../../../Threads';
+import { MessageComposerControllerProvider } from '../../MessageComposer';
+import {
+ generateMessage,
+ getOrCreateChannelApi,
+ getTestClientWithUser,
+ useMockedApis,
+} from '../../../../mock-builders';
+import { generateChannel } from '../../../../mock-builders/generator';
+
+const buildStandaloneComposer = (
+ client: StreamChat,
+ channel: Channel,
+): MessageComposerController =>
+ new MessageComposerController({ client, compositionContext: channel });
+
+const buildStubThreadInstance = (composer: MessageComposerController): Thread =>
+ fromPartial({ messageComposer: composer });
+
+type SetupOptions = {
+ channel: Channel;
+ client: StreamChat;
+ legacyThread?: LocalMessage;
+ overrideComposer?: MessageComposerController;
+ threadInstance?: Thread;
+};
+
+const setup = async ({
+ channel,
+ client,
+ legacyThread,
+ overrideComposer,
+ threadInstance,
+}: SetupOptions) => {
+ const wrapper = ({ children }: PropsWithChildren) => {
+ let content: React.ReactNode = children;
+
+ if (overrideComposer !== undefined) {
+ content = (
+
+ {content}
+
+ );
+ }
+
+ if (threadInstance !== undefined) {
+ content = (
+ {content}
+ );
+ }
+
+ if (legacyThread !== undefined) {
+ content = (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+ };
+
+ let result!: RenderHookResult;
+ await act(() => {
+ result = renderHook(() => useMessageComposerController(), { wrapper });
+ });
+ return result;
+};
+
+describe('useMessageComposerController', () => {
+ let client: StreamChat;
+ let channel: Channel;
+
+ beforeEach(async () => {
+ client = await getTestClientWithUser({ id: 'test-user' });
+ const mockedChannelData = generateChannel();
+ useMockedApis(client, [getOrCreateChannelApi(mockedChannelData)]);
+ channel = client.channel('messaging', mockedChannelData.channel.id);
+ await channel.watch();
+ });
+
+ describe('retrieval hierarchy', () => {
+ it('returns channel.messageComposer when no override, thread instance, or legacy thread is present', async () => {
+ const { result } = await setup({ channel, client });
+ expect(result.current).toBe(channel.messageComposer);
+ });
+
+ it('returns the override composer when MessageComposerControllerProvider supplies one', async () => {
+ const overrideComposer = buildStandaloneComposer(client, channel);
+ const { result } = await setup({ channel, client, overrideComposer });
+ expect(result.current).toBe(overrideComposer);
+ expect(result.current).not.toBe(channel.messageComposer);
+ });
+
+ it('override composer takes precedence over a thread instance', async () => {
+ const overrideComposer = buildStandaloneComposer(client, channel);
+ const threadComposer = buildStandaloneComposer(client, channel);
+ const threadInstance = buildStubThreadInstance(threadComposer);
+
+ const { result } = await setup({
+ channel,
+ client,
+ overrideComposer,
+ threadInstance,
+ });
+ expect(result.current).toBe(overrideComposer);
+ });
+
+ it('override composer takes precedence over a legacy thread parent message', async () => {
+ const overrideComposer = buildStandaloneComposer(client, channel);
+ const legacyThread = generateMessage({
+ cid: channel.cid,
+ }) as unknown as LocalMessage;
+
+ const { result } = await setup({
+ channel,
+ client,
+ legacyThread,
+ overrideComposer,
+ });
+ expect(result.current).toBe(overrideComposer);
+ });
+
+ it('returns threadInstance.messageComposer when a thread instance is provided', async () => {
+ const threadComposer = buildStandaloneComposer(client, channel);
+ const threadInstance = buildStubThreadInstance(threadComposer);
+
+ const { result } = await setup({ channel, client, threadInstance });
+ expect(result.current).toBe(threadComposer);
+ expect(result.current).not.toBe(channel.messageComposer);
+ });
+
+ it('thread instance takes precedence over a legacy thread parent message', async () => {
+ const threadComposer = buildStandaloneComposer(client, channel);
+ const threadInstance = buildStubThreadInstance(threadComposer);
+ const legacyThread = generateMessage({
+ cid: channel.cid,
+ }) as unknown as LocalMessage;
+
+ const { result } = await setup({
+ channel,
+ client,
+ legacyThread,
+ threadInstance,
+ });
+ expect(result.current).toBe(threadComposer);
+ });
+
+ it('legacy thread parent takes precedence over the channel composer', async () => {
+ const legacyThread = generateMessage({
+ cid: channel.cid,
+ }) as unknown as LocalMessage;
+ const { result } = await setup({ channel, client, legacyThread });
+ expect(result.current).not.toBe(channel.messageComposer);
+ expect(result.current.contextType).toBe('legacy_thread');
+ });
+ });
+
+ describe('legacy thread composer', () => {
+ it('creates a new composer for a legacy thread parent when the cache is empty', async () => {
+ const legacyThread = generateMessage({
+ cid: channel.cid,
+ }) as unknown as LocalMessage;
+ const { result } = await setup({ channel, client, legacyThread });
+
+ expect(result.current).toBeInstanceOf(MessageComposerController);
+ expect(result.current.contextType).toBe('legacy_thread');
+ expect(result.current.tag).toBe(
+ MessageComposerController.constructTag({
+ ...legacyThread,
+ legacyThreadId: legacyThread.id,
+ }),
+ );
+ });
+
+ it('adds the created legacy-thread composer to the client message composer cache', async () => {
+ const legacyThread = generateMessage({
+ cid: channel.cid,
+ }) as unknown as LocalMessage;
+ const { result } = await setup({ channel, client, legacyThread });
+
+ expect(client.messageComposerCache.peek(result.current.tag)).toBe(result.current);
+ });
+
+ it('reuses an already-cached composer for the same legacy thread parent id', async () => {
+ const legacyThread = generateMessage({
+ cid: channel.cid,
+ }) as unknown as LocalMessage;
+ const compositionContext = {
+ ...legacyThread,
+ legacyThreadId: legacyThread.id,
+ };
+ const preExistingComposer = new MessageComposerController({
+ client,
+ compositionContext,
+ });
+ client.messageComposerCache.add(
+ MessageComposerController.constructTag(compositionContext),
+ preExistingComposer,
+ );
+
+ const { result } = await setup({ channel, client, legacyThread });
+ expect(result.current).toBe(preExistingComposer);
+ });
+
+ it('returns a stable composer reference across re-renders for the same legacy thread parent id', async () => {
+ const legacyThread = generateMessage({
+ cid: channel.cid,
+ }) as unknown as LocalMessage;
+ const { rerender, result } = await setup({ channel, client, legacyThread });
+
+ const first = result.current;
+ await act(() => {
+ rerender();
+ });
+ expect(result.current).toBe(first);
+ });
+ });
+
+ describe('cache membership', () => {
+ it('does not add the channel composer to the cache', async () => {
+ const { result } = await setup({ channel, client });
+ expect(result.current.contextType).toBe('channel');
+ expect(client.messageComposerCache.peek(result.current.tag)).toBeUndefined();
+ });
+
+ it('does not add a thread-instance composer to the cache', async () => {
+ const threadComposer = buildStandaloneComposer(client, channel);
+ const threadInstance = buildStubThreadInstance(threadComposer);
+
+ const { result } = await setup({ channel, client, threadInstance });
+ expect(client.messageComposerCache.peek(result.current.tag)).toBeUndefined();
+ });
+ });
+
+ describe('subscriptions', () => {
+ it('registers subscriptions on the resolved composer and unsubscribes on unmount', async () => {
+ const unsubscribe = vi.fn();
+ const registerSpy = vi
+ .spyOn(channel.messageComposer, 'registerSubscriptions')
+ .mockReturnValue(unsubscribe);
+
+ const { unmount } = await setup({ channel, client });
+ expect(registerSpy).toHaveBeenCalledTimes(1);
+ expect(unsubscribe).not.toHaveBeenCalled();
+
+ unmount();
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
+ });
+
+ it('registers subscriptions on the override composer when one is supplied', async () => {
+ const overrideComposer = buildStandaloneComposer(client, channel);
+ const registerSpy = vi
+ .spyOn(overrideComposer, 'registerSubscriptions')
+ .mockReturnValue(vi.fn());
+
+ await setup({ channel, client, overrideComposer });
+ expect(registerSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/src/components/MessageComposer/hooks/useMessageComposerController.ts b/src/components/MessageComposer/hooks/useMessageComposerController.ts
index 9bf922453..63ee64f49 100644
--- a/src/components/MessageComposer/hooks/useMessageComposerController.ts
+++ b/src/components/MessageComposer/hooks/useMessageComposerController.ts
@@ -3,6 +3,7 @@ import { MessageComposer as MessageComposerController } from 'stream-chat';
import { useThreadContext } from '../../Threads';
import { useChannelStateContext, useChatContext } from '../../../context';
import { useLegacyThreadContext } from '../../Thread';
+import { useMessageComposerControllerContext } from '../MessageComposer';
export const useMessageComposerController = () => {
const { client } = useChatContext();
@@ -10,6 +11,8 @@ export const useMessageComposerController = () => {
const { channel } = useChannelStateContext();
const { legacyThread: parentMessage } = useLegacyThreadContext();
const threadInstance = useThreadContext();
+ // custom supplied composer overriding default composer retrieval behavior
+ const composerFromOverrideContext = useMessageComposerControllerContext();
const cachedParentMessage = useMemo(() => {
if (!parentMessage) return undefined;
@@ -22,6 +25,8 @@ export const useMessageComposerController = () => {
// edited message (always new) -> thread instance (own) -> thread message (always new) -> channel (own)
// editedMessage ?? thread ?? parentMessage ?? channel;
const messageComposer = useMemo(() => {
+ if (composerFromOverrideContext) return composerFromOverrideContext;
+
if (threadInstance) {
return threadInstance.messageComposer;
} else if (cachedParentMessage) {
@@ -42,7 +47,14 @@ export const useMessageComposerController = () => {
} else {
return channel.messageComposer;
}
- }, [cachedParentMessage, channel, client, queueCache, threadInstance]);
+ }, [
+ cachedParentMessage,
+ channel.messageComposer,
+ client,
+ composerFromOverrideContext,
+ queueCache,
+ threadInstance,
+ ]);
if (
(['legacy_thread', 'message'] as MessageComposerController['contextType'][]).includes(
diff --git a/src/components/MessageList/__tests__/VirtualizedMessageList.test.tsx b/src/components/MessageList/__tests__/VirtualizedMessageList.test.tsx
index 081162fb0..0354d7751 100644
--- a/src/components/MessageList/__tests__/VirtualizedMessageList.test.tsx
+++ b/src/components/MessageList/__tests__/VirtualizedMessageList.test.tsx
@@ -1,5 +1,5 @@
import React, { act } from 'react';
-import { cleanup, render, type RenderResult } from '@testing-library/react';
+import { cleanup, render } from '@testing-library/react';
import { nanoid } from 'nanoid';
import {
@@ -87,17 +87,18 @@ describe('VirtualizedMessageList', () => {
const { channel, client } = await createChannel(true);
vi.mocked(nanoid).mockReturnValue('mockedId');
- let result: RenderResult;
- await act(() => {
- result = render(
-
-
-
-
- ,
- );
- });
- expect(result.container).toMatchSnapshot();
+ const { container, findByText } = render(
+
+
+
+
+ ,
+ );
+
+ const emptyStateText = await findByText('Send a message to start the conversation');
+ const virtualList = container.querySelector('.str-chat__virtual-list');
+ expect(virtualList).toBeInTheDocument();
+ expect(virtualList).toContainElement(emptyStateText);
});
});
diff --git a/src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.tsx.snap b/src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.tsx.snap
deleted file mode 100644
index a25b68ac9..000000000
--- a/src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.tsx.snap
+++ /dev/null
@@ -1,63 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`VirtualizedMessageList > should render the list without any message 1`] = `
-
-`;