diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index 4ef776d11d..c7b1bf0eff 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -5,7 +5,6 @@ import { ChannelSort, LocalMessage, TextComposerMiddleware, - Event, createCommandInjectionMiddleware, createDraftCommandInjectionMiddleware, createActiveCommandGuardMiddleware, @@ -28,6 +27,7 @@ import { Window, WithComponents, ReactionsList, + WithDragAndDropUpload, } from 'stream-chat-react'; import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis'; import { init, SearchIndex } from 'emoji-mart'; @@ -38,6 +38,14 @@ import { useAppSettingsState } from './AppSettings'; init({ data }); +const parseUserIdFromToken = (token: string) => { + const [, payload] = token.split('.'); + + if (!payload) throw new Error('Token is missing'); + + return JSON.parse(atob(payload))?.user_id; +}; + const apiKey = import.meta.env.VITE_STREAM_API_KEY; const token = new URLSearchParams(window.location.search).get('token') || @@ -79,7 +87,7 @@ const useUser = () => { }, [userId]); const tokenProvider = useCallback(() => { - return token + return token && userId === parseUserIdFromToken(token) ? Promise.resolve(token) : fetch( `https://pronto.getstream.io/api/auth/create-token?environment=shared-chat-redesign&user_id=${userId}`, @@ -190,18 +198,22 @@ const App = () => { additionalChannelSearchProps={{ searchForChannels: true }} /> - - - - - - - + + + + + + + + + + + diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index be264710e9..bb96e23edf 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -90,16 +90,18 @@ body { //max-width: none; } + .str-chat__dropzone-root--thread, .str-chat__thread-list-container, .str-chat__thread-container { - flex: 0 0 360px; + //flex: 0 0 360px; max-width: 360px; } .str-chat__chat-view__threads { + .str-chat__dropzone-root--thread, .str-chat__thread-container { flex: 1 1 auto; - min-width: 360px; + //min-width: 360px; max-width: none; } } @@ -119,7 +121,7 @@ body { } } - @container (max-width: 760px) { + @container (max-width: 860px) { .str-chat__channel-list, .str-chat__chat-view__selector { display: none; diff --git a/examples/vite/src/stream-imports-layout.scss b/examples/vite/src/stream-imports-layout.scss index b3bbaebc36..7d4b8d4468 100644 --- a/examples/vite/src/stream-imports-layout.scss +++ b/examples/vite/src/stream-imports-layout.scss @@ -16,8 +16,8 @@ @use 'stream-chat-react/dist/scss/v2/common/CTAButton/CTAButton-layout'; @use 'stream-chat-react/dist/scss/v2/common/CircleFAButton/CircleFAButton-layout'; //@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-layout'; -@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-layout'; -@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-layout'; // X +//@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-layout'; +//@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-layout'; // X //@use 'stream-chat-react/dist/scss/v2/EditMessageForm/EditMessageForm-layout'; //@use 'stream-chat-react/dist/scss/v2/Form/Form-layout'; @use 'stream-chat-react/dist/scss/v2/ImageCarousel/ImageCarousel-layout'; diff --git a/examples/vite/src/stream-imports-theme.scss b/examples/vite/src/stream-imports-theme.scss index 3d9efd72dc..6d284451e1 100644 --- a/examples/vite/src/stream-imports-theme.scss +++ b/examples/vite/src/stream-imports-theme.scss @@ -14,8 +14,8 @@ @use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-theme'; @use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-theme'; //@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-theme'; -@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-theme'; -@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-theme'; +//@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-theme'; +//@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-theme'; //@use 'stream-chat-react/dist/scss/v2/Form/Form-theme'; //@use 'stream-chat-react/dist/scss/v2/Icon/Icon-theme'; @use 'stream-chat-react/dist/scss/v2/ImageCarousel/ImageCarousel-theme'; diff --git a/src/components/Dialog/base/ContextMenu.tsx b/src/components/Dialog/base/ContextMenu.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/DragAndDrop/styling/DragAndDropContainer.scss b/src/components/DragAndDrop/styling/DragAndDropContainer.scss new file mode 100644 index 0000000000..cc5c9613ef --- /dev/null +++ b/src/components/DragAndDrop/styling/DragAndDropContainer.scss @@ -0,0 +1,51 @@ +// Ported from stream-chat-css DragAndDropContainer (layout + theme) + +.str-chat { + /* Top border of the component */ + --str-chat__drag-and-drop-container-border-block-start: 2px solid transparent; + + /* Bottom border of the component */ + --str-chat__drag-and-drop-container-border-block-end: 2px solid transparent; + + /* Top border of the component on dragover */ + --str-chat__drag-and-drop-container-on-dragover-border-block-start: 2px solid + var(--str-chat__primary-color); + + /* Bottom border of the component on dragover */ + --str-chat__drag-and-drop-container-on-dragover-border-block-end: 2px solid + var(--str-chat__primary-color); + + /* Left (right in RTL layout) border of the component on dragover */ + --str-chat__drag-and-drop-container-on-dragover-border-inline-start: none; + + /* Right (left in RTL layout) border of the component on dragover */ + --str-chat__drag-and-drop-container-on-dragover-border-inline-end: none; +} + +.str-chat__drag-and-drop-container--dragging { + cursor: grabbing; +} + +.str-chat__drag-and-drop-container__item[draggable='true'] { + cursor: grab; + + &:active { + background: transparent; + } +} + +.str-chat__drag-and-drop-container__item { + display: flex; + width: 100%; + padding-block: 0.25rem; + border-bottom: var(--str-chat__drag-and-drop-container-border-block-start); + border-top: var(--str-chat__drag-and-drop-container-border-block-start); + + &.str-chat__drag-and-drop-container__item--dragged-over-from-top { + border-bottom: var(--str-chat__drag-and-drop-container-on-dragover-border-block-end); + } + + &.str-chat__drag-and-drop-container__item--dragged-over-from-bottom { + border-top: var(--str-chat__drag-and-drop-container-on-dragover-border-block-start); + } +} diff --git a/src/components/DragAndDrop/styling/index.scss b/src/components/DragAndDrop/styling/index.scss new file mode 100644 index 0000000000..47a721ee82 --- /dev/null +++ b/src/components/DragAndDrop/styling/index.scss @@ -0,0 +1 @@ +@use "DragAndDropContainer"; \ No newline at end of file diff --git a/src/components/MessageInput/WithDragAndDropUpload.tsx b/src/components/MessageInput/WithDragAndDropUpload.tsx index d9bf4f54ec..83c5ba360d 100644 --- a/src/components/MessageInput/WithDragAndDropUpload.tsx +++ b/src/components/MessageInput/WithDragAndDropUpload.tsx @@ -8,6 +8,7 @@ import { useMessageInputContext, useTranslationContext } from '../../context'; import { useAttachmentManagerState, useMessageComposer } from './hooks'; import { useStateStore } from '../../store'; import { useIsCooldownActive } from './hooks/useIsCooldownActive'; +import { IconFileArrowLeftIn } from '../Icons'; const DragAndDropUploadContext = React.createContext<{ subscribeToDrop: ((fn: (files: File[]) => void) => () => void) | null; @@ -72,8 +73,6 @@ export const WithDragAndDropUpload = ({ style?: CSSProperties; }>) => { const dropHandlersRef = useRef void>>(new Set()); - const { t } = useTranslationContext(); - const messageInputContext = useMessageInputContext(); const dragAndDropUploadContext = useDragAndDropUploadContext(); const messageComposer = useMessageComposer(); @@ -108,7 +107,11 @@ export const WithDragAndDropUpload = ({ dropHandlersRef.current.forEach((fn) => fn(files)); }, []); - const { getRootProps, isDragActive, isDragReject } = useDropzone({ + const { + getRootProps, + isDragActive, + isDragReject: isDragRejected, + } = useDropzone({ accept, // apply `disabled` rules if available, otherwise allow anything and // let the `uploadNewFiles` handle the limitations internally @@ -126,22 +129,19 @@ export const WithDragAndDropUpload = ({ return {children}; } + const rootClassName = clsx('str-chat__dropzone-root', className); + 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} @@ -149,3 +149,25 @@ export const WithDragAndDropUpload = ({
); }; + +export type FileDragAndDropContentProps = { + isDragRejected: boolean; +}; + +export const FileDragAndDropContent = ({ + isDragRejected, +}: FileDragAndDropContentProps) => { + const { t } = useTranslationContext(); + return ( +
+ {isDragRejected ? ( +

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

+ ) : ( + <> + +

{t('Drag your files here')}

+ + )} +
+ ); +}; diff --git a/src/components/MessageInput/index.ts b/src/components/MessageInput/index.ts index 37be037392..f9d2a64cd4 100644 --- a/src/components/MessageInput/index.ts +++ b/src/components/MessageInput/index.ts @@ -18,4 +18,8 @@ export * from './MessageInput'; export * from './MessageInputFlat'; export * from './QuotedMessagePreview'; export * from './SendButton'; -export { WithDragAndDropUpload } from './WithDragAndDropUpload'; +export { + FileDragAndDropContent, + type FileDragAndDropContentProps, + WithDragAndDropUpload, +} from './WithDragAndDropUpload'; diff --git a/src/components/MessageInput/styling/DropzoneContainer.scss b/src/components/MessageInput/styling/DropzoneContainer.scss new file mode 100644 index 0000000000..d973b437ea --- /dev/null +++ b/src/components/MessageInput/styling/DropzoneContainer.scss @@ -0,0 +1,65 @@ +.str-chat { + /* The text/icon color of the dropzone container */ + --str-chat__dropzone-container-color: var(--text-primary); + + /* The background color of the dropzone container */ + --str-chat__dropzone-container-background-color: var(--background-core-overlay-light); + + /* The backdrop filter applied to the dropzone container */ + --str-chat__dropzone-container-backdrop-filter: blur(3.5px); +} + +.str-chat__dropzone-root { + position: relative; + min-height: 0; + min-width: 0; + max-width: 100%; + max-height: 100%; + flex: 1; +} + +// When wrapper has no content (e.g. Thread returns null when closed), take no space — keeps wrapper mounted to avoid remounts. +.str-chat__dropzone-root:empty { + display: none; +} + +.str-chat__dropzone-container { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + inset: 0; + z-index: 5; + background-color: var(--str-chat__dropzone-container-background-color); + color: var(--str-chat__dropzone-container-color); + backdrop-filter: var(--str-chat__dropzone-container-backdrop-filter); + font: var(--str-chat__heading-sm-text); + + .str-chat__dropzone-container__content { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-xs, 8px); + } + + svg { + height: 32px; + width: 32px; + } + + p { + margin: unset; + + } +} + +// When backdrop-filter is not supported, use a dimmed background only (no blur). +@supports not (backdrop-filter: blur(1px)) { + .str-chat__dropzone-container { + backdrop-filter: none; + background-color: var( + --str-chat__dropzone-container-background-color-fallback, + rgba(0, 0, 0, 0.4) + ); + } +} \ No newline at end of file diff --git a/src/components/MessageInput/styling/index.scss b/src/components/MessageInput/styling/index.scss index 26fe1a357f..fdd206e8f4 100644 --- a/src/components/MessageInput/styling/index.scss +++ b/src/components/MessageInput/styling/index.scss @@ -2,6 +2,7 @@ @use 'AttachmentPreviewThumbnail'; @use 'AttachmentSelector'; @use 'CommandChip'; +@use 'DropzoneContainer'; @use 'CommandsMenu'; @use 'LinkPreviewList'; @use 'MessageComposer'; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 5784a73ba1..296ea30e58 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -13,6 +13,7 @@ import type { EmojiSearchIndex, EmptyStateIndicatorProps, EventComponentProps, + FileDragAndDropContentProps, FixedHeightMessageProps, GalleryProps, GiphyPreviewMessageProps, @@ -103,6 +104,8 @@ export type ComponentContextValue = { CooldownTimer?: React.ComponentType; /** Custom UI component for date separators, defaults to and accepts same props as: [DateSeparator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/DateSeparator.tsx) */ DateSeparator?: React.ComponentType; + /** Custom UI component to display the contents on file drag-and-drop overlay, defaults to and accepts same props as: [FileDragAndDropContent](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/WithDragAndDropUpload.tsx) */ + FileDragAndDropContent?: React.ComponentType; /** Custom UI component to override default preview of edited message, defaults to and accepts same props as: [EditedMessagePreview](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/EditedMessagePreview.tsx) */ EditedMessagePreview?: React.ComponentType; /** Custom UI component for rendering button with emoji picker in MessageInput */ diff --git a/src/styling/index.scss b/src/styling/index.scss index 8f1748f55f..c5687314af 100644 --- a/src/styling/index.scss +++ b/src/styling/index.scss @@ -28,6 +28,7 @@ @use '../components/Channel/styling' as Channel; @use '../components/ChatView/styling' as ChatView; @use '../components/DateSeparator/styling' as DateSeparator; +@use '../components/DragAndDrop/styling' as DragAndDrop; @use '../components/Gallery/styling' as Gallery; @use '../components/MediaRecorder/AudioRecorder/styling' as AudioRecorder; @use '../components/Message/styling' as Message;