@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,189 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import Bubble from '../_shared/Bubble'
|
|
4
|
+
import DismissButton from '../_shared/DismissButton'
|
|
5
|
+
import {
|
|
6
|
+
bubbleVariantForState,
|
|
7
|
+
type AudioItem,
|
|
8
|
+
type ComposerExtras,
|
|
9
|
+
type MediaPreloadMode,
|
|
10
|
+
type MessageAttachmentBaseProps,
|
|
11
|
+
type MessageAttachmentState,
|
|
12
|
+
} from '../types'
|
|
13
|
+
|
|
14
|
+
export interface AudioAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
15
|
+
/** Audio source URL (`mp3`, `aac`, `wav`, …). */
|
|
16
|
+
src?: string
|
|
17
|
+
/** MIME type hint — typed onto the inline `<source>` element. */
|
|
18
|
+
mimeType?: string
|
|
19
|
+
/**
|
|
20
|
+
* Filename — used as the download default name (consumed by the
|
|
21
|
+
* native player's kebab menu). The HTML `<audio>` element doesn't
|
|
22
|
+
* expose a built-in title slot, so we don't render the filename
|
|
23
|
+
* inside the bubble itself.
|
|
24
|
+
*/
|
|
25
|
+
filename?: string
|
|
26
|
+
/**
|
|
27
|
+
* Stacked audio. Takes precedence over `src` when set. Each item
|
|
28
|
+
* renders its own native `<audio controls>` player, vertically
|
|
29
|
+
* stacked inside the same bubble with an 8px gap between players.
|
|
30
|
+
* Sent + Received only — the composer surface accepts a single
|
|
31
|
+
* attachment at a time.
|
|
32
|
+
*/
|
|
33
|
+
items?: AudioItem[]
|
|
34
|
+
/**
|
|
35
|
+
* `<audio preload>` hint applied to every player on the bubble.
|
|
36
|
+
* When omitted, the default depends on the rendered shape:
|
|
37
|
+
*
|
|
38
|
+
* - Single audio (no `items`, or `items.length === 1`) →
|
|
39
|
+
* `'metadata'` so the native player surfaces duration
|
|
40
|
+
* immediately.
|
|
41
|
+
* - Stacked audio (`items.length > 1`) → `'none'` so a thread
|
|
42
|
+
* of voice memos doesn't fan out N parallel metadata requests
|
|
43
|
+
* on first paint.
|
|
44
|
+
*
|
|
45
|
+
* Per-track overrides live on `AudioItem.preload`.
|
|
46
|
+
*/
|
|
47
|
+
preload?: MediaPreloadMode
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolveItems = ({
|
|
51
|
+
src,
|
|
52
|
+
mimeType,
|
|
53
|
+
filename,
|
|
54
|
+
items,
|
|
55
|
+
}: {
|
|
56
|
+
src?: string
|
|
57
|
+
mimeType?: string
|
|
58
|
+
filename?: string
|
|
59
|
+
items?: AudioItem[]
|
|
60
|
+
}): AudioItem[] => {
|
|
61
|
+
if (items && items.length > 0) return items
|
|
62
|
+
if (src) return [{ src, mimeType, filename }]
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const NativeAudioPlayer: React.FC<{
|
|
67
|
+
item: AudioItem
|
|
68
|
+
preload: MediaPreloadMode
|
|
69
|
+
trailingAction?: React.ReactNode
|
|
70
|
+
}> = ({ item, preload, trailingAction }) => (
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
{/* No `<track>` is rendered — we don't author caption sidecars
|
|
73
|
+
for chat attachments, and an empty `<track kind="captions" />`
|
|
74
|
+
emits a runtime warning. Re-add a real track (with `src`,
|
|
75
|
+
`srcLang`, and `default`) once caption support actually ships.
|
|
76
|
+
The `jsx-a11y/media-has-caption` rule is suppressed for the
|
|
77
|
+
same reason. */}
|
|
78
|
+
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
|
79
|
+
<audio
|
|
80
|
+
src={item.src}
|
|
81
|
+
controls
|
|
82
|
+
preload={item.preload ?? preload}
|
|
83
|
+
className="block min-w-0 flex-1"
|
|
84
|
+
>
|
|
85
|
+
{item.mimeType ? <source src={item.src} type={item.mimeType} /> : null}
|
|
86
|
+
</audio>
|
|
87
|
+
{trailingAction ?? null}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
interface InternalAudioRowProps extends AudioAttachmentSharedProps {
|
|
92
|
+
state: MessageAttachmentState
|
|
93
|
+
onDismiss?: () => void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const AudioAttachmentRow: React.FC<InternalAudioRowProps> = ({
|
|
97
|
+
state,
|
|
98
|
+
src,
|
|
99
|
+
mimeType,
|
|
100
|
+
filename,
|
|
101
|
+
items,
|
|
102
|
+
text,
|
|
103
|
+
groupPosition,
|
|
104
|
+
preload,
|
|
105
|
+
onDismiss,
|
|
106
|
+
}) => {
|
|
107
|
+
const variant = bubbleVariantForState(state)
|
|
108
|
+
const showDismiss = state === 'composer' && !!onDismiss
|
|
109
|
+
const resolvedItems = resolveItems({ src, mimeType, filename, items })
|
|
110
|
+
|
|
111
|
+
if (resolvedItems.length === 0) {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Default rule: surface duration immediately for a single player,
|
|
116
|
+
// but avoid fanning out N metadata requests when the bubble is a
|
|
117
|
+
// thread of voice memos. An explicit `preload` prop overrides;
|
|
118
|
+
// per-track overrides live on `AudioItem.preload`.
|
|
119
|
+
const resolvedPreload: MediaPreloadMode =
|
|
120
|
+
preload ?? (resolvedItems.length > 1 ? 'none' : 'metadata')
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Bubble
|
|
124
|
+
variant={variant}
|
|
125
|
+
text={text}
|
|
126
|
+
groupPosition={groupPosition}
|
|
127
|
+
data-testid="audio-attachment"
|
|
128
|
+
>
|
|
129
|
+
{/* Native `<audio controls>` already exposes a download in its
|
|
130
|
+
kebab menu, so we don't render a separate Download button.
|
|
131
|
+
The element also has no built-in title slot — the dismiss
|
|
132
|
+
button (Composer only) sits inline next to the player.
|
|
133
|
+
Stacked players get an 8px vertical gap so each track reads
|
|
134
|
+
as a discrete attachment. */}
|
|
135
|
+
<div className="flex flex-col gap-2">
|
|
136
|
+
{resolvedItems.map((item, index) => (
|
|
137
|
+
<NativeAudioPlayer
|
|
138
|
+
key={`${item.src}-${index}`}
|
|
139
|
+
item={item}
|
|
140
|
+
preload={resolvedPreload}
|
|
141
|
+
trailingAction={
|
|
142
|
+
// Composer only supports a single attachment, so the
|
|
143
|
+
// dismiss control sits on the only player.
|
|
144
|
+
showDismiss && index === 0 ? (
|
|
145
|
+
<DismissButton onClick={onDismiss!} variant="inline" />
|
|
146
|
+
) : undefined
|
|
147
|
+
}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</Bubble>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Composer-only props. Single audio (`src`) only — the composer
|
|
157
|
+
* surface accepts a single attachment at a time, so `items` is not
|
|
158
|
+
* supported. Captions (`text`) and `groupPosition` are also dropped:
|
|
159
|
+
* the composer renders a standalone draft, not part of a same-author
|
|
160
|
+
* run, and captions live in the message-input textarea, not inside
|
|
161
|
+
* the draft attachment preview.
|
|
162
|
+
*/
|
|
163
|
+
export interface AudioComposerProps extends ComposerExtras {
|
|
164
|
+
src?: string
|
|
165
|
+
mimeType?: string
|
|
166
|
+
filename?: string
|
|
167
|
+
/** See `AudioAttachmentSharedProps.preload`. */
|
|
168
|
+
preload?: MediaPreloadMode
|
|
169
|
+
}
|
|
170
|
+
export type AudioSentProps = AudioAttachmentSharedProps
|
|
171
|
+
export type AudioReceivedProps = AudioAttachmentSharedProps
|
|
172
|
+
|
|
173
|
+
const AudioComposer: React.FC<AudioComposerProps> = (props) => (
|
|
174
|
+
<AudioAttachmentRow {...props} state="composer" />
|
|
175
|
+
)
|
|
176
|
+
const AudioSent: React.FC<AudioSentProps> = (props) => (
|
|
177
|
+
<AudioAttachmentRow {...props} state="sent" />
|
|
178
|
+
)
|
|
179
|
+
const AudioReceived: React.FC<AudioReceivedProps> = (props) => (
|
|
180
|
+
<AudioAttachmentRow {...props} state="received" />
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const AudioAttachment = {
|
|
184
|
+
Composer: AudioComposer,
|
|
185
|
+
Sent: AudioSent,
|
|
186
|
+
Received: AudioReceived,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default AudioAttachment
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
StoryGrid,
|
|
6
|
+
StoryHeading,
|
|
7
|
+
StoryPage,
|
|
8
|
+
StoryRow,
|
|
9
|
+
} from '../stories/StoryTable'
|
|
10
|
+
import type { FileItem } from '../types'
|
|
11
|
+
|
|
12
|
+
import FileAttachment from '.'
|
|
13
|
+
|
|
14
|
+
const FILE_SRC = 'https://www.w3.org/WAI/WCAG21/wcag21.pdf'
|
|
15
|
+
|
|
16
|
+
const STACK_FILES: FileItem[] = [
|
|
17
|
+
{
|
|
18
|
+
src: FILE_SRC,
|
|
19
|
+
filename: 'workout-plan.zip',
|
|
20
|
+
mimeType: 'application/zip',
|
|
21
|
+
fileSize: 2_457_600,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
src: FILE_SRC,
|
|
25
|
+
filename: 'Q4-revenue.xlsx',
|
|
26
|
+
mimeType:
|
|
27
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
28
|
+
fileSize: 146_432,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
src: FILE_SRC,
|
|
32
|
+
filename: 'brand-guidelines-2026.zip',
|
|
33
|
+
mimeType: 'application/zip',
|
|
34
|
+
fileSize: 11_500_000,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
src: FILE_SRC,
|
|
38
|
+
filename: 'meeting-notes.docx',
|
|
39
|
+
mimeType:
|
|
40
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
41
|
+
fileSize: 28_672,
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const meta: Meta = {
|
|
46
|
+
title: 'MessageAttachment/File',
|
|
47
|
+
parameters: { layout: 'fullscreen' },
|
|
48
|
+
}
|
|
49
|
+
export default meta
|
|
50
|
+
|
|
51
|
+
const handleDismiss = () => {
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.log('Dismissed file attachment')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compact file row inside a chat bubble — typed icon + filename +
|
|
58
|
+
* `EXT · SIZE` — with a download icon in the trailing slot. Clicking
|
|
59
|
+
* any state (Composer / Sent / Received) downloads the asset.
|
|
60
|
+
*/
|
|
61
|
+
export const Single: StoryFn = () => (
|
|
62
|
+
<StoryPage>
|
|
63
|
+
<StoryHeading
|
|
64
|
+
title="File attachment"
|
|
65
|
+
description="Click any bubble to download. Trailing icon hints at the download action."
|
|
66
|
+
/>
|
|
67
|
+
<StoryGrid>
|
|
68
|
+
<StoryRow
|
|
69
|
+
label="Generic"
|
|
70
|
+
composer={
|
|
71
|
+
<FileAttachment.Composer
|
|
72
|
+
src={FILE_SRC}
|
|
73
|
+
filename="workout-plan.zip"
|
|
74
|
+
mimeType="application/zip"
|
|
75
|
+
fileSize={2_457_600}
|
|
76
|
+
onDismiss={handleDismiss}
|
|
77
|
+
/>
|
|
78
|
+
}
|
|
79
|
+
sent={
|
|
80
|
+
<FileAttachment.Sent
|
|
81
|
+
src={FILE_SRC}
|
|
82
|
+
filename="workout-plan.zip"
|
|
83
|
+
mimeType="application/zip"
|
|
84
|
+
fileSize={2_457_600}
|
|
85
|
+
/>
|
|
86
|
+
}
|
|
87
|
+
received={
|
|
88
|
+
<FileAttachment.Received
|
|
89
|
+
src={FILE_SRC}
|
|
90
|
+
filename="workout-plan.zip"
|
|
91
|
+
mimeType="application/zip"
|
|
92
|
+
fileSize={2_457_600}
|
|
93
|
+
/>
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
<StoryRow
|
|
97
|
+
label="Spreadsheet"
|
|
98
|
+
composer={
|
|
99
|
+
<FileAttachment.Composer
|
|
100
|
+
src={FILE_SRC}
|
|
101
|
+
filename="Q4-revenue.xlsx"
|
|
102
|
+
mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
103
|
+
fileSize={146_432}
|
|
104
|
+
onDismiss={handleDismiss}
|
|
105
|
+
/>
|
|
106
|
+
}
|
|
107
|
+
sent={
|
|
108
|
+
<FileAttachment.Sent
|
|
109
|
+
src={FILE_SRC}
|
|
110
|
+
filename="Q4-revenue.xlsx"
|
|
111
|
+
mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
112
|
+
fileSize={146_432}
|
|
113
|
+
/>
|
|
114
|
+
}
|
|
115
|
+
received={
|
|
116
|
+
<FileAttachment.Received
|
|
117
|
+
src={FILE_SRC}
|
|
118
|
+
filename="Q4-revenue.xlsx"
|
|
119
|
+
mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
120
|
+
fileSize={146_432}
|
|
121
|
+
/>
|
|
122
|
+
}
|
|
123
|
+
/>
|
|
124
|
+
<StoryRow
|
|
125
|
+
label="Long filename"
|
|
126
|
+
composer={
|
|
127
|
+
<FileAttachment.Composer
|
|
128
|
+
src={FILE_SRC}
|
|
129
|
+
filename="brand-guidelines-2026-final-revision-with-color-tokens.zip"
|
|
130
|
+
mimeType="application/zip"
|
|
131
|
+
fileSize={11_500_000}
|
|
132
|
+
onDismiss={handleDismiss}
|
|
133
|
+
/>
|
|
134
|
+
}
|
|
135
|
+
sent={
|
|
136
|
+
<FileAttachment.Sent
|
|
137
|
+
src={FILE_SRC}
|
|
138
|
+
filename="brand-guidelines-2026-final-revision-with-color-tokens.zip"
|
|
139
|
+
mimeType="application/zip"
|
|
140
|
+
fileSize={11_500_000}
|
|
141
|
+
/>
|
|
142
|
+
}
|
|
143
|
+
received={
|
|
144
|
+
<FileAttachment.Received
|
|
145
|
+
src={FILE_SRC}
|
|
146
|
+
filename="brand-guidelines-2026-final-revision-with-color-tokens.zip"
|
|
147
|
+
mimeType="application/zip"
|
|
148
|
+
fileSize={11_500_000}
|
|
149
|
+
/>
|
|
150
|
+
}
|
|
151
|
+
/>
|
|
152
|
+
</StoryGrid>
|
|
153
|
+
</StoryPage>
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
export const SingleWithText: StoryFn = () => (
|
|
157
|
+
<StoryPage>
|
|
158
|
+
<StoryHeading
|
|
159
|
+
title="File with caption"
|
|
160
|
+
description="Caption sits inside the same bubble below the file row."
|
|
161
|
+
/>
|
|
162
|
+
<StoryGrid>
|
|
163
|
+
<StoryRow
|
|
164
|
+
label="Caption"
|
|
165
|
+
composer={
|
|
166
|
+
<FileAttachment.Composer
|
|
167
|
+
src={FILE_SRC}
|
|
168
|
+
filename="workout-plan.zip"
|
|
169
|
+
mimeType="application/zip"
|
|
170
|
+
fileSize={2_457_600}
|
|
171
|
+
onDismiss={handleDismiss}
|
|
172
|
+
/>
|
|
173
|
+
}
|
|
174
|
+
sent={
|
|
175
|
+
<FileAttachment.Sent
|
|
176
|
+
src={FILE_SRC}
|
|
177
|
+
filename="workout-plan.zip"
|
|
178
|
+
mimeType="application/zip"
|
|
179
|
+
fileSize={2_457_600}
|
|
180
|
+
text="Here is the file"
|
|
181
|
+
/>
|
|
182
|
+
}
|
|
183
|
+
received={
|
|
184
|
+
<FileAttachment.Received
|
|
185
|
+
src={FILE_SRC}
|
|
186
|
+
filename="workout-plan.zip"
|
|
187
|
+
mimeType="application/zip"
|
|
188
|
+
fileSize={2_457_600}
|
|
189
|
+
text="Here is the file"
|
|
190
|
+
/>
|
|
191
|
+
}
|
|
192
|
+
/>
|
|
193
|
+
</StoryGrid>
|
|
194
|
+
</StoryPage>
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Stacked file attachments — multiple files inside one bubble. Each
|
|
199
|
+
* row has its own download trigger, and rows are separated by an 8px
|
|
200
|
+
* gap to match stacked PDF / Audio.
|
|
201
|
+
*
|
|
202
|
+
* Sent + Received only — the composer surface accepts a single
|
|
203
|
+
* attachment at a time.
|
|
204
|
+
*/
|
|
205
|
+
export const Stacked: StoryFn = () => (
|
|
206
|
+
<StoryPage>
|
|
207
|
+
<StoryHeading
|
|
208
|
+
title="Stacked files"
|
|
209
|
+
description="Each row downloads independently. Composer column is intentionally blank — the composer accepts a single attachment at a time."
|
|
210
|
+
/>
|
|
211
|
+
<StoryGrid>
|
|
212
|
+
<StoryRow
|
|
213
|
+
label="2 files"
|
|
214
|
+
composer={
|
|
215
|
+
<FileAttachment.Composer
|
|
216
|
+
src={FILE_SRC}
|
|
217
|
+
filename="workout-plan.zip"
|
|
218
|
+
mimeType="application/zip"
|
|
219
|
+
fileSize={2_457_600}
|
|
220
|
+
onDismiss={handleDismiss}
|
|
221
|
+
/>
|
|
222
|
+
}
|
|
223
|
+
sent={<FileAttachment.Sent items={STACK_FILES.slice(0, 2)} />}
|
|
224
|
+
received={<FileAttachment.Received items={STACK_FILES.slice(0, 2)} />}
|
|
225
|
+
/>
|
|
226
|
+
<StoryRow
|
|
227
|
+
label="3 files"
|
|
228
|
+
composer={
|
|
229
|
+
<FileAttachment.Composer
|
|
230
|
+
src={FILE_SRC}
|
|
231
|
+
filename="workout-plan.zip"
|
|
232
|
+
mimeType="application/zip"
|
|
233
|
+
fileSize={2_457_600}
|
|
234
|
+
onDismiss={handleDismiss}
|
|
235
|
+
/>
|
|
236
|
+
}
|
|
237
|
+
sent={<FileAttachment.Sent items={STACK_FILES.slice(0, 3)} />}
|
|
238
|
+
received={<FileAttachment.Received items={STACK_FILES.slice(0, 3)} />}
|
|
239
|
+
/>
|
|
240
|
+
<StoryRow
|
|
241
|
+
label="4 files"
|
|
242
|
+
composer={
|
|
243
|
+
<FileAttachment.Composer
|
|
244
|
+
src={FILE_SRC}
|
|
245
|
+
filename="workout-plan.zip"
|
|
246
|
+
mimeType="application/zip"
|
|
247
|
+
fileSize={2_457_600}
|
|
248
|
+
onDismiss={handleDismiss}
|
|
249
|
+
/>
|
|
250
|
+
}
|
|
251
|
+
sent={<FileAttachment.Sent items={STACK_FILES} />}
|
|
252
|
+
received={<FileAttachment.Received items={STACK_FILES} />}
|
|
253
|
+
/>
|
|
254
|
+
</StoryGrid>
|
|
255
|
+
</StoryPage>
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Bubble grouping — mirrors the corner-flattening stream-chat-react
|
|
260
|
+
* applies to text bubbles in a same-author run. The four positions
|
|
261
|
+
* (`single` / `first` / `middle` / `end`) flatten the corners that
|
|
262
|
+
* face the previous / next bubble in the cluster, so a `MessageAttachment`
|
|
263
|
+
* sitting inside a thread visually merges with the surrounding text
|
|
264
|
+
* bubbles instead of floating as its own ungrouped pill.
|
|
265
|
+
*
|
|
266
|
+
* Stack the four positions vertically with `gap-[2px]` to see them
|
|
267
|
+
* snap into a single visual run — the same gap stream uses between
|
|
268
|
+
* grouped messages.
|
|
269
|
+
*/
|
|
270
|
+
export const Grouping: StoryFn = () => (
|
|
271
|
+
<StoryPage>
|
|
272
|
+
<StoryHeading
|
|
273
|
+
title="Bubble grouping"
|
|
274
|
+
description="Pass `groupPosition='single' | 'first' | 'middle' | 'end'` (or use `bubbleGroupPositionFromStream({ firstOfGroup, endOfGroup, groupedByUser })`) to make the attachment join a same-author message run. Sender bubbles flatten the corners on their right edge; receiver bubbles mirror onto the left."
|
|
275
|
+
/>
|
|
276
|
+
<StoryGrid>
|
|
277
|
+
<StoryRow
|
|
278
|
+
label="single"
|
|
279
|
+
sent={<FileAttachment.Sent {...STACK_FILES[0]} groupPosition="single" />}
|
|
280
|
+
received={
|
|
281
|
+
<FileAttachment.Received {...STACK_FILES[0]} groupPosition="single" />
|
|
282
|
+
}
|
|
283
|
+
/>
|
|
284
|
+
<StoryRow
|
|
285
|
+
label="run of 3"
|
|
286
|
+
sent={
|
|
287
|
+
<div className="flex flex-col gap-[2px]">
|
|
288
|
+
<FileAttachment.Sent {...STACK_FILES[0]} groupPosition="first" />
|
|
289
|
+
<FileAttachment.Sent {...STACK_FILES[1]} groupPosition="middle" />
|
|
290
|
+
<FileAttachment.Sent {...STACK_FILES[2]} groupPosition="end" />
|
|
291
|
+
</div>
|
|
292
|
+
}
|
|
293
|
+
received={
|
|
294
|
+
<div className="flex flex-col gap-[2px]">
|
|
295
|
+
<FileAttachment.Received
|
|
296
|
+
{...STACK_FILES[0]}
|
|
297
|
+
groupPosition="first"
|
|
298
|
+
/>
|
|
299
|
+
<FileAttachment.Received
|
|
300
|
+
{...STACK_FILES[1]}
|
|
301
|
+
groupPosition="middle"
|
|
302
|
+
/>
|
|
303
|
+
<FileAttachment.Received {...STACK_FILES[2]} groupPosition="end" />
|
|
304
|
+
</div>
|
|
305
|
+
}
|
|
306
|
+
/>
|
|
307
|
+
</StoryGrid>
|
|
308
|
+
</StoryPage>
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
/** Stacked files with an accompanying caption.
|
|
312
|
+
* Sent + Received only — see `Stacked` for the rationale. */
|
|
313
|
+
export const StackedWithText: StoryFn = () => (
|
|
314
|
+
<StoryPage>
|
|
315
|
+
<StoryHeading
|
|
316
|
+
title="Stacked files with caption"
|
|
317
|
+
description="Caption sits inside the same bubble, below the file stack. Composer column is intentionally blank — the composer accepts a single attachment at a time."
|
|
318
|
+
/>
|
|
319
|
+
<StoryGrid>
|
|
320
|
+
<StoryRow
|
|
321
|
+
label="3 files + caption"
|
|
322
|
+
sent={
|
|
323
|
+
<FileAttachment.Sent
|
|
324
|
+
items={STACK_FILES.slice(0, 3)}
|
|
325
|
+
text="Here are the deliverables — let me know if anything's missing."
|
|
326
|
+
/>
|
|
327
|
+
}
|
|
328
|
+
received={
|
|
329
|
+
<FileAttachment.Received
|
|
330
|
+
items={STACK_FILES.slice(0, 3)}
|
|
331
|
+
text="Here are the deliverables — let me know if anything's missing."
|
|
332
|
+
/>
|
|
333
|
+
}
|
|
334
|
+
/>
|
|
335
|
+
<StoryRow
|
|
336
|
+
label="4 files + caption"
|
|
337
|
+
sent={
|
|
338
|
+
<FileAttachment.Sent
|
|
339
|
+
items={STACK_FILES}
|
|
340
|
+
text="Final pass for review."
|
|
341
|
+
/>
|
|
342
|
+
}
|
|
343
|
+
received={
|
|
344
|
+
<FileAttachment.Received
|
|
345
|
+
items={STACK_FILES}
|
|
346
|
+
text="Final pass for review."
|
|
347
|
+
/>
|
|
348
|
+
}
|
|
349
|
+
/>
|
|
350
|
+
</StoryGrid>
|
|
351
|
+
</StoryPage>
|
|
352
|
+
)
|