Skip to content

Commit 91996b5

Browse files
committed
test: start making message composition tests work
1 parent 16d3b13 commit 91996b5

25 files changed

+449
-621
lines changed

examples/vite/src/App.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
useCreateChatClient,
2222
VirtualizedMessageList as MessageList,
2323
Window,
24+
SendButtonProps,
25+
useMessageComposer,
2426
} from 'stream-chat-react';
2527
import { createTextComposerEmojiMiddleware } from 'stream-chat-react/emojis';
2628
import { init, SearchIndex } from 'emoji-mart';
@@ -47,14 +49,28 @@ const userId = parseUserIdFromToken(userToken);
4749
const filters: ChannelFilters = {
4850
members: { $in: [userId] },
4951
type: 'messaging',
50-
archived: false,
52+
// archived: false,
5153
};
5254
const options: ChannelOptions = { limit: 5, presence: true, state: true };
5355
const sort: ChannelSort = { pinned_at: 1, last_message_at: -1, updated_at: -1 };
5456

5557
// @ts-ignore
5658
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;
5759

60+
const Btn = ({ sendMessage }: SendButtonProps) => {
61+
const messageComposer = useMessageComposer();
62+
return (
63+
<button
64+
onClick={(e) => {
65+
messageComposer.customDataManager.setData({ a: 'b' });
66+
sendMessage(e);
67+
}}
68+
>
69+
Submit
70+
</button>
71+
);
72+
};
73+
5874
const App = () => {
5975
const chatClient = useCreateChatClient({
6076
apiKey,
@@ -70,9 +86,13 @@ const App = () => {
7086
middleware: [
7187
createTextComposerEmojiMiddleware(SearchIndex) as TextComposerMiddleware,
7288
],
73-
position: { before: 'stream-io/mentions-middleware' },
89+
position: { before: 'stream-io/text-composer/mentions-middleware' },
7490
unique: true,
7591
});
92+
composer.attachmentManager.setCustomUploadFn(async (fileLike) => {
93+
return composer.attachmentManager.doDefaultUploadRequest(fileLike);
94+
});
95+
// composer.customDataManager =
7696
});
7797
}, [chatClient]);
7898

@@ -91,7 +111,7 @@ const App = () => {
91111
showChannelSearch
92112
additionalChannelSearchProps={{ searchForChannels: true }}
93113
/>
94-
<Channel emojiSearchIndex={SearchIndex}>
114+
<Channel emojiSearchIndex={SearchIndex} SendButton={Btn}>
95115
<Window>
96116
<ChannelHeader Avatar={ChannelAvatar} />
97117
<MessageList returnAllReadData />

jest.env.setup.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,38 @@ Object.defineProperty(globalThis, 'crypto', {
1111
},
1212
},
1313
});
14+
15+
// Mock proper File API behavior
16+
if (typeof File === 'undefined') {
17+
class File extends Blob {
18+
constructor(bits, name, options = {}) {
19+
super(bits, options);
20+
this.name = name;
21+
this.lastModified = options.lastModified || Date.now();
22+
}
23+
}
24+
global.File = File;
25+
}
26+
27+
// Ensure FileReader is available
28+
if (typeof FileReader === 'undefined') {
29+
class FileReader {
30+
readAsDataURL(blob) {
31+
const result = `data:${blob.type};base64,${Buffer.from(blob).toString('base64')}`;
32+
setTimeout(() => {
33+
this.result = result;
34+
this.onload?.();
35+
}, 0);
36+
}
37+
}
38+
global.FileReader = FileReader;
39+
}
40+
41+
// Mock URL.createObjectURL
42+
if (typeof URL.createObjectURL === 'undefined') {
43+
URL.createObjectURL = (file) => `blob:${file.name}`;
44+
}
45+
// Mock URL.createObjectURL
46+
if (typeof URL.revokeObjectURL === 'undefined') {
47+
URL.revokeObjectURL = () => null;
48+
}

