Skip to content

feat: message composer #2669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 47 commits into
base: rc
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
10251ec
feat: support message draft management for main message list
MartinCupela Feb 20, 2025
6bb0e4c
feat: use Thread class instance to manage Thread component state when…
MartinCupela Feb 21, 2025
7780290
feat: add MessageComposer
MartinCupela Mar 13, 2025
991b070
style: fix lint issues
MartinCupela Mar 13, 2025
a2e4155
Post-merge fixes
arnautov-anton Mar 13, 2025
979ab8b
Link latest stream-chat
arnautov-anton Mar 13, 2025
081376b
refactor: make the message composer code runable
MartinCupela Mar 13, 2025
47bbd2e
feat: add react namespaced class on TextAreaComposer SuggestionList
MartinCupela Mar 18, 2025
b458810
refactor: remove emojis search index initiation from textComposerEmoj…
MartinCupela Mar 18, 2025
c8dff0f
refactor: use SendMessageOptions type from stream-chat
MartinCupela Mar 20, 2025
39a749f
refactor: replace MiddlewareParams with TextComposerMiddlewareParams …
MartinCupela Mar 20, 2025
f528274
Remove trigger generics, refactor useMessageComposer
arnautov-anton Mar 20, 2025
43561ce
refactor: pass channel to createMentionsMiddleware instead of client
MartinCupela Mar 20, 2025
ebe89a4
style: apply linter
MartinCupela Mar 20, 2025
74cf07e
feat: replace StreamMessage and MessageToSend with LocalMessage and R…
MartinCupela Mar 26, 2025
38e7a08
refactor: register only custom text composer middleware
MartinCupela Mar 26, 2025
53a0a87
chore: add todo
MartinCupela Mar 26, 2025
e9f0175
Adjust useMessageComposer & Thread-related stuff
arnautov-anton Mar 26, 2025
fe3c100
WIP: composer queue cache
arnautov-anton Mar 28, 2025
9e9fc5b
Make FixedSizeQueueCache slightly faster
arnautov-anton Mar 28, 2025
40043dd
Adjust composer subscription behavior
arnautov-anton Mar 28, 2025
7d4ed43
style: fix typo
MartinCupela Mar 28, 2025
9f6c7bf
feat: use MessageComposer's PollComposer to handle poll creation
MartinCupela Mar 28, 2025
7e95cb7
Merge remote-tracking branch 'origin/feat/message-composer' into feat…
MartinCupela Mar 31, 2025
64ae4d0
refactor: use message composer to upload audio recording
MartinCupela Mar 31, 2025
a58d7f7
refactor: reflect updateMessage signature change and LocalMessage typ…
MartinCupela Apr 1, 2025
373ec98
refactor: support draft management through MessageComposer
MartinCupela Apr 2, 2025
1f4cdd5
refactor: adapt AttachmentPreviewList to LLC attachment types
MartinCupela Apr 4, 2025
80f908c
refactor: remove unused code related to useMessageInputState
MartinCupela Apr 4, 2025
58854a4
fix: retrieve draft for on legacy thread opening
MartinCupela Apr 7, 2025
6ced10f
wip: compositionContext
arnautov-anton Apr 7, 2025
3007a5d
feat: prepare TextAreaComposer and SuggestionList UI to work with tex…
MartinCupela Apr 9, 2025
84bfba2
feat: allow to specify and thus exclude tabIndex for dialog container
MartinCupela Apr 9, 2025
7e87140
fix: keep Enter key as reserved for message submission
MartinCupela Apr 9, 2025
29ae50e
fix: export textComposerEmojiMiddleware from emoji plugin
MartinCupela Apr 9, 2025
067a43a
feat(examples): add textComposerEmojiMiddleware to message composer
MartinCupela Apr 9, 2025
ee7991b
fix: fix message editing bugs
MartinCupela Apr 11, 2025
f4e4dd0
fix: handle max_votes_allowed field blur in poll creation dialog
MartinCupela Apr 11, 2025
e28de80
fix: forward paste handler to Textarea component
MartinCupela Apr 11, 2025
81dec28
applyModifications -> setupFunction
arnautov-anton Apr 11, 2025
44a57c8
fix: add EditMessageModal to prevent creating message composer before…
MartinCupela Apr 13, 2025
1da2ae6
fix: return message composer for edited message only when editing
MartinCupela Apr 13, 2025
e0d008e
fix: do not clear message composer state on edited message submission
MartinCupela Apr 13, 2025
aa9144a
fix: control the textarea cursor position to prevent jumping on change
MartinCupela Apr 13, 2025
5b6a18a
Merge remote-tracking branch 'origin/feat/message-composer' into feat…
MartinCupela Apr 13, 2025
a5addee
refactor: remove referencing message draft in Channel state context
MartinCupela Apr 14, 2025
80676d0
fix: do not check for message input context to retrieve message composer
MartinCupela Apr 14, 2025
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
37 changes: 32 additions & 5 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
import { useEffect } from 'react';
import {
ChannelFilters,
ChannelOptions,
ChannelSort,
LocalMessage,
TextComposerMiddleware,
} from 'stream-chat';
import {
AIStateIndicator,
Channel,
Expand All @@ -8,13 +15,18 @@ import {
Chat,
ChatView,
MessageInput,
StreamMessage,
Thread,
ThreadList,
useChatContext,
useCreateChatClient,
VirtualizedMessageList as MessageList,
Window,
} from 'stream-chat-react';
import { createTextComposerEmojiMiddleware } from 'stream-chat-react/emojis';
import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data';

init({ data });

const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, property) => searchParams.get(property as string),
Expand All @@ -40,7 +52,8 @@ const filters: ChannelFilters = {
const options: ChannelOptions = { limit: 5, presence: true, state: true };
const sort: ChannelSort = { pinned_at: 1, last_message_at: -1, updated_at: -1 };

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

const App = () => {
const chatClient = useCreateChatClient({
Expand All @@ -49,6 +62,20 @@ const App = () => {
userData: { id: userId },
});

useEffect(() => {
if (!chatClient) return;

chatClient.setMessageComposerSetupFunction(({ composer }) => {
composer.textComposer.middlewareExecutor.insert({
middleware: [
createTextComposerEmojiMiddleware(SearchIndex) as TextComposerMiddleware,
],
position: { before: 'stream-io/mentions-middleware' },
unique: true,
});
});
}, [chatClient]);

if (!chatClient) return <>Loading...</>;

return (
Expand All @@ -64,12 +91,12 @@ const App = () => {
showChannelSearch
additionalChannelSearchProps={{ searchForChannels: true }}
/>
<Channel>
<Channel emojiSearchIndex={SearchIndex}>
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
<AIStateIndicator />
<MessageInput focus />
<MessageInput focus audioRecordingEnabled />
</Window>
<Thread virtualized />
</Channel>
Expand Down
1,134 changes: 619 additions & 515 deletions examples/vite/yarn.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"semantic-release": "^24.2.3",
"stream-chat": "^9.0.0-rc.8",
"stream-chat": "https://github.com/GetStream/stream-chat-js.git#feat/message-composer",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5",
"typescript-eslint": "^8.17.0"
Expand Down
18 changes: 9 additions & 9 deletions src/components/Attachment/Attachment.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import React, { useMemo } from 'react';
import type { ReactPlayerProps } from 'react-player';
import type { Attachment as StreamAttachment } from 'stream-chat';

import {
isAudioAttachment,

Check failure on line 3 in src/components/Attachment/Attachment.tsx

View workflow job for this annotation

GitHub Actions / TypeScript

'"stream-chat"' has no exported member named 'isAudioAttachment'. Did you mean 'AudioAttachment'?
isFileAttachment,
isMediaAttachment,
isImageAttachment,

Check failure on line 4 in src/components/Attachment/Attachment.tsx

View workflow job for this annotation

GitHub Actions / TypeScript

'"stream-chat"' has no exported member named 'isImageAttachment'. Did you mean 'isLocalImageAttachment'?
isScrapedContent,
isUploadedImage,
isVoiceRecordingAttachment,

Check failure on line 6 in src/components/Attachment/Attachment.tsx

View workflow job for this annotation

GitHub Actions / TypeScript

'"stream-chat"' has no exported member named 'isVoiceRecordingAttachment'. Did you mean 'VoiceRecordingAttachment'?
} from './utils';
} from 'stream-chat';

import {
AudioContainer,
CardContainer,
Expand All @@ -20,6 +16,10 @@
UnsupportedAttachmentContainer,
VoiceRecordingContainer,
} from './AttachmentContainer';
import { isFileAttachment, isMediaAttachment } from './utils';

import type { ReactPlayerProps } from 'react-player';
import type { Attachment as StreamAttachment } from 'stream-chat';
import type { AttachmentActionsProps } from './AttachmentActions';
import type { AudioProps } from './Audio';
import type { VoiceRecordingProps } from './VoiceRecording';
Expand Down Expand Up @@ -104,11 +104,11 @@
...rest
}: AttachmentProps): GroupedRenderedAttachment => {
const uploadedImages: StreamAttachment[] = attachments.filter((attachment) =>
isUploadedImage(attachment),
isImageAttachment(attachment),
);

const containers = attachments
.filter((attachment) => !isUploadedImage(attachment))
.filter((attachment) => !isImageAttachment(attachment))
.reduce<GroupedRenderedAttachment>(
(typeMap, attachment) => {
const attachmentType = getAttachmentType(attachment);
Expand Down
3 changes: 1 addition & 2 deletions src/components/Attachment/AttachmentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useLayoutEffect, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import clsx from 'clsx';
import * as linkify from 'linkifyjs';
import type { Attachment } from 'stream-chat';
import type { Attachment, LocalAttachment } from 'stream-chat';

import { AttachmentActions as DefaultAttachmentActions } from './AttachmentActions';
import { Audio as DefaultAudio } from './Audio';
Expand All @@ -20,7 +20,6 @@ import type {
} from './utils';
import { isGalleryAttachmentType, isSvgAttachment } from './utils';
import { useChannelStateContext } from '../../context/ChannelStateContext';
import type { LocalAttachment } from '../MessageInput';
import type {
ImageAttachmentConfiguration,
VideoAttachmentConfiguration,
Expand Down
11 changes: 5 additions & 6 deletions src/components/Attachment/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { ReactNode } from 'react';
import type { Attachment } from 'stream-chat';

import type { ATTACHMENT_GROUPS_ORDER, AttachmentProps } from './Attachment';
import type {
Attachment,
LocalAttachment,
LocalAudioAttachment,
LocalFileAttachment,
LocalImageAttachment,
LocalVideoAttachment,
LocalVoiceRecordingAttachment,
VoiceRecordingAttachment,
} from '../MessageInput';
} from 'stream-chat';

import type { LocalFileAttachment, LocalVideoAttachment } from 'stream-chat';
import type { ATTACHMENT_GROUPS_ORDER, AttachmentProps } from './Attachment';

export const SUPPORTED_VIDEO_FORMATS = [
'video/mp4',
Expand Down Expand Up @@ -76,8 +75,8 @@
export const isFileAttachment = (attachment: Attachment | LocalAttachment) =>
attachment.type === 'file' ||
!!(
attachment.mime_type &&

Check failure on line 78 in src/components/Attachment/utils.tsx

View workflow job for this annotation

GitHub Actions / TypeScript

Property 'mime_type' does not exist on type 'Attachment | LocalAttachment'.
SUPPORTED_VIDEO_FORMATS.indexOf(attachment.mime_type) === -1 &&

Check failure on line 79 in src/components/Attachment/utils.tsx

View workflow job for this annotation

GitHub Actions / TypeScript

Property 'mime_type' does not exist on type 'Attachment | LocalAttachment'.
attachment.type !== 'video'
);

Expand All @@ -87,8 +86,8 @@
isFileAttachment(attachment) && isLocalAttachment(attachment);

export const isMediaAttachment = (attachment: Attachment | LocalAttachment) =>
(attachment.mime_type &&

Check failure on line 89 in src/components/Attachment/utils.tsx

View workflow job for this annotation

GitHub Actions / TypeScript

Property 'mime_type' does not exist on type 'Attachment | LocalAttachment'.
SUPPORTED_VIDEO_FORMATS.indexOf(attachment.mime_type) !== -1) ||

Check failure on line 90 in src/components/Attachment/utils.tsx

View workflow job for this annotation

GitHub Actions / TypeScript

Property 'mime_type' does not exist on type 'Attachment | LocalAttachment'.
attachment.type === 'video';

export const isLocalMediaAttachment = (
Expand Down
37 changes: 0 additions & 37 deletions src/components/AutoCompleteTextarea/Item.jsx

This file was deleted.

167 changes: 0 additions & 167 deletions src/components/AutoCompleteTextarea/List.jsx

This file was deleted.

Loading
Loading