@linktr.ee/messaging-react 2.1.0 → 2.2.0-rc-1778753733
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-CsJvUF_b.js → Card-BdTueeyk.js} +2 -2
- package/dist/{Card-CsJvUF_b.js.map → Card-BdTueeyk.js.map} +1 -1
- package/dist/{Card-DlMSDSdm.js → Card-ChR37pLZ.js} +2 -2
- package/dist/{Card-DlMSDSdm.js.map → Card-ChR37pLZ.js.map} +1 -1
- package/dist/{Card-CFFNq49v.js → Card-EKxCn56j.js} +3 -3
- package/dist/{Card-CFFNq49v.js.map → Card-EKxCn56j.js.map} +1 -1
- package/dist/{LockedThumbnail-DpJx169C.js → LockedThumbnail-B16qP3eH.js} +2 -2
- package/dist/{LockedThumbnail-DpJx169C.js.map → LockedThumbnail-B16qP3eH.js.map} +1 -1
- package/dist/index-Dn7BC9xK.js +4748 -0
- package/dist/index-Dn7BC9xK.js.map +1 -0
- package/dist/index.d.ts +591 -25
- package/dist/index.js +24 -19
- package/package.json +1 -1
- package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +841 -0
- package/src/components/LinkAttachment/LinkAttachment.stories.tsx +7 -92
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +69 -0
- package/src/components/LinkAttachment/components/Received/Card.tsx +10 -30
- package/src/components/LinkAttachment/components/_shared/CardShell.tsx +5 -1
- package/src/components/LinkAttachment/index.tsx +24 -50
- package/src/components/LinkAttachment/types.ts +12 -5
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +203 -0
- package/src/components/MessageAttachment/Audio/index.tsx +189 -0
- package/src/components/MessageAttachment/File/FileAttachment.stories.tsx +352 -0
- package/src/components/MessageAttachment/File/index.tsx +240 -0
- package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +288 -0
- package/src/components/MessageAttachment/Image/index.tsx +257 -0
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +783 -0
- package/src/components/MessageAttachment/Pdf/PdfAttachment.stories.tsx +292 -0
- package/src/components/MessageAttachment/Pdf/index.tsx +228 -0
- package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +272 -0
- package/src/components/MessageAttachment/Video/index.tsx +281 -0
- package/src/components/MessageAttachment/_shared/Bubble.tsx +173 -0
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +152 -0
- package/src/components/MessageAttachment/_shared/DismissButton.tsx +39 -0
- package/src/components/MessageAttachment/_shared/DownloadAction.tsx +175 -0
- package/src/components/MessageAttachment/_shared/ImageViewer.tsx +314 -0
- package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +139 -0
- package/src/components/MessageAttachment/_shared/PdfViewer.tsx +100 -0
- package/src/components/MessageAttachment/_shared/VideoViewer.tsx +171 -0
- package/src/components/MessageAttachment/_shared/ViewerShell.tsx +159 -0
- package/src/components/MessageAttachment/_shared/fileMeta.test.ts +82 -0
- package/src/components/MessageAttachment/_shared/fileMeta.ts +95 -0
- package/src/components/MessageAttachment/_shared/triggerDownload.ts +54 -0
- package/src/components/MessageAttachment/_shared/useViewer.ts +53 -0
- package/src/components/MessageAttachment/index.tsx +149 -0
- package/src/components/MessageAttachment/stories/StoryTable.tsx +72 -0
- package/src/components/MessageAttachment/types.ts +178 -0
- package/src/index.ts +32 -0
- package/dist/Card-D32U6KfZ.js +0 -85
- package/dist/Card-D32U6KfZ.js.map +0 -1
- package/dist/Card-DlSSJPip.js +0 -60
- package/dist/Card-DlSSJPip.js.map +0 -1
- package/dist/Card-zGbhRBwv.js +0 -48
- package/dist/Card-zGbhRBwv.js.map +0 -1
- package/dist/CardThumbnail-DTBuRQHF.js +0 -239
- package/dist/CardThumbnail-DTBuRQHF.js.map +0 -1
- package/dist/index-DfcRe-Hj.js +0 -3103
- package/dist/index-DfcRe-Hj.js.map +0 -1
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
currentUserArgType,
|
|
7
|
+
StoryUser,
|
|
8
|
+
storyUsers,
|
|
9
|
+
} from '../../stories/decorators/storyUser'
|
|
10
|
+
import { Avatar } from '../Avatar'
|
|
11
|
+
import MessageAttachment, {
|
|
12
|
+
type BubbleGroupPosition,
|
|
13
|
+
} from '../MessageAttachment'
|
|
14
|
+
|
|
15
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
16
|
+
* Lightweight conversation mock used only by these stories.
|
|
17
|
+
*
|
|
18
|
+
* The production `CustomMessage` flow needs the full stream-chat-react
|
|
19
|
+
* machinery (Channel + MessageList + the `Attachment` override pipe).
|
|
20
|
+
* That's overkill for a visual showcase, so these stories render a
|
|
21
|
+
* bare conversation timeline that mimics the same layout primitives:
|
|
22
|
+
*
|
|
23
|
+
* - Sender (current user) bubbles align right, no avatar.
|
|
24
|
+
* - Recipient bubbles align left with a 28px circle avatar.
|
|
25
|
+
* - Adjacent bubbles from the same user gap by 2px (matches the
|
|
26
|
+
* `.str-chat__message-bubble-wrapper { gap: 2px }` rule in
|
|
27
|
+
* `styles.css`), separate-author boundaries get more breathing
|
|
28
|
+
* room.
|
|
29
|
+
* - Text bubbles inherit the same `MessageAttachment.Bubble` chrome
|
|
30
|
+
* (background, padding, radius, font) so attachments and text sit
|
|
31
|
+
* consistently inside the same conversation column.
|
|
32
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
type ChatRole = 'sender' | 'receiver'
|
|
35
|
+
|
|
36
|
+
interface ChatNode {
|
|
37
|
+
id: string
|
|
38
|
+
role: ChatRole
|
|
39
|
+
/** Either a string (rendered as a text bubble) or a custom React node
|
|
40
|
+
* (e.g. `<MessageAttachment.Image.Sent ... />`). */
|
|
41
|
+
content: React.ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TEXT_BG_BY_ROLE: Record<ChatRole, string> = {
|
|
45
|
+
sender: 'bg-[#121110] text-white',
|
|
46
|
+
receiver: 'bg-[#e9eaed] text-[#080707]',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const TextBubble: React.FC<{ role: ChatRole; children: React.ReactNode }> = ({
|
|
50
|
+
role,
|
|
51
|
+
children,
|
|
52
|
+
}) => (
|
|
53
|
+
<div
|
|
54
|
+
className={classNames(
|
|
55
|
+
// Same 18px radius + 8px / 16px padding as
|
|
56
|
+
// `MessageAttachment.Bubble`, so text bubbles share the same
|
|
57
|
+
// chrome as attachment bubbles.
|
|
58
|
+
'max-w-[280px] whitespace-pre-wrap break-words rounded-[18px] px-4 py-2',
|
|
59
|
+
TEXT_BG_BY_ROLE[role]
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const ChatRow: React.FC<{
|
|
67
|
+
role: ChatRole
|
|
68
|
+
showAvatar: boolean
|
|
69
|
+
user: StoryUser
|
|
70
|
+
children: React.ReactNode
|
|
71
|
+
}> = ({ role, showAvatar, user, children }) => (
|
|
72
|
+
<div
|
|
73
|
+
className={classNames(
|
|
74
|
+
'flex items-end gap-2',
|
|
75
|
+
role === 'sender' ? 'justify-end' : 'justify-start'
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{role === 'receiver' ? (
|
|
79
|
+
<div className="w-7 shrink-0">
|
|
80
|
+
{showAvatar ? (
|
|
81
|
+
<Avatar
|
|
82
|
+
id={user.id}
|
|
83
|
+
name={user.name}
|
|
84
|
+
image={user.image}
|
|
85
|
+
size={28}
|
|
86
|
+
shape="circle"
|
|
87
|
+
/>
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
) : null}
|
|
91
|
+
<div className={role === 'sender' ? 'flex justify-end' : 'flex justify-start'}>
|
|
92
|
+
{children}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const ChatMock: React.FC<{
|
|
98
|
+
currentUser: StoryUser
|
|
99
|
+
receiver: StoryUser
|
|
100
|
+
messages: ChatNode[]
|
|
101
|
+
}> = ({ currentUser, receiver, messages }) => {
|
|
102
|
+
// Render an avatar on the *last* message of each receiver run — same
|
|
103
|
+
// grouping rule the real chat uses so consecutive bubbles from one
|
|
104
|
+
// sender share avatar real estate.
|
|
105
|
+
const isLastInRun = (index: number): boolean => {
|
|
106
|
+
const next = messages[index + 1]
|
|
107
|
+
return !next || next.role !== messages[index].role
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="mx-auto flex max-w-2xl flex-col gap-1 p-12">
|
|
112
|
+
{messages.map((msg, index) => {
|
|
113
|
+
const prev = messages[index - 1]
|
|
114
|
+
const newAuthorBoundary = !prev || prev.role !== msg.role
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
key={msg.id}
|
|
118
|
+
className={newAuthorBoundary && index > 0 ? 'mt-3' : undefined}
|
|
119
|
+
>
|
|
120
|
+
<ChatRow
|
|
121
|
+
role={msg.role}
|
|
122
|
+
user={msg.role === 'sender' ? currentUser : receiver}
|
|
123
|
+
showAvatar={msg.role === 'receiver' && isLastInRun(index)}
|
|
124
|
+
>
|
|
125
|
+
{typeof msg.content === 'string' ? (
|
|
126
|
+
<TextBubble role={msg.role}>{msg.content}</TextBubble>
|
|
127
|
+
) : (
|
|
128
|
+
msg.content
|
|
129
|
+
)}
|
|
130
|
+
</ChatRow>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
139
|
+
* Multi-attachment message wrapper.
|
|
140
|
+
*
|
|
141
|
+
* Real chat UIs let one logical "message" carry several attachments
|
|
142
|
+
* of different kinds — e.g. a voice memo + a PDF, or two photos + an
|
|
143
|
+
* audio note. Each attachment renders as its own `MessageAttachment.*`
|
|
144
|
+
* bubble, but the whole set should read as a single visual cluster
|
|
145
|
+
* from the same author. To do that we:
|
|
146
|
+
*
|
|
147
|
+
* - Stack the bubbles vertically with `gap-[2px]` (the same gap
|
|
148
|
+
* `.str-chat__message-bubble-wrapper` uses between grouped text
|
|
149
|
+
* bubbles in `styles.css`).
|
|
150
|
+
* - Inject `groupPosition='first' | 'middle' | 'end'` on each child
|
|
151
|
+
* so the corners flatten on the edges that face siblings inside
|
|
152
|
+
* the cluster — same merging rule the `Bubble` component already
|
|
153
|
+
* applies to consecutive text messages.
|
|
154
|
+
* - Right-align for sender clusters / left-align for receiver
|
|
155
|
+
* clusters so the run anchors to the correct chat edge.
|
|
156
|
+
*
|
|
157
|
+
* The wrapper is intentionally local to this stories file: the real
|
|
158
|
+
* `CustomMessage` integration derives `groupPosition` from
|
|
159
|
+
* stream-chat-react's `firstOfGroup` / `endOfGroup` flags via the
|
|
160
|
+
* `bubbleGroupPositionFromStream` helper instead. This wrapper just
|
|
161
|
+
* gives the story author a clean way to express "one message, many
|
|
162
|
+
* attachments" without wiring up the full message-context plumbing.
|
|
163
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
164
|
+
|
|
165
|
+
interface MultiAttachmentMessageProps {
|
|
166
|
+
align: 'sender' | 'receiver'
|
|
167
|
+
children: React.ReactNode
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const MultiAttachmentMessage: React.FC<MultiAttachmentMessageProps> = ({
|
|
171
|
+
align,
|
|
172
|
+
children,
|
|
173
|
+
}) => {
|
|
174
|
+
const items = React.Children.toArray(children).filter(
|
|
175
|
+
React.isValidElement
|
|
176
|
+
) as React.ReactElement<{ groupPosition?: BubbleGroupPosition }>[]
|
|
177
|
+
const total = items.length
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
className={classNames(
|
|
182
|
+
'flex flex-col gap-[2px]',
|
|
183
|
+
align === 'sender' ? 'items-end' : 'items-start'
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{items.map((child, index) => {
|
|
187
|
+
const groupPosition: BubbleGroupPosition =
|
|
188
|
+
total === 1
|
|
189
|
+
? 'single'
|
|
190
|
+
: index === 0
|
|
191
|
+
? 'first'
|
|
192
|
+
: index === total - 1
|
|
193
|
+
? 'end'
|
|
194
|
+
: 'middle'
|
|
195
|
+
return React.cloneElement(child, {
|
|
196
|
+
key: child.key ?? index,
|
|
197
|
+
groupPosition,
|
|
198
|
+
})
|
|
199
|
+
})}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const meta: Meta = {
|
|
205
|
+
title: 'CustomMessage',
|
|
206
|
+
argTypes: currentUserArgType,
|
|
207
|
+
args: { currentUser: storyUsers.creator },
|
|
208
|
+
parameters: {
|
|
209
|
+
layout: 'fullscreen',
|
|
210
|
+
// The render functions build a `messages` array containing JSX
|
|
211
|
+
// attachment elements (with live React fiber `_owner` refs once
|
|
212
|
+
// mounted) and hand it to `<ChatMock messages={…}>`. Storybook's
|
|
213
|
+
// default `dynamic` source extractor recurses through that prop
|
|
214
|
+
// via `react-element-to-jsx-string` → `prettyPrint`, walks each
|
|
215
|
+
// element's fiber graph, and blows the call stack. Pin source
|
|
216
|
+
// generation to the static story code instead.
|
|
217
|
+
docs: { source: { type: 'code' } },
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
export default meta
|
|
221
|
+
|
|
222
|
+
interface ConversationStoryArgs {
|
|
223
|
+
currentUser: StoryUser
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const HERO_PHOTO = 'https://picsum.photos/seed/portrait/720/720'
|
|
227
|
+
const STUDIO_PHOTOS = [
|
|
228
|
+
{ src: 'https://picsum.photos/seed/studio-1/720/720', alt: 'Studio shot 1' },
|
|
229
|
+
{ src: 'https://picsum.photos/seed/studio-2/720/720', alt: 'Studio shot 2' },
|
|
230
|
+
{ src: 'https://picsum.photos/seed/studio-3/720/720', alt: 'Studio shot 3' },
|
|
231
|
+
]
|
|
232
|
+
const VIDEO_SRC = '/video-source.mp4'
|
|
233
|
+
const VIDEO_POSTER = '/video-thumbnail.jpg'
|
|
234
|
+
const AUDIO_SRC =
|
|
235
|
+
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
|
236
|
+
const PDF_SRC = 'https://www.w3.org/WAI/WCAG21/wcag21.pdf'
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Image attachments inside a conversation — the receiver shares a
|
|
240
|
+
* single hero shot (with caption) and the sender replies with a
|
|
241
|
+
* stacked album. Click any bubble to open the lightbox viewer.
|
|
242
|
+
*/
|
|
243
|
+
export const ConversationWithImages: StoryFn<ConversationStoryArgs> = ({
|
|
244
|
+
currentUser,
|
|
245
|
+
}) => {
|
|
246
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
247
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
248
|
+
return (
|
|
249
|
+
<ChatMock
|
|
250
|
+
currentUser={currentUser}
|
|
251
|
+
receiver={receiver}
|
|
252
|
+
messages={[
|
|
253
|
+
{
|
|
254
|
+
id: 'msg-1',
|
|
255
|
+
role: 'receiver',
|
|
256
|
+
content: 'Got the new headshots back — thoughts?',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: 'msg-2',
|
|
260
|
+
role: 'receiver',
|
|
261
|
+
content: (
|
|
262
|
+
<MessageAttachment.Image.Received
|
|
263
|
+
src={HERO_PHOTO}
|
|
264
|
+
alt="Headshot pick"
|
|
265
|
+
filename="headshot-pick.jpg"
|
|
266
|
+
text="This one's my favorite"
|
|
267
|
+
/>
|
|
268
|
+
),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'msg-3',
|
|
272
|
+
role: 'sender',
|
|
273
|
+
content: 'Love it! Here are a few more from the studio set.',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: 'msg-4',
|
|
277
|
+
role: 'sender',
|
|
278
|
+
content: (
|
|
279
|
+
<MessageAttachment.Image.Sent
|
|
280
|
+
items={STUDIO_PHOTOS}
|
|
281
|
+
filename="studio-album"
|
|
282
|
+
/>
|
|
283
|
+
),
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: 'msg-5',
|
|
287
|
+
role: 'receiver',
|
|
288
|
+
content: 'These are great — let me pick two for the launch page.',
|
|
289
|
+
},
|
|
290
|
+
]}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Video attachments — receiver sends a single clip with a caption,
|
|
297
|
+
* sender replies and shares back a stacked set.
|
|
298
|
+
*/
|
|
299
|
+
export const ConversationWithVideos: StoryFn<ConversationStoryArgs> = ({
|
|
300
|
+
currentUser,
|
|
301
|
+
}) => {
|
|
302
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
303
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
304
|
+
const stack = Array.from({ length: 3 }, () => ({
|
|
305
|
+
src: VIDEO_SRC,
|
|
306
|
+
poster: VIDEO_POSTER,
|
|
307
|
+
mimeType: 'video/mp4',
|
|
308
|
+
}))
|
|
309
|
+
return (
|
|
310
|
+
<ChatMock
|
|
311
|
+
currentUser={currentUser}
|
|
312
|
+
receiver={receiver}
|
|
313
|
+
messages={[
|
|
314
|
+
{
|
|
315
|
+
id: 'msg-1',
|
|
316
|
+
role: 'receiver',
|
|
317
|
+
content: (
|
|
318
|
+
<MessageAttachment.Image.Received
|
|
319
|
+
src={VIDEO_POSTER}
|
|
320
|
+
alt="Reference still"
|
|
321
|
+
/>
|
|
322
|
+
),
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'msg-2',
|
|
326
|
+
role: 'receiver',
|
|
327
|
+
content: 'And here is the rough cut from yesterday.',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: 'msg-3',
|
|
331
|
+
role: 'receiver',
|
|
332
|
+
content: (
|
|
333
|
+
<MessageAttachment.Video.Received
|
|
334
|
+
src={VIDEO_SRC}
|
|
335
|
+
poster={VIDEO_POSTER}
|
|
336
|
+
mimeType="video/mp4"
|
|
337
|
+
filename="rough-cut.mp4"
|
|
338
|
+
text="Open ending — tell me which version lands."
|
|
339
|
+
/>
|
|
340
|
+
),
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: 'msg-4',
|
|
344
|
+
role: 'sender',
|
|
345
|
+
content: 'On it. Sending three angles back.',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: 'msg-5',
|
|
349
|
+
role: 'sender',
|
|
350
|
+
content: (
|
|
351
|
+
<MessageAttachment.Video.Sent items={stack} filename="angles" />
|
|
352
|
+
),
|
|
353
|
+
},
|
|
354
|
+
]}
|
|
355
|
+
/>
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Audio attachments — voice memo back-and-forth, plus a stacked
|
|
361
|
+
* studio takes message.
|
|
362
|
+
*/
|
|
363
|
+
export const ConversationWithAudio: StoryFn<ConversationStoryArgs> = ({
|
|
364
|
+
currentUser,
|
|
365
|
+
}) => {
|
|
366
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
367
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
368
|
+
return (
|
|
369
|
+
<ChatMock
|
|
370
|
+
currentUser={currentUser}
|
|
371
|
+
receiver={receiver}
|
|
372
|
+
messages={[
|
|
373
|
+
{
|
|
374
|
+
id: 'msg-1',
|
|
375
|
+
role: 'receiver',
|
|
376
|
+
content: 'Quick voice memo with the new hook idea ↓',
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: 'msg-2',
|
|
380
|
+
role: 'receiver',
|
|
381
|
+
content: (
|
|
382
|
+
<MessageAttachment.Audio.Received
|
|
383
|
+
src={AUDIO_SRC}
|
|
384
|
+
mimeType="audio/mpeg"
|
|
385
|
+
filename="hook-idea.mp3"
|
|
386
|
+
/>
|
|
387
|
+
),
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: 'msg-3',
|
|
391
|
+
role: 'sender',
|
|
392
|
+
content: 'Love that. Here are three takes I cut tonight.',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 'msg-4',
|
|
396
|
+
role: 'sender',
|
|
397
|
+
content: (
|
|
398
|
+
<MessageAttachment.Audio.Sent
|
|
399
|
+
items={[
|
|
400
|
+
{
|
|
401
|
+
src: AUDIO_SRC,
|
|
402
|
+
mimeType: 'audio/mpeg',
|
|
403
|
+
filename: 'Take 1 — full mix.mp3',
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
src: AUDIO_SRC,
|
|
407
|
+
mimeType: 'audio/mpeg',
|
|
408
|
+
filename: 'Take 2 — vocals isolated.mp3',
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
src: AUDIO_SRC,
|
|
412
|
+
mimeType: 'audio/mpeg',
|
|
413
|
+
filename: 'Take 3 — instrumental only.mp3',
|
|
414
|
+
},
|
|
415
|
+
]}
|
|
416
|
+
text="Pick whichever lands; happy to keep iterating."
|
|
417
|
+
/>
|
|
418
|
+
),
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: 'msg-5',
|
|
422
|
+
role: 'receiver',
|
|
423
|
+
content: 'Take 2 is the one. Shipping it.',
|
|
424
|
+
},
|
|
425
|
+
]}
|
|
426
|
+
/>
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* PDF attachments — single document with a caption and a stacked
|
|
432
|
+
* package of related documents.
|
|
433
|
+
*/
|
|
434
|
+
export const ConversationWithPdfs: StoryFn<ConversationStoryArgs> = ({
|
|
435
|
+
currentUser,
|
|
436
|
+
}) => {
|
|
437
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
438
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
439
|
+
return (
|
|
440
|
+
<ChatMock
|
|
441
|
+
currentUser={currentUser}
|
|
442
|
+
receiver={receiver}
|
|
443
|
+
messages={[
|
|
444
|
+
{
|
|
445
|
+
id: 'msg-1',
|
|
446
|
+
role: 'receiver',
|
|
447
|
+
content: 'Can you send the ESOP summary before the call?',
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: 'msg-2',
|
|
451
|
+
role: 'sender',
|
|
452
|
+
content: 'Yep, here it is.',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: 'msg-3',
|
|
456
|
+
role: 'sender',
|
|
457
|
+
content: (
|
|
458
|
+
<MessageAttachment.Pdf.Sent
|
|
459
|
+
src={PDF_SRC}
|
|
460
|
+
filename="ESOP-summary.pdf"
|
|
461
|
+
fileSize={388_658}
|
|
462
|
+
text="Section 4 has the vesting cliff details."
|
|
463
|
+
/>
|
|
464
|
+
),
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: 'msg-4',
|
|
468
|
+
role: 'receiver',
|
|
469
|
+
content: 'Got it. Sending back the full package for tonight.',
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: 'msg-5',
|
|
473
|
+
role: 'receiver',
|
|
474
|
+
content: (
|
|
475
|
+
<MessageAttachment.Pdf.Received
|
|
476
|
+
items={[
|
|
477
|
+
{
|
|
478
|
+
src: PDF_SRC,
|
|
479
|
+
filename: 'ESOP-summary.pdf',
|
|
480
|
+
fileSize: 388_658,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
src: PDF_SRC,
|
|
484
|
+
filename: 'Vesting-schedule.pdf',
|
|
485
|
+
fileSize: 142_336,
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
src: PDF_SRC,
|
|
489
|
+
filename: 'Cap-table-snapshot.pdf',
|
|
490
|
+
fileSize: 96_021,
|
|
491
|
+
},
|
|
492
|
+
]}
|
|
493
|
+
text="Full package — review whatever is most relevant."
|
|
494
|
+
/>
|
|
495
|
+
),
|
|
496
|
+
},
|
|
497
|
+
]}
|
|
498
|
+
/>
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Generic file attachments — non-PDF documents with a download
|
|
504
|
+
* affordance on each row. Includes a stacked-files example where
|
|
505
|
+
* three documents share a single bubble with an 8px gap between
|
|
506
|
+
* rows, matching the stacked PDF / Audio treatment.
|
|
507
|
+
*/
|
|
508
|
+
export const ConversationWithFiles: StoryFn<ConversationStoryArgs> = ({
|
|
509
|
+
currentUser,
|
|
510
|
+
}) => {
|
|
511
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
512
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
513
|
+
return (
|
|
514
|
+
<ChatMock
|
|
515
|
+
currentUser={currentUser}
|
|
516
|
+
receiver={receiver}
|
|
517
|
+
messages={[
|
|
518
|
+
{
|
|
519
|
+
id: 'msg-1',
|
|
520
|
+
role: 'receiver',
|
|
521
|
+
content: 'Do you still have the workout plan archive?',
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: 'msg-2',
|
|
525
|
+
role: 'sender',
|
|
526
|
+
content:
|
|
527
|
+
'Sending it now — let me know if you need the source files too.',
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
id: 'msg-3',
|
|
531
|
+
role: 'sender',
|
|
532
|
+
content: (
|
|
533
|
+
<MessageAttachment.File.Sent
|
|
534
|
+
src="https://cdn.example.com/workout-plan.zip"
|
|
535
|
+
filename="workout-plan.zip"
|
|
536
|
+
mimeType="application/zip"
|
|
537
|
+
fileSize={2_457_600}
|
|
538
|
+
/>
|
|
539
|
+
),
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: 'msg-4',
|
|
543
|
+
role: 'receiver',
|
|
544
|
+
content: 'Perfect — also got the brand kit handy?',
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
id: 'msg-5',
|
|
548
|
+
role: 'sender',
|
|
549
|
+
content: (
|
|
550
|
+
<MessageAttachment.File.Sent
|
|
551
|
+
items={[
|
|
552
|
+
{
|
|
553
|
+
src: 'https://cdn.example.com/brand-guidelines.zip',
|
|
554
|
+
filename: 'brand-guidelines.zip',
|
|
555
|
+
mimeType: 'application/zip',
|
|
556
|
+
fileSize: 11_500_000,
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
src: 'https://cdn.example.com/Q4-revenue.xlsx',
|
|
560
|
+
filename: 'Q4-revenue.xlsx',
|
|
561
|
+
mimeType:
|
|
562
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
563
|
+
fileSize: 146_432,
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
src: 'https://cdn.example.com/meeting-notes.docx',
|
|
567
|
+
filename: 'meeting-notes.docx',
|
|
568
|
+
mimeType:
|
|
569
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
570
|
+
fileSize: 28_672,
|
|
571
|
+
},
|
|
572
|
+
]}
|
|
573
|
+
text="Here's everything in one go."
|
|
574
|
+
/>
|
|
575
|
+
),
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: 'msg-6',
|
|
579
|
+
role: 'receiver',
|
|
580
|
+
content: 'Amazing, thanks!',
|
|
581
|
+
},
|
|
582
|
+
]}
|
|
583
|
+
/>
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Mixed conversation — a realistic chat where the same thread carries
|
|
589
|
+
* a text exchange, a single image, a stacked video set, an audio memo,
|
|
590
|
+
* and a PDF handoff. Useful for sanity-checking spacing and bubble
|
|
591
|
+
* rhythm when multiple attachment types coexist.
|
|
592
|
+
*/
|
|
593
|
+
export const ConversationWithMixedAttachments: StoryFn<ConversationStoryArgs> =
|
|
594
|
+
({ currentUser }) => {
|
|
595
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
596
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
597
|
+
return (
|
|
598
|
+
<ChatMock
|
|
599
|
+
currentUser={currentUser}
|
|
600
|
+
receiver={receiver}
|
|
601
|
+
messages={[
|
|
602
|
+
{
|
|
603
|
+
id: 'msg-1',
|
|
604
|
+
role: 'receiver',
|
|
605
|
+
content: 'Hey! Got a sec to look at the launch package?',
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: 'msg-2',
|
|
609
|
+
role: 'sender',
|
|
610
|
+
content: 'Yep, send it over.',
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
id: 'msg-3',
|
|
614
|
+
role: 'receiver',
|
|
615
|
+
content: 'Here is the hero shot for the page',
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
id: 'msg-4',
|
|
619
|
+
role: 'receiver',
|
|
620
|
+
content: (
|
|
621
|
+
<MessageAttachment.Image.Received
|
|
622
|
+
src={HERO_PHOTO}
|
|
623
|
+
alt="Hero shot"
|
|
624
|
+
filename="hero.jpg"
|
|
625
|
+
/>
|
|
626
|
+
),
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
id: 'msg-5',
|
|
630
|
+
role: 'receiver',
|
|
631
|
+
content: (
|
|
632
|
+
<MessageAttachment.Video.Received
|
|
633
|
+
src={VIDEO_SRC}
|
|
634
|
+
poster={VIDEO_POSTER}
|
|
635
|
+
mimeType="video/mp4"
|
|
636
|
+
filename="trailer.mp4"
|
|
637
|
+
text="And the rough trailer ↑"
|
|
638
|
+
/>
|
|
639
|
+
),
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: 'msg-6',
|
|
643
|
+
role: 'sender',
|
|
644
|
+
content: 'Looks great. Quick voice memo with notes ↓',
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
id: 'msg-7',
|
|
648
|
+
role: 'sender',
|
|
649
|
+
content: (
|
|
650
|
+
<MessageAttachment.Audio.Sent
|
|
651
|
+
src={AUDIO_SRC}
|
|
652
|
+
mimeType="audio/mpeg"
|
|
653
|
+
filename="notes.mp3"
|
|
654
|
+
/>
|
|
655
|
+
),
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: 'msg-8',
|
|
659
|
+
role: 'sender',
|
|
660
|
+
content: (
|
|
661
|
+
<MessageAttachment.Pdf.Sent
|
|
662
|
+
src={PDF_SRC}
|
|
663
|
+
filename="launch-checklist.pdf"
|
|
664
|
+
fileSize={142_336}
|
|
665
|
+
text="And the launch checklist for the dry run."
|
|
666
|
+
/>
|
|
667
|
+
),
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
id: 'msg-9',
|
|
671
|
+
role: 'receiver',
|
|
672
|
+
content: 'Got everything. Will sync back in the morning.',
|
|
673
|
+
},
|
|
674
|
+
]}
|
|
675
|
+
/>
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Multi-attachment message — audio + PDF.
|
|
681
|
+
*
|
|
682
|
+
* Mirrors the mobile picker UX where the composer can hold a single
|
|
683
|
+
* attachment but the recipient sees them sent together as one
|
|
684
|
+
* cluster. The voice memo sits on top (`groupPosition='first'`, flat
|
|
685
|
+
* bottom-right) and the PDF carries the caption + the cluster's tail
|
|
686
|
+
* corner (`groupPosition='end'`, flat top-right).
|
|
687
|
+
*/
|
|
688
|
+
export const ConversationWithAudioAndPdfAttached: StoryFn<
|
|
689
|
+
ConversationStoryArgs
|
|
690
|
+
> = ({ currentUser }) => {
|
|
691
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
692
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
693
|
+
return (
|
|
694
|
+
<ChatMock
|
|
695
|
+
currentUser={currentUser}
|
|
696
|
+
receiver={receiver}
|
|
697
|
+
messages={[
|
|
698
|
+
{
|
|
699
|
+
id: 'msg-1',
|
|
700
|
+
role: 'receiver',
|
|
701
|
+
content: 'Did you get a chance to record the walkthrough?',
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: 'msg-2',
|
|
705
|
+
role: 'sender',
|
|
706
|
+
content: (
|
|
707
|
+
<MultiAttachmentMessage align="sender">
|
|
708
|
+
<MessageAttachment.Audio.Sent
|
|
709
|
+
src={AUDIO_SRC}
|
|
710
|
+
mimeType="audio/mpeg"
|
|
711
|
+
filename="walkthrough.mp3"
|
|
712
|
+
/>
|
|
713
|
+
<MessageAttachment.Pdf.Sent
|
|
714
|
+
src={PDF_SRC}
|
|
715
|
+
filename="walkthrough-notes.pdf"
|
|
716
|
+
fileSize={142_336}
|
|
717
|
+
text="Voice memo + the written notes that go with it."
|
|
718
|
+
/>
|
|
719
|
+
</MultiAttachmentMessage>
|
|
720
|
+
),
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
id: 'msg-3',
|
|
724
|
+
role: 'receiver',
|
|
725
|
+
content: 'Perfect — listening now.',
|
|
726
|
+
},
|
|
727
|
+
]}
|
|
728
|
+
/>
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Multi-attachment message — two photos + a voice memo.
|
|
734
|
+
*
|
|
735
|
+
* Demonstrates a mixed sensory recap: the receiver shares two
|
|
736
|
+
* reference photos in one stacked image bubble (`groupPosition='first'`)
|
|
737
|
+
* paired with an audio clip explaining what to look at
|
|
738
|
+
* (`groupPosition='end'`, carries the caption). All three pieces feel
|
|
739
|
+
* like one unit instead of three loose bubbles.
|
|
740
|
+
*/
|
|
741
|
+
export const ConversationWithPhotosAndAudioAttached: StoryFn<
|
|
742
|
+
ConversationStoryArgs
|
|
743
|
+
> = ({ currentUser }) => {
|
|
744
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
745
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
746
|
+
return (
|
|
747
|
+
<ChatMock
|
|
748
|
+
currentUser={currentUser}
|
|
749
|
+
receiver={receiver}
|
|
750
|
+
messages={[
|
|
751
|
+
{
|
|
752
|
+
id: 'msg-1',
|
|
753
|
+
role: 'sender',
|
|
754
|
+
content: 'Anything stand out from the studio set?',
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
id: 'msg-2',
|
|
758
|
+
role: 'receiver',
|
|
759
|
+
content: (
|
|
760
|
+
<MultiAttachmentMessage align="receiver">
|
|
761
|
+
<MessageAttachment.Image.Received
|
|
762
|
+
items={STUDIO_PHOTOS.slice(0, 2)}
|
|
763
|
+
filename="studio-picks"
|
|
764
|
+
/>
|
|
765
|
+
<MessageAttachment.Audio.Received
|
|
766
|
+
src={AUDIO_SRC}
|
|
767
|
+
mimeType="audio/mpeg"
|
|
768
|
+
filename="reactions.mp3"
|
|
769
|
+
text="Two favorites — voice note has the why."
|
|
770
|
+
/>
|
|
771
|
+
</MultiAttachmentMessage>
|
|
772
|
+
),
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
id: 'msg-3',
|
|
776
|
+
role: 'sender',
|
|
777
|
+
content: 'Great picks. Locking those in.',
|
|
778
|
+
},
|
|
779
|
+
]}
|
|
780
|
+
/>
|
|
781
|
+
)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Multi-attachment message — image + video + PDF (3 different types).
|
|
786
|
+
*
|
|
787
|
+
* Stress-tests the cluster with three different bubble shapes back to
|
|
788
|
+
* back. Image first (cropped poster, flat bottom-right), video middle
|
|
789
|
+
* (poster tile with play badge, flats on top-right + bottom-right),
|
|
790
|
+
* PDF last (compact row with download trailing button + caption,
|
|
791
|
+
* flat top-right). The whole stack reads as one message run.
|
|
792
|
+
*/
|
|
793
|
+
export const ConversationWithImageVideoPdfAttached: StoryFn<
|
|
794
|
+
ConversationStoryArgs
|
|
795
|
+
> = ({ currentUser }) => {
|
|
796
|
+
const isSenderTheCreator = currentUser.id === storyUsers.creator.id
|
|
797
|
+
const receiver = isSenderTheCreator ? storyUsers.visitor : storyUsers.creator
|
|
798
|
+
return (
|
|
799
|
+
<ChatMock
|
|
800
|
+
currentUser={currentUser}
|
|
801
|
+
receiver={receiver}
|
|
802
|
+
messages={[
|
|
803
|
+
{
|
|
804
|
+
id: 'msg-1',
|
|
805
|
+
role: 'receiver',
|
|
806
|
+
content: 'Send everything you have for the launch deck.',
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
id: 'msg-2',
|
|
810
|
+
role: 'sender',
|
|
811
|
+
content: (
|
|
812
|
+
<MultiAttachmentMessage align="sender">
|
|
813
|
+
<MessageAttachment.Image.Sent
|
|
814
|
+
src={HERO_PHOTO}
|
|
815
|
+
alt="Hero shot"
|
|
816
|
+
filename="hero.jpg"
|
|
817
|
+
/>
|
|
818
|
+
<MessageAttachment.Video.Sent
|
|
819
|
+
src={VIDEO_SRC}
|
|
820
|
+
poster={VIDEO_POSTER}
|
|
821
|
+
mimeType="video/mp4"
|
|
822
|
+
filename="trailer.mp4"
|
|
823
|
+
/>
|
|
824
|
+
<MessageAttachment.Pdf.Sent
|
|
825
|
+
src={PDF_SRC}
|
|
826
|
+
filename="launch-deck.pdf"
|
|
827
|
+
fileSize={428_032}
|
|
828
|
+
text="Hero, trailer, deck — everything in one drop."
|
|
829
|
+
/>
|
|
830
|
+
</MultiAttachmentMessage>
|
|
831
|
+
),
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
id: 'msg-3',
|
|
835
|
+
role: 'receiver',
|
|
836
|
+
content: 'Got it. Reviewing now.',
|
|
837
|
+
},
|
|
838
|
+
]}
|
|
839
|
+
/>
|
|
840
|
+
)
|
|
841
|
+
}
|