Skip to content

Commit d2a72b5

Browse files
authored
feat: redesign file drag-and-drop overlay (#2959)
1 parent 2859c76 commit d2a72b5

13 files changed

Lines changed: 197 additions & 35 deletions

File tree

examples/vite/src/App.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
ChannelSort,
66
LocalMessage,
77
TextComposerMiddleware,
8-
Event,
98
createCommandInjectionMiddleware,
109
createDraftCommandInjectionMiddleware,
1110
createActiveCommandGuardMiddleware,
@@ -28,6 +27,7 @@ import {
2827
Window,
2928
WithComponents,
3029
ReactionsList,
30+
WithDragAndDropUpload,
3131
} from 'stream-chat-react';
3232
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
3333
import { init, SearchIndex } from 'emoji-mart';
@@ -38,6 +38,14 @@ import { useAppSettingsState } from './AppSettings';
3838

3939
init({ data });
4040

41+
const parseUserIdFromToken = (token: string) => {
42+
const [, payload] = token.split('.');
43+
44+
if (!payload) throw new Error('Token is missing');
45+
46+
return JSON.parse(atob(payload))?.user_id;
47+
};
48+
4149
const apiKey = import.meta.env.VITE_STREAM_API_KEY;
4250
const token =
4351
new URLSearchParams(window.location.search).get('token') ||
@@ -79,7 +87,7 @@ const useUser = () => {
7987
}, [userId]);
8088