src/components/Message/MessageSimple.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const EditMessageModal = ({
4545
additionalMessageInputProps,
4646
}: Pick<MessageUIComponentProps, 'additionalMessageInputProps'>) => {
4747
const { EditMessageInput = DefaultEditMessageForm } = useComponentContext();
48-
const { clearEditingState, message } = useMessageContext();
48+
const { clearEditingState } = useMessageContext();
4949
const messageComposer = useMessageComposer();
5050
const onEditModalClose = useCallback(() => {
5151
clearEditingState();
@@ -63,7 +63,6 @@ const EditMessageModal = ({
6363
grow
6464
hideSendButton
6565
Input={EditMessageInput}
66-
message={message}
6766
{...additionalMessageInputProps}
6867
/>
6968
</Modal>

src/components/MessageInput/AttachmentPreviewList/FileAttachmentPreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const FileAttachmentPreview = ({
4747
<CloseIcon />
4848
</button>
4949

50-
{uploadState === 'failed' && !!handleRetry && (
50+
{['blocked', 'failed'].includes(uploadState) && !!handleRetry && (
5151
<button
5252
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-file'
5353
data-testid='file-preview-item-retry-button'

src/components/MessageInput/AttachmentPreviewList/ImageAttachmentPreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const ImageAttachmentPreview = ({
4040
<CloseIcon />
4141
</button>
4242

43-
{uploadState === 'failed' && (
43+
{['blocked', 'failed'].includes(uploadState) && (
4444
<button
4545
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-image'
4646
data-testid='image-preview-item-retry-button'

src/components/MessageInput/AttachmentPreviewList/UnsupportedAttachmentPreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const UnsupportedAttachmentPreview = ({
4444
</button>
4545

4646
{isLocalUploadAttachment(attachment) &&
47-
attachment.localMetadata?.uploadState === 'failed' &&
47+
['blocked', 'failed'].includes(attachment.localMetadata?.uploadState) &&
4848
!!handleRetry && (
4949
<button
5050
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-file'

src/components/MessageInput/AttachmentPreviewList/VoiceRecordingPreview.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,16 @@ export const VoiceRecordingPreview = ({
4444
<CloseIcon />
4545
</button>
4646

47-
{attachment.localMetadata?.uploadState === 'failed' && !!handleRetry && (
48-
<button
49-
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-file'
50-
data-testid='file-preview-item-retry-button'
51-
onClick={() => handleRetry(attachment)}
52-
>
53-
<RetryIcon />
54-
</button>
55-
)}
47+
{['blocked', 'failed'].includes(attachment.localMetadata?.uploadState) &&
48+
!!handleRetry && (
49+
<button
50+
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-file'
51+
data-testid='file-preview-item-retry-button'
52+
onClick={() => handleRetry(attachment)}
53+
>
54+
<RetryIcon />
55+
</button>
56+
)}
5657

5758
<div className='str-chat__attachment-preview-metadata'>
5859
<div className='str-chat__attachment-preview-file-name' title={attachment.title}>

src/components/MessageInput/AttachmentSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export const DefaultAttachmentSelectorComponents = {
9999
return (
100100
<DialogMenuButton
101101
className='str-chat__attachment-selector-actions-menu__button str-chat__attachment-selector-actions-menu__upload-file-button'
102-
disabled={!isUploadEnabled} // todo: add styles
102+
disabled={!isUploadEnabled} // todo: add styles for disabled state
103103
onClick={() => {
104104
if (fileInput) fileInput.click();
105105
closeMenu();

src/components/MessageInput/EditMessageForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import React, { useCallback, useEffect } from 'react';
22
import { MessageInputFlat } from './MessageInputFlat';
33

44
import { useMessageInputContext, useTranslationContext } from '../../context';
5-
import { useComposerHasSendableData, useMessageComposer } from './hooks';
5+
import { useMessageComposer, useMessageComposerHasSendableData } from './hooks';
66

77
const EditMessageFormSendButton = () => {
8-
const { t } = useTranslationContext('EditMessageForm');
9-
const hasSendableData = useComposerHasSendableData();
8+
const { t } = useTranslationContext();
9+
const hasSendableData = useMessageComposerHasSendableData();
1010
return (
1111
<button
1212
className='str-chat__edit-message-send'

src/components/MessageInput/MessageInput.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { PropsWithChildren } from 'react';
2-
import { useEffect } from 'react';
3-
import React from 'react';
2+
import React, { useEffect } from 'react';
43

54
import { MessageInputFlat } from './MessageInputFlat';
65
import { useMessageComposer } from './hooks/messageComposer/useMessageComposer';
@@ -14,18 +13,18 @@ import { MessageInputContextProvider } from '../../context/MessageInputContext';
1413
import { DialogManagerProvider } from '../../context';
1514

1615
import type {
17-
BaseLocalAttachmentMetadata,
1816
Channel,
1917
LinkPreviewsManagerConfig,
2018
LocalAttachmentUploadMetadata,
2119
LocalMessage,
20+
Message,
2221
SendFileAPIResponse,
22+
SendMessageOptions,
2323
} from 'stream-chat';
2424

2525
import type { SearchQueryParams } from '../ChannelSearch/hooks/useChannelSearch';
2626
import type { CustomAudioRecordingConfig } from '../MediaRecorder';
2727

28-
// todo: move to a relevant place
2928
export type EmojiSearchIndexResult = {
3029
id: string;
3130
name: string;
@@ -70,13 +69,6 @@ export type MessageInputProps = {
7069
) => Promise<SendFileAPIResponse>;
7170
/** Mechanism to be used with autocomplete and text replace features of the `MessageInput` component, see [emoji-mart `SearchIndex`](https://github.com/missive/emoji-mart#%EF%B8%8F%EF%B8%8F-headless-search) */
7271
emojiSearchIndex?: ComponentContextValue['emojiSearchIndex'];
73-
// todo: document how to subscribe to notifications to handle errors
74-
/** Custom error handler function to be called with a file/image upload fails */
75-
errorHandler?: (
76-
error: Error,
77-
type: string,
78-
file: LocalAttachmentUploadMetadata['file'] & BaseLocalAttachmentMetadata,
79-
) => void;
8072
/** If true, focuses the text input on component mount */
8173
focus?: boolean;
8274
/** Generates the default value for the underlying textarea element. The function's return value takes precedence before additionalTextareaProps.defaultValue. */
@@ -95,15 +87,21 @@ export type MessageInputProps = {
9587
mentionAllAppUsers?: boolean;
9688
/** Object containing filters/sort/options overrides for an @mention user query */
9789
mentionQueryParams?: SearchQueryParams['userFilters'];
98-
/** If provided, the existing message will be edited on submit */
99-
message?: LocalMessage;
10090
/** Min number of rows the underlying `textarea` will start with. The `grow` on MessageInput prop has to be enabled for `minRows` to take effect. */
10191
minRows?: number;
92+
/** Function to override the default message sending process. Not message updating process. */
93+
overrideSubmitHandler?: (params: {
94+
cid: string;
95+
localMessage: LocalMessage;
96+
message: Message;
97+
sendOptions: SendMessageOptions;
98+
}) => Promise<void> | void;
10299
/** When replying in a thread, the parent message object */
103100
parent?: LocalMessage;
101+
// todo: X document change in configuration
104102
/** If true, triggers typing events on text input keystroke */
105103
publishTypingEvent?: boolean;
106-
// todo: document the change of transliterate prop
104+
// todo: X document the change of transliterate prop
107105
/** If true, will use an optional dependency to support transliteration in the input for mentions, default is false. See: https://github.com/getstream/transliterate */
108106
/**
109107
* Currently, `Enter` is the default submission key and `Shift`+`Enter` is the default combination for the new line.
@@ -125,6 +123,7 @@ const MessageInputProvider = (props: PropsWithChildren<MessageInputProps>) => {
125123
const messageInputState = useMessageInputState(props);
126124
const { emojiSearchIndex } = useComponentContext('MessageInput');
127125

126+
// todo: X document how to disable publishTypingEvents
128127
// if (typeof props.publishTypingEvent !== 'undefined') {
129128
// messageComposer.config.publishTypingEvents = props.publishTypingEvent;
130129
// }

0 commit comments

Comments
 (0)