From 17c0a6cd7f8ae55fd55eb9a2381d71fa0a335090 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:41:50 +0200 Subject: [PATCH 1/5] feat: introduce WithDragAndDropUpload component (#2688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🎯 Goal Current `Channel.dragAndDropWindow` and `Channel.optionalMessageInputProps` architecture which used to allow drag&drop file upload by dropping files onto the message list (channel) component suffers from a few pain points: - duplicate `MessageInputContextProvider` initialization, see [here](https://github.com/GetStream/stream-chat-react/blob/5fa6b0fc239a7d48032ae4c3d34e29969c52bd0c/src/components/Channel/Channel.tsx#L1479-L1484), [here](https://github.com/GetStream/stream-chat-react/blob/5fa6b0fc239a7d48032ae4c3d34e29969c52bd0c/src/components/MessageInput/MessageInput.tsx#L137-L159) and [here](https://github.com/GetStream/stream-chat-react/blob/5fa6b0fc239a7d48032ae4c3d34e29969c52bd0c/src/components/MessageInput/DropzoneProvider.tsx#L47-L67) ([related issue](https://getstream.slack.com/archives/C02R5UCGN6N/p1740392263047589)) - broken styling ([v2 vendor folder](https://github.com/GetStream/stream-chat-css/tree/v5.8.0/src/v2/styles/vendor) is missing [react-file-utils styling](https://github.com/GetStream/stream-chat-css/blob/v5.8.0/src/vendor/react-file-utils.scss)) New solution allows dragging and uploading files both in "channel" and in thread individually - which was previously impossible. The new solution also reuses drag and drop styling which is used by default in `MessageInputFlat` component (some minor adjustments from integrators are needed - such as setting relative positioning on required parents). #### Old API: ```tsx ``` #### New API: ```tsx ``` ### Deprecations #### Public - `Channel.dragAndDropWindow` prop (will remove in v13) - `Channel.optionalMessageInputProps` prop (will remove in v13) #### Private - `DropzoneProvider` - `DropzoneInner` - `ImageDropzone` Note: This is not a direct fix of the issue I mentioned above (Slack link), I believe the proper fix is to move away from this messy architecture (by removing deprecated components and options which we'll do in v13 once this PR is merged). THIS PR RELIES ON [CSS CHANGES](https://github.com/GetStream/stream-chat-css/pull/329) ### 🎨 UI Changes Before: ![image](https://github.com/user-attachments/assets/0514c338-8d52-44d8-aecd-60bf86acdee2) After: ![image](https://github.com/user-attachments/assets/3afd1468-d552-4180-9be6-b9ffbe503526) --- src/components/Channel/Channel.tsx | 10 +- src/components/MessageInput/MessageInput.tsx | 4 + .../MessageInput/MessageInputFlat.tsx | 167 +++++++----------- .../MessageInput/WithDragAndDropUpload.tsx | 150 ++++++++++++++++ src/components/MessageInput/index.ts | 1 + src/context/MessageInputContext.tsx | 5 +- 6 files changed, 228 insertions(+), 109 deletions(-) create mode 100644 src/components/MessageInput/WithDragAndDropUpload.tsx diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index ff5f5a392e..4aca0a2c43 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -212,7 +212,10 @@ export type ChannelProps< updatedMessage: UpdatedMessage, options?: UpdateMessageOptions, ) => ReturnType['updateMessage']>; - /** If true, chat users will be able to drag and drop file uploads to the entire channel window */ + /** + * @deprecated Use `WithDragAndDropUpload` instead (wrap draggable-to elements with this component). + * @description If true, chat users will be able to drag and drop file uploads to the entire channel window + */ dragAndDropWindow?: boolean; /** Custom UI component to be shown if no active channel is set, defaults to null and skips rendering the Channel component */ EmptyPlaceholder?: React.ReactElement; @@ -246,7 +249,10 @@ export type ChannelProps< onMentionsClick?: OnMentionAction; /** Custom action handler function to run on hover of an @mention in a message */ onMentionsHover?: OnMentionAction; - /** If `dragAndDropWindow` prop is true, the props to pass to the MessageInput component (overrides props placed directly on MessageInput) */ + /** + * @deprecated Use `WithDragAndDropUpload` instead (wrap draggable-to elements with this component). + * @description If `dragAndDropWindow` prop is `true`, the props to pass to the `MessageInput` component (overrides props placed directly on `MessageInput`) + */ optionalMessageInputProps?: MessageInputProps; /** You can turn on/off thumbnail generation for video attachments */ shouldGenerateVideoThumbnail?: boolean; diff --git a/src/components/MessageInput/MessageInput.tsx b/src/components/MessageInput/MessageInput.tsx index 7c27cf62ee..55b67502bb 100644 --- a/src/components/MessageInput/MessageInput.tsx +++ b/src/components/MessageInput/MessageInput.tsx @@ -26,6 +26,7 @@ import type { } from '../../types/types'; import type { URLEnrichmentConfig } from './hooks/useLinkPreviews'; import type { CustomAudioRecordingConfig } from '../MediaRecorder'; +import { useHandleDragAndDropQueuedFiles } from './WithDragAndDropUpload'; export type EmojiSearchIndexResult = { id: string; @@ -151,6 +152,9 @@ const MessageInputProvider = < emojiSearchIndex: props.emojiSearchIndex ?? emojiSearchIndex, }); + // @ts-expect-error generics to be removed + useHandleDragAndDropQueuedFiles(messageInputContextValue); + return ( value={messageInputContextValue}> {props.children} diff --git a/src/components/MessageInput/MessageInputFlat.tsx b/src/components/MessageInput/MessageInputFlat.tsx index 86b52661b7..b0e7003651 100644 --- a/src/components/MessageInput/MessageInputFlat.tsx +++ b/src/components/MessageInput/MessageInputFlat.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { Event } from 'stream-chat'; -import clsx from 'clsx'; -import { useDropzone } from 'react-dropzone'; import { AttachmentSelector as DefaultAttachmentSelector, SimpleAttachmentSelector, @@ -28,17 +26,16 @@ import { RecordingAttachmentType } from '../MediaRecorder/classes'; import { useChatContext } from '../../context/ChatContext'; import { useChannelActionContext } from '../../context/ChannelActionContext'; import { useChannelStateContext } from '../../context/ChannelStateContext'; -import { useTranslationContext } from '../../context/TranslationContext'; import { useMessageInputContext } from '../../context/MessageInputContext'; import { useComponentContext } from '../../context/ComponentContext'; import type { DefaultStreamChatGenerics } from '../../types/types'; import { AIStates, useAIState } from '../AIStateIndicator'; +import { WithDragAndDropUpload } from './WithDragAndDropUpload'; export const MessageInputFlat = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >() => { - const { t } = useTranslationContext('MessageInputFlat'); const { asyncMessagesMultiSendEnabled, attachments, @@ -48,14 +45,12 @@ export const MessageInputFlat = < hideSendButton, isUploadEnabled, linkPreviews, - maxFilesLeft, message, numberOfUploads, parent, recordingController, setCooldownRemaining, text, - uploadNewFiles, } = useMessageInputContext('MessageInputFlat'); const { @@ -71,11 +66,8 @@ export const MessageInputFlat = < StartRecordingAudioButton = DefaultStartRecordingAudioButton, StopAIGenerationButton: StopAIGenerationButtonOverride, } = useComponentContext('MessageInputFlat'); - const { - acceptedFiles = [], - multipleUploads, - quotedMessage, - } = useChannelStateContext('MessageInputFlat'); + const { quotedMessage } = + useChannelStateContext('MessageInputFlat'); const { setQuotedMessage } = useChannelActionContext('MessageInputFlat'); const { channel } = useChatContext('MessageInputFlat'); @@ -96,23 +88,6 @@ export const MessageInputFlat = < [attachments], ); - const accept = useMemo( - () => - acceptedFiles.reduce>>((mediaTypeMap, mediaType) => { - mediaTypeMap[mediaType] ??= []; - return mediaTypeMap; - }, {}), - [acceptedFiles], - ); - - const { getRootProps, isDragActive, isDragReject } = useDropzone({ - accept, - disabled: !isUploadEnabled || maxFilesLeft === 0, - multiple: multipleUploads, - noClick: true, - onDrop: uploadNewFiles, - }); - useEffect(() => { const handleQuotedMessageUpdate = (e: Event) => { if (e.message?.id !== quotedMessage?.id) return; @@ -156,90 +131,76 @@ export const MessageInputFlat = < !!StopAIGenerationButton; return ( - <> -
- {recordingEnabled && - recordingController.permissionState === 'denied' && - showRecordingPermissionDeniedNotification && ( - - )} - {findAndEnqueueURLsToEnrich && ( - - )} - {isDragActive && ( -
- {!isDragReject &&

{t('Drag your files here')}

} - {isDragReject &&

{t('Some of the files will not be accepted')}

} -
+ + {recordingEnabled && + recordingController.permissionState === 'denied' && + showRecordingPermissionDeniedNotification && ( + )} - {displayQuotedMessage && } - -
- -
- {displayQuotedMessage && ( - + {findAndEnqueueURLsToEnrich && ( + + )} + {displayQuotedMessage && } + +
+ +
+ {displayQuotedMessage && } + {isUploadEnabled && + !!(numberOfUploads + failedUploadsCount || attachments.length > 0) && ( + )} - {isUploadEnabled && - !!(numberOfUploads + failedUploadsCount || attachments.length > 0) && ( - - )} -
- +
+ - {EmojiPicker && } -
+ {EmojiPicker && }
- {shouldDisplayStopAIGeneration ? ( - - ) : ( - !hideSendButton && ( - <> - {cooldownRemaining ? ( - + {shouldDisplayStopAIGeneration ? ( + + ) : ( + !hideSendButton && ( + <> + {cooldownRemaining ? ( + + ) : ( + <> + - ) : ( - <> - a.type === RecordingAttachmentType.VOICE_RECORDING, + )) } - sendMessage={handleSubmit} + onClick={() => { + recordingController.recorder?.start(); + setShowRecordingPermissionDeniedNotification(true); + }} /> - {recordingEnabled && ( - a.type === RecordingAttachmentType.VOICE_RECORDING, - )) - } - onClick={() => { - recordingController.recorder?.start(); - setShowRecordingPermissionDeniedNotification(true); - }} - /> - )} - - )} - - ) - )} -
+ )} + + )} + + ) + )}
- + ); }; diff --git a/src/components/MessageInput/WithDragAndDropUpload.tsx b/src/components/MessageInput/WithDragAndDropUpload.tsx new file mode 100644 index 0000000000..29a77f2fb8 --- /dev/null +++ b/src/components/MessageInput/WithDragAndDropUpload.tsx @@ -0,0 +1,150 @@ +import React, { + CSSProperties, + ElementType, + PropsWithChildren, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { + MessageInputContextValue, + useChannelStateContext, + useMessageInputContext, + useTranslationContext, +} from '../../context'; +import { useDropzone } from 'react-dropzone'; +import clsx from 'clsx'; + +const DragAndDropUploadContext = React.createContext<{ + fileQueue: File[]; + addFilesToQueue: ((files: File[]) => void) | null; + removeFilesFromQueue: ((files: File[]) => void) | null; +}>({ + addFilesToQueue: null, + fileQueue: [], + removeFilesFromQueue: null, +}); + +export const useDragAndDropUploadContext = () => useContext(DragAndDropUploadContext); + +/** + * @private To maintain top -> bottom data flow, the drag-and-drop functionality allows dragging any files to the queue - the closest + * `MessageInputProvider` will be notified through `DragAndDropUploadContext.fileQueue` and starts the upload with `uploadNewAttachments`, + * forwarded files are removed from the queue immediately after. + */ +export const useHandleDragAndDropQueuedFiles = ({ + uploadNewFiles, +}: MessageInputContextValue) => { + const { fileQueue, removeFilesFromQueue } = useDragAndDropUploadContext(); + + useEffect(() => { + if (!removeFilesFromQueue) return; + + uploadNewFiles(fileQueue); + + removeFilesFromQueue(fileQueue); + }, [fileQueue, removeFilesFromQueue, uploadNewFiles]); +}; + +/** + * Wrapper to replace now deprecated `Channel.dragAndDropWindow` option. + * + * @example + * ```tsx + * + * + * + * + * + * + * + * + * + * ``` + */ +export const WithDragAndDropUpload = ({ + children, + className, + component: Component = 'div', + style, +}: PropsWithChildren<{ + /** + * @description An element to render as a wrapper onto which drag & drop functionality will be applied. + * @default 'div' + */ + component?: ElementType; + className?: string; + style?: CSSProperties; +}>) => { + const [files, setFiles] = useState([]); + const { acceptedFiles = [], multipleUploads } = useChannelStateContext(); + const { t } = useTranslationContext(); + + const messageInputContext = useMessageInputContext(); + const dragAndDropUploadContext = useDragAndDropUploadContext(); + + // if message input context is available, there's no need to use the queue + const isWithinMessageInputContext = + typeof messageInputContext.uploadNewFiles === 'function'; + + const accept = useMemo( + () => + acceptedFiles.reduce>>((mediaTypeMap, mediaType) => { + mediaTypeMap[mediaType] ??= []; + return mediaTypeMap; + }, {}), + [acceptedFiles], + ); + + const addFilesToQueue = useCallback((files: File[]) => { + setFiles((cv) => cv.concat(files)); + }, []); + + const removeFilesFromQueue = useCallback((files: File[]) => { + if (!files.length) return; + setFiles((cv) => cv.filter((f) => files.indexOf(f) === -1)); + }, []); + + const { getRootProps, isDragActive, isDragReject } = useDropzone({ + accept, + // apply `disabled` rules if available, otherwise allow anything and + // let the `uploadNewFiles` handle the limitations internally + disabled: isWithinMessageInputContext + ? !messageInputContext.isUploadEnabled || messageInputContext.maxFilesLeft === 0 + : false, + multiple: multipleUploads, + noClick: true, + onDrop: isWithinMessageInputContext + ? messageInputContext.uploadNewFiles + : addFilesToQueue, + }); + + // nested WithDragAndDropUpload components render wrappers without functionality + // (MessageInputFlat has a default WithDragAndDropUpload) + if (dragAndDropUploadContext.removeFilesFromQueue !== null) { + return {children}; + } + + return ( + + + {/* TODO: could be a replaceable component */} + {isDragActive && ( +
+ {!isDragReject &&

{t('Drag your files here')}

} + {isDragReject &&

{t('Some of the files will not be accepted')}

} +
+ )} + {children} +
+
+ ); +}; diff --git a/src/components/MessageInput/index.ts b/src/components/MessageInput/index.ts index 46c2d6e13e..0f0bbaa0db 100644 --- a/src/components/MessageInput/index.ts +++ b/src/components/MessageInput/index.ts @@ -18,4 +18,5 @@ export * from './MessageInput'; export * from './MessageInputFlat'; export * from './QuotedMessagePreview'; export * from './SendButton'; +export { WithDragAndDropUpload } from './WithDragAndDropUpload'; export * from './types'; diff --git a/src/context/MessageInputContext.tsx b/src/context/MessageInputContext.tsx index 95228c9ecb..587e4e7beb 100644 --- a/src/context/MessageInputContext.tsx +++ b/src/context/MessageInputContext.tsx @@ -44,15 +44,12 @@ export const useMessageInputContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, V extends CustomTrigger = CustomTrigger, >( + // eslint-disable-next-line @typescript-eslint/no-unused-vars componentName?: string, ) => { const contextValue = useContext(MessageInputContext); if (!contextValue) { - console.warn( - `The useMessageInputContext hook was called outside of the MessageInputContext provider. Make sure this hook is called within the MessageInput's UI component. The errored call is located in the ${componentName} component.`, - ); - return {} as MessageInputContextValue; } From 1beadf232e70a1d4b4ee1d54389343af05f2b6d8 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Fri, 25 Apr 2025 13:10:49 +0200 Subject: [PATCH 2/5] refactor: simplify WithDragAndDropUpload API (#2691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🎯 Goal Yesterday evening I reconsidered how the wrapper should work - this way it's much more simpler and allows multiple `MessageInput` components to work under one wrapper without any clashing. Since the previous feature has not been released yet, we can change the context API without any worry and thus `refactor`. --- src/components/MessageInput/MessageInput.tsx | 4 +- .../MessageInput/WithDragAndDropUpload.tsx | 56 +++++++++---------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/components/MessageInput/MessageInput.tsx b/src/components/MessageInput/MessageInput.tsx index 55b67502bb..b2899ed70c 100644 --- a/src/components/MessageInput/MessageInput.tsx +++ b/src/components/MessageInput/MessageInput.tsx @@ -26,7 +26,7 @@ import type { } from '../../types/types'; import type { URLEnrichmentConfig } from './hooks/useLinkPreviews'; import type { CustomAudioRecordingConfig } from '../MediaRecorder'; -import { useHandleDragAndDropQueuedFiles } from './WithDragAndDropUpload'; +import { useRegisterDropHandlers } from './WithDragAndDropUpload'; export type EmojiSearchIndexResult = { id: string; @@ -153,7 +153,7 @@ const MessageInputProvider = < }); // @ts-expect-error generics to be removed - useHandleDragAndDropQueuedFiles(messageInputContextValue); + useRegisterDropHandlers(messageInputContextValue); return ( value={messageInputContextValue}> diff --git a/src/components/MessageInput/WithDragAndDropUpload.tsx b/src/components/MessageInput/WithDragAndDropUpload.tsx index 29a77f2fb8..849cd86b64 100644 --- a/src/components/MessageInput/WithDragAndDropUpload.tsx +++ b/src/components/MessageInput/WithDragAndDropUpload.tsx @@ -6,7 +6,7 @@ import React, { useContext, useEffect, useMemo, - useState, + useRef, } from 'react'; import { MessageInputContextValue, @@ -18,34 +18,27 @@ import { useDropzone } from 'react-dropzone'; import clsx from 'clsx'; const DragAndDropUploadContext = React.createContext<{ - fileQueue: File[]; - addFilesToQueue: ((files: File[]) => void) | null; - removeFilesFromQueue: ((files: File[]) => void) | null; + subscribeToDrop: ((fn: (files: File[]) => void) => () => void) | null; }>({ - addFilesToQueue: null, - fileQueue: [], - removeFilesFromQueue: null, + subscribeToDrop: null, }); export const useDragAndDropUploadContext = () => useContext(DragAndDropUploadContext); /** - * @private To maintain top -> bottom data flow, the drag-and-drop functionality allows dragging any files to the queue - the closest - * `MessageInputProvider` will be notified through `DragAndDropUploadContext.fileQueue` and starts the upload with `uploadNewAttachments`, - * forwarded files are removed from the queue immediately after. + * @private This hook should be used only once directly in the `MessageInputProvider` to + * register `uploadNewFiles` functions of the rendered `MessageInputs`. Each `MessageInput` + * will then be notified when the drop event occurs from within the `WithDragAndDropUpload` + * component. */ -export const useHandleDragAndDropQueuedFiles = ({ - uploadNewFiles, -}: MessageInputContextValue) => { - const { fileQueue, removeFilesFromQueue } = useDragAndDropUploadContext(); +export const useRegisterDropHandlers = ({ uploadNewFiles }: MessageInputContextValue) => { + const { subscribeToDrop } = useDragAndDropUploadContext(); useEffect(() => { - if (!removeFilesFromQueue) return; + const unsubscribe = subscribeToDrop?.(uploadNewFiles); - uploadNewFiles(fileQueue); - - removeFilesFromQueue(fileQueue); - }, [fileQueue, removeFilesFromQueue, uploadNewFiles]); + return unsubscribe; + }, [subscribeToDrop, uploadNewFiles]); }; /** @@ -78,7 +71,7 @@ export const WithDragAndDropUpload = ({ className?: string; style?: CSSProperties; }>) => { - const [files, setFiles] = useState([]); + const dropHandlersRef = useRef void>>(new Set()); const { acceptedFiles = [], multipleUploads } = useChannelStateContext(); const { t } = useTranslationContext(); @@ -98,13 +91,16 @@ export const WithDragAndDropUpload = ({ [acceptedFiles], ); - const addFilesToQueue = useCallback((files: File[]) => { - setFiles((cv) => cv.concat(files)); + const subscribeToDrop = useCallback((fn: (files: File[]) => void) => { + dropHandlersRef.current.add(fn); + + return () => { + dropHandlersRef.current.delete(fn); + }; }, []); - const removeFilesFromQueue = useCallback((files: File[]) => { - if (!files.length) return; - setFiles((cv) => cv.filter((f) => files.indexOf(f) === -1)); + const handleDrop = useCallback((files: File[]) => { + dropHandlersRef.current.forEach((fn) => fn(files)); }, []); const { getRootProps, isDragActive, isDragReject } = useDropzone({ @@ -116,20 +112,20 @@ export const WithDragAndDropUpload = ({ : false, multiple: multipleUploads, noClick: true, - onDrop: isWithinMessageInputContext - ? messageInputContext.uploadNewFiles - : addFilesToQueue, + onDrop: isWithinMessageInputContext ? messageInputContext.uploadNewFiles : handleDrop, }); // nested WithDragAndDropUpload components render wrappers without functionality // (MessageInputFlat has a default WithDragAndDropUpload) - if (dragAndDropUploadContext.removeFilesFromQueue !== null) { + if (dragAndDropUploadContext.subscribeToDrop !== null) { return {children}; } return ( {/* TODO: could be a replaceable component */} From 8dbb73cd1bcd30a7440eadc64a8629afa3739bdf Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 2 May 2025 11:07:31 +0000 Subject: [PATCH 3/5] chore(release): 12.15.0 [skip ci] ## [12.15.0](https://github.com/GetStream/stream-chat-react/compare/v12.14.0...v12.15.0) (2025-05-02) ### Features * introduce WithDragAndDropUpload component ([#2688](https://github.com/GetStream/stream-chat-react/issues/2688)) ([6b03abd](https://github.com/GetStream/stream-chat-react/commit/6b03abd707165d08539af435b940dd13025481d2)) ### Chores * **deps:** upgrade @stream-io/stream-chat-css to v5.8.1 ([#2689](https://github.com/GetStream/stream-chat-react/issues/2689)) ([d0c32e3](https://github.com/GetStream/stream-chat-react/commit/d0c32e33225c2e72bf4f14f368c34fa3a34c543c)) ### Refactors * simplify WithDragAndDropUpload API ([#2691](https://github.com/GetStream/stream-chat-react/issues/2691)) ([46c9add](https://github.com/GetStream/stream-chat-react/commit/46c9add73d8c37ed65cd0c2808c148199820889a)) --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ef3f92b3..3bc1b0da11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [12.15.0](https://github.com/GetStream/stream-chat-react/compare/v12.14.0...v12.15.0) (2025-05-02) + +### Features + +* introduce WithDragAndDropUpload component ([#2688](https://github.com/GetStream/stream-chat-react/issues/2688)) ([6b03abd](https://github.com/GetStream/stream-chat-react/commit/6b03abd707165d08539af435b940dd13025481d2)) + +### Chores + +* **deps:** upgrade @stream-io/stream-chat-css to v5.8.1 ([#2689](https://github.com/GetStream/stream-chat-react/issues/2689)) ([d0c32e3](https://github.com/GetStream/stream-chat-react/commit/d0c32e33225c2e72bf4f14f368c34fa3a34c543c)) + +### Refactors + +* simplify WithDragAndDropUpload API ([#2691](https://github.com/GetStream/stream-chat-react/issues/2691)) ([46c9add](https://github.com/GetStream/stream-chat-react/commit/46c9add73d8c37ed65cd0c2808c148199820889a)) + ## [12.14.0](https://github.com/GetStream/stream-chat-react/compare/v12.13.1...v12.14.0) (2025-04-08) ### Features From 97710eaf84f8bcb721864f982981b4b0911a6dcf Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Thu, 8 May 2025 10:30:00 +0200 Subject: [PATCH 4/5] fix: prevent sorting poll options in place (#2699) (cherry picked from commit 88590f194eb4d8a0627a4ca7b8876c1166094ca0) --- src/components/Poll/PollActions/PollResults/PollResults.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Poll/PollActions/PollResults/PollResults.tsx b/src/components/Poll/PollActions/PollResults/PollResults.tsx index 3797f1b8b4..91b81f399b 100644 --- a/src/components/Poll/PollActions/PollResults/PollResults.tsx +++ b/src/components/Poll/PollActions/PollResults/PollResults.tsx @@ -21,7 +21,7 @@ const pollStateSelector = < nextValue: PollState, ): PollStateSelectorReturnValue => ({ name: nextValue.name, - options: nextValue.options, + options: [...nextValue.options], vote_counts_by_option: nextValue.vote_counts_by_option, }); From 1834befd3636a01c75ffc90157d5a8843d009ce2 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 8 May 2025 11:41:45 +0200 Subject: [PATCH 5/5] fix: validate the field for max number of votes correctly --- .../Poll/PollCreationDialog/PollCreationDialog.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx index 9d8b09b6a7..3ba21c682c 100644 --- a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx +++ b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx @@ -123,8 +123,9 @@ export const PollCreationDialog = ({ close }: PollCreationDialogProps) => { id='max_votes_allowed' onChange={(e) => { const isValidValue = - !e.target.value || - e.target.value.match(VALID_MAX_VOTES_VALUE_REGEX); + e.target.validity.valid && + (!e.target.value || + e.target.value.match(VALID_MAX_VOTES_VALUE_REGEX)); if (!isValidValue) { setMultipleAnswerCountError( t('Type a number from 2 to 10'),