From 914950ce1d3c5e7a672320ad456005e40966e75f Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 20 Feb 2026 12:12:19 +0100 Subject: [PATCH 1/2] feat: render voice recording preview in a dedicated slot --- .../AttachmentPreviewList.tsx | 16 ++------ .../VoiceRecordingPreviewSlot.tsx | 38 +++++++++++++++++++ .../AttachmentPreviewList/index.ts | 4 ++ .../MessageInput/MessageInputFlat.tsx | 7 +++- .../__tests__/AttachmentPreviewList.test.js | 9 ++--- src/components/MessageInput/index.ts | 1 + .../styling/AttachmentPreview.scss | 19 +++++++++- src/context/ComponentContext.tsx | 3 ++ 8 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 src/components/MessageInput/AttachmentPreviewList/VoiceRecordingPreviewSlot.tsx diff --git a/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx b/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx index c45050c99..e9c7d74c1 100644 --- a/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx +++ b/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx @@ -13,7 +13,6 @@ import { UnsupportedAttachmentPreview as DefaultUnknownAttachmentPreview, type UnsupportedAttachmentPreviewProps, } from './UnsupportedAttachmentPreview'; -import { type VoiceRecordingPreviewProps } from './VoiceRecordingPreview'; import { FileAttachmentPreview as DefaultFileAttachmentPreview, type FileAttachmentPreviewProps, @@ -40,7 +39,6 @@ export type AttachmentPreviewListProps = { ImageAttachmentPreview?: ComponentType; UnsupportedAttachmentPreview?: ComponentType; VideoAttachmentPreview?: ComponentType; - VoiceRecordingPreview?: ComponentType; }; export const AttachmentPreviewList = ({ @@ -50,7 +48,6 @@ export const AttachmentPreviewList = ({ ImageAttachmentPreview = MediaAttachmentPreview, UnsupportedAttachmentPreview = DefaultUnknownAttachmentPreview, VideoAttachmentPreview = MediaAttachmentPreview, - VoiceRecordingPreview = DefaultAudioAttachmentPreview, }: AttachmentPreviewListProps) => { const messageComposer = useMessageComposer(); @@ -79,16 +76,9 @@ export const AttachmentPreviewList = ({ )} {attachments.map((attachment) => { if (isScrapedContent(attachment)) return null; - if (isLocalVoiceRecordingAttachment(attachment)) { - return ( - - ); - } else if (isLocalAudioAttachment(attachment)) { + // Voice recordings are rendered in the dedicated slot above (VoiceRecordingPreviewSlot) + if (isLocalVoiceRecordingAttachment(attachment)) return null; + if (isLocalAudioAttachment(attachment)) { return ( ; +}; + +/** + * Dedicated slot for voice recording preview(s), rendered apart from the main attachment preview list + */ +export const VoiceRecordingPreviewSlot = ({ + VoiceRecordingPreview = AudioAttachmentPreview, +}: VoiceRecordingPreviewSlotProps) => { + const messageComposer = useMessageComposer(); + const { attachments } = useAttachmentsForPreview(); + + const voiceAttachments = attachments.filter(isLocalVoiceRecordingAttachment); + const firstVoice = voiceAttachments[0]; + if (!firstVoice) return null; + + return ( +
+ +
+ ); +}; diff --git a/src/components/MessageInput/AttachmentPreviewList/index.ts b/src/components/MessageInput/AttachmentPreviewList/index.ts index 3d7db62a3..79f1c566a 100644 --- a/src/components/MessageInput/AttachmentPreviewList/index.ts +++ b/src/components/MessageInput/AttachmentPreviewList/index.ts @@ -7,3 +7,7 @@ export type { UploadAttachmentPreviewProps as AttachmentPreviewProps } from './t export type { UnsupportedAttachmentPreviewProps } from './UnsupportedAttachmentPreview'; export type { MediaAttachmentPreviewProps } from './MediaAttachmentPreview'; export type { VoiceRecordingPreviewProps } from './VoiceRecordingPreview'; +export { + VoiceRecordingPreviewSlot, + type VoiceRecordingPreviewSlotProps, +} from './VoiceRecordingPreviewSlot'; diff --git a/src/components/MessageInput/MessageInputFlat.tsx b/src/components/MessageInput/MessageInputFlat.tsx index 4a3f6fff0..6b3e72e4a 100644 --- a/src/components/MessageInput/MessageInputFlat.tsx +++ b/src/components/MessageInput/MessageInputFlat.tsx @@ -4,7 +4,10 @@ import { AttachmentSelector as DefaultAttachmentSelector, SimpleAttachmentSelector, } from './AttachmentSelector/AttachmentSelector'; -import { AttachmentPreviewList as DefaultAttachmentPreviewList } from './AttachmentPreviewList'; +import { + AttachmentPreviewList as DefaultAttachmentPreviewList, + VoiceRecordingPreviewSlot as DefaultVoiceRecordingPreviewSlot, +} from './AttachmentPreviewList'; import { AudioRecorder as DefaultAudioRecorder } from '../MediaRecorder'; import { EditedMessagePreview as DefaultEditedMessagePreview } from './EditedMessagePreview'; import { QuotedMessagePreview as DefaultQuotedMessagePreview } from './QuotedMessagePreview'; @@ -55,6 +58,7 @@ const MessageComposerPreviews = () => { EditedMessagePreview = DefaultEditedMessagePreview, LinkPreviewList = DefaultLinkPreviewList, QuotedMessagePreview = DefaultQuotedMessagePreview, + VoiceRecordingPreviewSlot = DefaultVoiceRecordingPreviewSlot, } = useComponentContext(); const messageComposer = useMessageComposer(); @@ -97,6 +101,7 @@ const MessageComposerPreviews = () => { ) : ( )} + diff --git a/src/components/MessageInput/__tests__/AttachmentPreviewList.test.js b/src/components/MessageInput/__tests__/AttachmentPreviewList.test.js index dbaeebd41..f0d6e9605 100644 --- a/src/components/MessageInput/__tests__/AttachmentPreviewList.test.js +++ b/src/components/MessageInput/__tests__/AttachmentPreviewList.test.js @@ -118,14 +118,13 @@ describe('AttachmentPreviewList', () => { expect(screen.getByTitle(`file-upload-${state}`)).toBeInTheDocument(); expect(screen.getByTitle(`image-upload-${state}`)).toBeInTheDocument(); expect(screen.getByTitle(`audio-attachment-${state}`)).toBeInTheDocument(); - expect( - screen.getByTitle(`voice-recording-attachment-${state}`), - ).toBeInTheDocument(); + // Voice recordings are rendered in VoiceRecordingPreviewSlot above the list (REACT-794) expect(screen.getByTitle(`video-attachment-${state}`)).toBeInTheDocument(); }, ); - describe.each(['audio', 'file', 'image', 'unsupported', 'voiceRecording', 'video'])( + // voiceRecording is rendered in VoiceRecordingPreviewSlot (REACT-794), not in AttachmentPreviewList + describe.each(['audio', 'file', 'image', 'unsupported', 'video'])( '%s attachments rendering', (type) => { const customAttachment = { @@ -143,7 +142,6 @@ describe('AttachmentPreviewList', () => { image: generateImageAttachment, unsupported: () => customAttachment, video: generateVideoAttachment, - voiceRecording: generateVoiceRecordingAttachment, }; it('retries upload on upload button click', async () => { @@ -265,7 +263,6 @@ describe('AttachmentPreviewList', () => { image: 'ImageAttachmentPreview', unsupported: 'UnsupportedAttachmentPreview', video: 'MediaAttachmentPreview', - voiceRecording: 'VoiceRecordingPreview', }; const title = `${type}-attachment`; const id = `${type}-id`; diff --git a/src/components/MessageInput/index.ts b/src/components/MessageInput/index.ts index 37be03739..4e6792e37 100644 --- a/src/components/MessageInput/index.ts +++ b/src/components/MessageInput/index.ts @@ -8,6 +8,7 @@ export type { AttachmentPreviewProps, UnsupportedAttachmentPreviewProps, VoiceRecordingPreviewProps, + VoiceRecordingPreviewSlotProps, } from './AttachmentPreviewList'; export * from './CommandChip'; export * from './CooldownTimer'; diff --git a/src/components/MessageInput/styling/AttachmentPreview.scss b/src/components/MessageInput/styling/AttachmentPreview.scss index 821cb058d..f612b6b3f 100644 --- a/src/components/MessageInput/styling/AttachmentPreview.scss +++ b/src/components/MessageInput/styling/AttachmentPreview.scss @@ -73,6 +73,24 @@ --str-chat__attachment-preview-file-fatal-error-color: var(--color-accent-error); + .str-chat__message-composer-voice-preview-slot { + display: flex; + align-items: center; + width: 100%; + padding: var(--spacing-xxs); + min-width: 0; + + .str-chat__attachment-preview-audio { + width: 100%; + min-width: 0; + max-width: none; + + .str-chat__attachment-preview-file__data { + width: 100%; + } + } + } + .str-chat__attachment-preview-list { @include utils.component-layer-overrides('attachment-preview-list'); padding: var(--spacing-xxs); @@ -309,6 +327,5 @@ height: var(--button-visual-height-md); width: var(--button-visual-height-md); border: 1px solid var(--control-play-control-border); - background-color: var(--control-play-control-bg); } } diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 5784a73ba..7be556cc1 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -51,6 +51,7 @@ import type { TypingIndicatorProps, UnreadMessagesNotificationProps, UnreadMessagesSeparatorProps, + VoiceRecordingPreviewSlotProps, } from '../components'; import type { @@ -79,6 +80,8 @@ export type ComponentContextValue = { AttachmentPreviewList?: React.ComponentType; /** Custom UI component to control adding attachments to MessageInput, defaults to and accepts same props as: [AttachmentSelector](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/AttachmentSelector.tsx) */ AttachmentSelector?: React.ComponentType; + /** Custom UI component for the dedicated voice recording preview slot above composer attachments (REACT-794), defaults to and accepts same props as: [VoiceRecordingPreviewSlot](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/AttachmentPreviewList/VoiceRecordingPreviewSlot.tsx) */ + VoiceRecordingPreviewSlot?: React.ComponentType; /** Custom UI component for contents of attachment selector initiation button */ AttachmentSelectorInitiationButtonContents?: React.ComponentType; /** Custom UI component to display AudioRecorder in MessageInput, defaults to and accepts same props as: [AudioRecorder](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/AudioRecorder.tsx) */ From f36ab675201068b580423a2377107c7292a4f94c Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 20 Feb 2026 18:16:18 +0100 Subject: [PATCH 2/2] fix: align slots in message composer previews --- .../AttachmentPreviewList.tsx | 14 +++++++------- .../MessageInput/QuotedMessagePreview.tsx | 14 ++++++++------ .../MessageInput/styling/MessageComposer.scss | 5 ++--- .../MessageInput/styling/QuotedMessagePreview.scss | 4 ++++ 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx b/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx index e9c7d74c1..14b630402 100644 --- a/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx +++ b/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx @@ -1,4 +1,4 @@ -import type { ComponentType } from 'react'; +import { type ComponentType, useMemo } from 'react'; import React from 'react'; import { isLocalAttachment, @@ -8,6 +8,7 @@ import { isLocalVideoAttachment, isLocalVoiceRecordingAttachment, isScrapedContent, + isVoiceRecordingAttachment, } from 'stream-chat'; import { UnsupportedAttachmentPreview as DefaultUnknownAttachmentPreview, @@ -53,15 +54,15 @@ export const AttachmentPreviewList = ({ // todo: we could also allow to attach poll to a message composition const { attachments, location } = useAttachmentsForPreview(); + const filteredAttachments = useMemo( + () => attachments.filter((a) => !isVoiceRecordingAttachment(a)), + [attachments], + ); - if (!attachments.length && !location) return null; + if (!filteredAttachments.length && !location) return null; return (
- {/**/} {location && ( */}
); }; diff --git a/src/components/MessageInput/QuotedMessagePreview.tsx b/src/components/MessageInput/QuotedMessagePreview.tsx index 74e528068..480882dff 100644 --- a/src/components/MessageInput/QuotedMessagePreview.tsx +++ b/src/components/MessageInput/QuotedMessagePreview.tsx @@ -253,12 +253,14 @@ export const QuotedMessagePreview = ({ ); return quotedMessage ? ( - messageComposer.setQuotedMessage(null)} - quotedMessage={quotedMessage} - renderText={renderText} - /> +
+ messageComposer.setQuotedMessage(null)} + quotedMessage={quotedMessage} + renderText={renderText} + /> +
) : null; }; diff --git a/src/components/MessageInput/styling/MessageComposer.scss b/src/components/MessageInput/styling/MessageComposer.scss index 148f26fab..8c02148d1 100644 --- a/src/components/MessageInput/styling/MessageComposer.scss +++ b/src/components/MessageInput/styling/MessageComposer.scss @@ -63,8 +63,6 @@ flex-direction: column; width: 100%; min-width: 0; - padding-inline: var(--spacing-xs); - padding-block: var(--spacing-sm); @include utils.component-layer-overrides('message-input'); } @@ -72,7 +70,7 @@ display: flex; flex-direction: column; width: 100%; - padding-bottom: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-xs) 0; gap: var(--spacing-xxs); min-width: 0; @@ -83,6 +81,7 @@ align-items: end; width: 100%; gap: var(--spacing-xs); + padding: var(--spacing-sm); $controls-containers-min-height: 26px; diff --git a/src/components/MessageInput/styling/QuotedMessagePreview.scss b/src/components/MessageInput/styling/QuotedMessagePreview.scss index 9a516dd6a..1c9cad4c6 100644 --- a/src/components/MessageInput/styling/QuotedMessagePreview.scss +++ b/src/components/MessageInput/styling/QuotedMessagePreview.scss @@ -1,6 +1,10 @@ @use '../../../styling/utils'; .str-chat { + .str-chat__message-composer__quoted-message-preview-slot { + padding: var(--spacing-xxs); + } + .str-chat__quoted-message-preview { display: flex; align-items: center;