8189
const tokenProvider = useCallback(() => {
82-
return token
90+
return token && userId === parseUserIdFromToken(token)
8391
? Promise.resolve(token)
8492
: fetch(
8593
`https://pronto.getstream.io/api/auth/create-token?environment=shared-chat-redesign&user_id=${userId}`,
@@ -190,18 +198,22 @@ const App = () => {
190198
additionalChannelSearchProps={{ searchForChannels: true }}
191199
/>
192200
<Channel>
193-
<Window>
194-
<ChannelHeader Avatar={ChannelAvatar} />
195-
<MessageList returnAllReadData />
196-
<AIStateIndicator />
197-
<MessageInput
198-
focus
199-
audioRecordingEnabled
200-
maxRows={10}
201-
asyncMessagesMultiSendEnabled
202-
/>
203-
</Window>
204-
<Thread virtualized />
201+
<WithDragAndDropUpload>
202+
<Window>
203+
<ChannelHeader Avatar={ChannelAvatar} />
204+
<MessageList returnAllReadData />
205+
<AIStateIndicator />
206+
<MessageInput
207+
focus
208+
audioRecordingEnabled
209+
maxRows={10}
210+
asyncMessagesMultiSendEnabled
211+
/>
212+
</Window>
213+
</WithDragAndDropUpload>
214+
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
215+
<Thread virtualized />
216+
</WithDragAndDropUpload>
205217
</Channel>
206218
</ChatView.Channels>
207219
<ChatView.Threads>

examples/vite/src/index.scss

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,18 @@ body {
9090
//max-width: none;
9191
}
9292

93+
.str-chat__dropzone-root--thread,
9394
.str-chat__thread-list-container,
9495
.str-chat__thread-container {
95-
flex: 0 0 360px;
96+
//flex: 0 0 360px;
9697
max-width: 360px;
9798
}
9899

99100
.str-chat__chat-view__threads {
101+
.str-chat__dropzone-root--thread,
100102
.str-chat__thread-container {
101103
flex: 1 1 auto;
102-
min-width: 360px;
104+
//min-width: 360px;
103105
max-width: none;
104106
}
105107
}
@@ -119,7 +121,7 @@ body {
119121
}
120122
}
121123

122-
@container (max-width: 760px) {
124+
@container (max-width: 860px) {
123125
.str-chat__channel-list,
124126
.str-chat__chat-view__selector {
125127
display: none;

examples/vite/src/stream-imports-layout.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
@use 'stream-chat-react/dist/scss/v2/common/CTAButton/CTAButton-layout';
1717
@use 'stream-chat-react/dist/scss/v2/common/CircleFAButton/CircleFAButton-layout';
1818
//@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-layout';
19-
@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-layout';
20-
@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-layout'; // X
19+
//@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-layout';
20+
//@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-layout'; // X
2121
//@use 'stream-chat-react/dist/scss/v2/EditMessageForm/EditMessageForm-layout';
2222
//@use 'stream-chat-react/dist/scss/v2/Form/Form-layout';
2323
@use 'stream-chat-react/dist/scss/v2/ImageCarousel/ImageCarousel-layout';

examples/vite/src/stream-imports-theme.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
@use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-theme';
1515
@use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-theme';
1616
//@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-theme';
17-
@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-theme';
18-
@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-theme';
17+
//@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-theme';
18+
//@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-theme';
1919
//@use 'stream-chat-react/dist/scss/v2/Form/Form-theme';
2020
//@use 'stream-chat-react/dist/scss/v2/Icon/Icon-theme';
2121
@use 'stream-chat-react/dist/scss/v2/ImageCarousel/ImageCarousel-theme';

src/components/Dialog/base/ContextMenu.tsx

Whitespace-only changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Ported from stream-chat-css DragAndDropContainer (layout + theme)
2+
3+
.str-chat {
4+
/* Top border of the component */
5+
--str-chat__drag-and-drop-container-border-block-start: 2px solid transparent;
6+
7+
/* Bottom border of the component */
8+
--str-chat__drag-and-drop-container-border-block-end: 2px solid transparent;
9+
10+
/* Top border of the component on dragover */
11+
--str-chat__drag-and-drop-container-on-dragover-border-block-start: 2px solid
12+
var(--str-chat__primary-color);
13+
14+
/* Bottom border of the component on dragover */
15+
--str-chat__drag-and-drop-container-on-dragover-border-block-end: 2px solid
16+
var(--str-chat__primary-color);
17+
18+
/* Left (right in RTL layout) border of the component on dragover */
19+
--str-chat__drag-and-drop-container-on-dragover-border-inline-start: none;
20+
21+
/* Right (left in RTL layout) border of the component on dragover */
22+
--str-chat__drag-and-drop-container-on-dragover-border-inline-end: none;
23+
}
24+
25+
.str-chat__drag-and-drop-container--dragging {
26+
cursor: grabbing;
27+
}
28+
29+
.str-chat__drag-and-drop-container__item[draggable='true'] {
30+
cursor: grab;
31+
32+
&:active {
33+
background: transparent;
34+
}
35+
}
36+
37+
.str-chat__drag-and-drop-container__item {
38+
display: flex;
39+
width: 100%;
40+
padding-block: 0.25rem;
41+
border-bottom: var(--str-chat__drag-and-drop-container-border-block-start);
42+
border-top: var(--str-chat__drag-and-drop-container-border-block-start);
43+
44+
&.str-chat__drag-and-drop-container__item--dragged-over-from-top {
45+
border-bottom: var(--str-chat__drag-and-drop-container-on-dragover-border-block-end);
46+
}
47+
48+
&.str-chat__drag-and-drop-container__item--dragged-over-from-bottom {
49+
border-top: var(--str-chat__drag-and-drop-container-on-dragover-border-block-start);
50+
}
51+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@use "DragAndDropContainer";

src/components/MessageInput/WithDragAndDropUpload.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useMessageInputContext, useTranslationContext } from '../../context';
88
import { useAttachmentManagerState, useMessageComposer } from './hooks';
99
import { useStateStore } from '../../store';
1010
import { useIsCooldownActive } from './hooks/useIsCooldownActive';
11+
import { IconFileArrowLeftIn } from '../Icons';
1112

1213
const DragAndDropUploadContext = React.createContext<{
1314
subscribeToDrop: ((fn: (files: File[]) => void) => () => void) | null;
@@ -72,8 +73,6 @@ export const WithDragAndDropUpload = ({
7273
style?: CSSProperties;
7374
}>) => {
7475
const dropHandlersRef = useRef<Set<(f: File[]) => void>>(new Set());
75-
const { t } = useTranslationContext();
76-
7776
const messageInputContext = useMessageInputContext();
7877
const dragAndDropUploadContext = useDragAndDropUploadContext();
7978
const messageComposer = useMessageComposer();
@@ -108,7 +107,11 @@ export const WithDragAndDropUpload = ({
108107
dropHandlersRef.current.forEach((fn) => fn(files));
109108
}, []);
110109

111-
const { getRootProps, isDragActive, isDragReject } = useDropzone({
110+
const {
111+
getRootProps,
112+
isDragActive,
113+
isDragReject: isDragRejected,
114+
} = useDropzone({
112115
accept,
113116
// apply `disabled` rules if available, otherwise allow anything and
114117
// let the `uploadNewFiles` handle the limitations internally
@@ -126,26 +129,45 @@ export const WithDragAndDropUpload = ({
126129
return <Component className={className}>{children}</Component>;
127130
}
128131

132+
const rootClassName = clsx('str-chat__dropzone-root', className);
133+
129134
return (
130-
<DragAndDropUploadContext.Provider
131-
value={{
132-
subscribeToDrop,
133-
}}
134-
>
135-
<Component {...getRootProps({ className, style })}>
136-
{/* TODO: could be a replaceable component */}
135+
<DragAndDropUploadContext.Provider value={{ subscribeToDrop }}>
136+
<Component {...getRootProps({ className: rootClassName, style })}>
137137
{isDragActive && (
138138
<div
139139
className={clsx('str-chat__dropzone-container', {
140-
'str-chat__dropzone-container--not-accepted': isDragReject,
140+
'str-chat__dropzone-container--not-accepted': isDragRejected,
141141
})}
142+
role='presentation'
142143
>
143-
{!isDragReject && <p>{t('Drag your files here')}</p>}
144-
{isDragReject && <p>{t('Some of the files will not be accepted')}</p>}
144+
<FileDragAndDropContent isDragRejected={isDragRejected} />
145145
</div>
146146
)}
147147
{children}
148148
</Component>
149149
</DragAndDropUploadContext.Provider>
150150
);
151151
};
152+
153+
export type FileDragAndDropContentProps = {
154+
isDragRejected: boolean;
155+
};
156+
157+
export const FileDragAndDropContent = ({
158+
isDragRejected,
159+
}: FileDragAndDropContentProps) => {
160+
const { t } = useTranslationContext();
161+
return (
162+
<div className='str-chat__dropzone-container__content'>
163+
{isDragRejected ? (
164+
<p>{t('Some of the files will not be accepted')}</p>
165+
) : (
166+
<>
167+
<IconFileArrowLeftIn />
168+
<p>{t('Drag your files here')}</p>
169+
</>
170+
)}
171+
</div>
172+
);
173+
};

src/components/MessageInput/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ export * from './MessageInput';
1919
export * from './MessageInputFlat';
2020
export * from './QuotedMessagePreview';
2121
export * from './SendButton';
22-
export { WithDragAndDropUpload } from './WithDragAndDropUpload';
22+
export {
23+
FileDragAndDropContent,
24+
type FileDragAndDropContentProps,
25+
WithDragAndDropUpload,
26+
} from './WithDragAndDropUpload';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.str-chat {
2+
/* The text/icon color of the dropzone container */
3+
--str-chat__dropzone-container-color: var(--text-primary);
4+
5+
/* The background color of the dropzone container */
6+
--str-chat__dropzone-container-background-color: var(--background-core-overlay-light);
7+
8+
/* The backdrop filter applied to the dropzone container */
9+
--str-chat__dropzone-container-backdrop-filter: blur(3.5px);
10+
}
11+
12+
.str-chat__dropzone-root {
13+
position: relative;
14+
min-height: 0;
15+
min-width: 0;
16+
max-width: 100%;
17+
max-height: 100%;
18+
flex: 1;
19+
}
20+
21+
// When wrapper has no content (e.g. Thread returns null when closed), take no space — keeps wrapper mounted to avoid remounts.
22+
.str-chat__dropzone-root:empty {
23+
display: none;
24+
}
25+
26+
.str-chat__dropzone-container {
27+
display: flex;
28+
align-items: center;
29+
justify-content: center;
30+
position: absolute;
31+
inset: 0;
32+
z-index: 5;
33+
background-color: var(--str-chat__dropzone-container-background-color);
34+
color: var(--str-chat__dropzone-container-color);
35+
backdrop-filter: var(--str-chat__dropzone-container-backdrop-filter);
36+
font: var(--str-chat__heading-sm-text);
37+
38+
.str-chat__dropzone-container__content {
39+
display: flex;
40+
flex-direction: column;
41+
align-items: center;
42+
gap: var(--spacing-xs, 8px);
43+
}
44+
45+
svg {
46+
height: 32px;
47+
width: 32px;
48+
}
49+
50+
p {
51+
margin: unset;
52+
53+
}
54+
}
55+
56+
// When backdrop-filter is not supported, use a dimmed background only (no blur).
57+
@supports not (backdrop-filter: blur(1px)) {
58+
.str-chat__dropzone-container {
59+
backdrop-filter: none;
60+
background-color: var(
61+
--str-chat__dropzone-container-background-color-fallback,
62+
rgba(0, 0, 0, 0.4)
63+
);
64+
}
65+
}

0 commit comments

Comments
 (0)