- {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'),