@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,292 @@
|
|
|
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 { PdfItem } from '../types'
|
|
11
|
+
|
|
12
|
+
import PdfAttachment from '.'
|
|
13
|
+
|
|
14
|
+
const PDF_SRC = 'https://www.w3.org/WAI/WCAG21/wcag21.pdf'
|
|
15
|
+
|
|
16
|
+
const STACK_PDFS: PdfItem[] = [
|
|
17
|
+
{ src: PDF_SRC, filename: 'ESOP-summary.pdf', fileSize: 388_658 },
|
|
18
|
+
{ src: PDF_SRC, filename: 'Vesting-schedule.pdf', fileSize: 142_336 },
|
|
19
|
+
{ src: PDF_SRC, filename: 'Cap-table-snapshot.pdf', fileSize: 96_021 },
|
|
20
|
+
{ src: PDF_SRC, filename: 'Board-resolution.pdf', fileSize: 24_580 },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const meta: Meta = {
|
|
24
|
+
title: 'MessageAttachment/PDF',
|
|
25
|
+
parameters: { layout: 'fullscreen' },
|
|
26
|
+
}
|
|
27
|
+
export default meta
|
|
28
|
+
|
|
29
|
+
const handleDismiss = () => {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.log('Dismissed PDF attachment')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compact PDF row inside a chat bubble — icon + filename + `EXT · SIZE`.
|
|
36
|
+
* Composer / Sent / Received are all interactive: clicking opens the
|
|
37
|
+
* built-in `PdfViewer` (full-viewport iframe + filename + download).
|
|
38
|
+
*/
|
|
39
|
+
export const Single: StoryFn = () => (
|
|
40
|
+
<StoryPage>
|
|
41
|
+
<StoryHeading
|
|
42
|
+
title="PDF attachment"
|
|
43
|
+
description="Click any bubble to open the full-viewport PDF viewer with built-in download."
|
|
44
|
+
/>
|
|
45
|
+
<StoryGrid>
|
|
46
|
+
<StoryRow
|
|
47
|
+
label="Default"
|
|
48
|
+
composer={
|
|
49
|
+
<PdfAttachment.Composer
|
|
50
|
+
src={PDF_SRC}
|
|
51
|
+
filename="ESOP-summary.pdf"
|
|
52
|
+
fileSize={388_658}
|
|
53
|
+
onDismiss={handleDismiss}
|
|
54
|
+
/>
|
|
55
|
+
}
|
|
56
|
+
sent={
|
|
57
|
+
<PdfAttachment.Sent
|
|
58
|
+
src={PDF_SRC}
|
|
59
|
+
filename="ESOP-summary.pdf"
|
|
60
|
+
fileSize={388_658}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
received={
|
|
64
|
+
<PdfAttachment.Received
|
|
65
|
+
src={PDF_SRC}
|
|
66
|
+
filename="ESOP-summary.pdf"
|
|
67
|
+
fileSize={388_658}
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
<StoryRow
|
|
72
|
+
label="Long filename"
|
|
73
|
+
composer={
|
|
74
|
+
<PdfAttachment.Composer
|
|
75
|
+
src={PDF_SRC}
|
|
76
|
+
filename="ESOP-summary-and-vesting-schedule-2026-Q4-FINAL.pdf"
|
|
77
|
+
fileSize={1_240_000}
|
|
78
|
+
onDismiss={handleDismiss}
|
|
79
|
+
/>
|
|
80
|
+
}
|
|
81
|
+
sent={
|
|
82
|
+
<PdfAttachment.Sent
|
|
83
|
+
src={PDF_SRC}
|
|
84
|
+
filename="ESOP-summary-and-vesting-schedule-2026-Q4-FINAL.pdf"
|
|
85
|
+
fileSize={1_240_000}
|
|
86
|
+
/>
|
|
87
|
+
}
|
|
88
|
+
received={
|
|
89
|
+
<PdfAttachment.Received
|
|
90
|
+
src={PDF_SRC}
|
|
91
|
+
filename="ESOP-summary-and-vesting-schedule-2026-Q4-FINAL.pdf"
|
|
92
|
+
fileSize={1_240_000}
|
|
93
|
+
/>
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
<StoryRow
|
|
97
|
+
label="No file size"
|
|
98
|
+
composer={
|
|
99
|
+
<PdfAttachment.Composer
|
|
100
|
+
src={PDF_SRC}
|
|
101
|
+
filename="contract.pdf"
|
|
102
|
+
onDismiss={handleDismiss}
|
|
103
|
+
/>
|
|
104
|
+
}
|
|
105
|
+
sent={<PdfAttachment.Sent src={PDF_SRC} filename="contract.pdf" />}
|
|
106
|
+
received={<PdfAttachment.Received src={PDF_SRC} filename="contract.pdf" />}
|
|
107
|
+
/>
|
|
108
|
+
</StoryGrid>
|
|
109
|
+
</StoryPage>
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
export const SingleWithText: StoryFn = () => (
|
|
113
|
+
<StoryPage>
|
|
114
|
+
<StoryHeading
|
|
115
|
+
title="PDF with caption"
|
|
116
|
+
description="Caption sits inside the same bubble, below the file row — matches the mobile chat 'Here is the file' screenshot."
|
|
117
|
+
/>
|
|
118
|
+
<StoryGrid>
|
|
119
|
+
<StoryRow
|
|
120
|
+
label="Caption"
|
|
121
|
+
composer={
|
|
122
|
+
<PdfAttachment.Composer
|
|
123
|
+
src={PDF_SRC}
|
|
124
|
+
filename="ESOP-summary.pdf"
|
|
125
|
+
fileSize={388_658}
|
|
126
|
+
onDismiss={handleDismiss}
|
|
127
|
+
/>
|
|
128
|
+
}
|
|
129
|
+
sent={
|
|
130
|
+
<PdfAttachment.Sent
|
|
131
|
+
src={PDF_SRC}
|
|
132
|
+
filename="ESOP-summary.pdf"
|
|
133
|
+
fileSize={388_658}
|
|
134
|
+
text="Here is the file"
|
|
135
|
+
/>
|
|
136
|
+
}
|
|
137
|
+
received={
|
|
138
|
+
<PdfAttachment.Received
|
|
139
|
+
src={PDF_SRC}
|
|
140
|
+
filename="ESOP-summary.pdf"
|
|
141
|
+
fileSize={388_658}
|
|
142
|
+
text="Here is the file"
|
|
143
|
+
/>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
<StoryRow
|
|
147
|
+
label="Long caption"
|
|
148
|
+
composer={
|
|
149
|
+
<PdfAttachment.Composer
|
|
150
|
+
src={PDF_SRC}
|
|
151
|
+
filename="ESOP-summary.pdf"
|
|
152
|
+
fileSize={388_658}
|
|
153
|
+
onDismiss={handleDismiss}
|
|
154
|
+
/>
|
|
155
|
+
}
|
|
156
|
+
sent={
|
|
157
|
+
<PdfAttachment.Sent
|
|
158
|
+
src={PDF_SRC}
|
|
159
|
+
filename="ESOP-summary.pdf"
|
|
160
|
+
fileSize={388_658}
|
|
161
|
+
text="Sharing the latest ESOP summary — let me know if you need any sections clarified before we sign."
|
|
162
|
+
/>
|
|
163
|
+
}
|
|
164
|
+
received={
|
|
165
|
+
<PdfAttachment.Received
|
|
166
|
+
src={PDF_SRC}
|
|
167
|
+
filename="ESOP-summary.pdf"
|
|
168
|
+
fileSize={388_658}
|
|
169
|
+
text="Sharing the latest ESOP summary — let me know if you need any sections clarified before we sign."
|
|
170
|
+
/>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
</StoryGrid>
|
|
174
|
+
</StoryPage>
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Stacked PDF attachments — multiple PDFs in one bubble. Each row is
|
|
179
|
+
* its own tappable target that opens the `PdfViewer` for that file.
|
|
180
|
+
*
|
|
181
|
+
* Sent + Received only — the composer surface accepts a single
|
|
182
|
+
* attachment at a time (the user picks files one at a time inside the
|
|
183
|
+
* file picker, so the composer preview lives outside this scenario).
|
|
184
|
+
*/
|
|
185
|
+
export const Stacked: StoryFn = () => (
|
|
186
|
+
<StoryPage>
|
|
187
|
+
<StoryHeading
|
|
188
|
+
title="Stacked PDFs"
|
|
189
|
+
description="Each row is its own tappable target; clicking opens that PDF in the viewer. Composer column is intentionally blank — the composer accepts a single attachment at a time."
|
|
190
|
+
/>
|
|
191
|
+
<StoryGrid>
|
|
192
|
+
<StoryRow
|
|
193
|
+
label="2 PDFs"
|
|
194
|
+
composer={
|
|
195
|
+
<PdfAttachment.Composer
|
|
196
|
+
src={PDF_SRC}
|
|
197
|
+
filename="ESOP-summary.pdf"
|
|
198
|
+
fileSize={388_658}
|
|
199
|
+
onDismiss={handleDismiss}
|
|
200
|
+
/>
|
|
201
|
+
}
|
|
202
|
+
sent={<PdfAttachment.Sent items={STACK_PDFS.slice(0, 2)} />}
|
|
203
|
+
received={<PdfAttachment.Received items={STACK_PDFS.slice(0, 2)} />}
|
|
204
|
+
/>
|
|
205
|
+
<StoryRow
|
|
206
|
+
label="3 PDFs"
|
|
207
|
+
composer={
|
|
208
|
+
<PdfAttachment.Composer
|
|
209
|
+
src={PDF_SRC}
|
|
210
|
+
filename="ESOP-summary.pdf"
|
|
211
|
+
fileSize={388_658}
|
|
212
|
+
onDismiss={handleDismiss}
|
|
213
|
+
/>
|
|
214
|
+
}
|
|
215
|
+
sent={<PdfAttachment.Sent items={STACK_PDFS.slice(0, 3)} />}
|
|
216
|
+
received={<PdfAttachment.Received items={STACK_PDFS.slice(0, 3)} />}
|
|
217
|
+
/>
|
|
218
|
+
<StoryRow
|
|
219
|
+
label="4 PDFs"
|
|
220
|
+
composer={
|
|
221
|
+
<PdfAttachment.Composer
|
|
222
|
+
src={PDF_SRC}
|
|
223
|
+
filename="ESOP-summary.pdf"
|
|
224
|
+
fileSize={388_658}
|
|
225
|
+
onDismiss={handleDismiss}
|
|
226
|
+
/>
|
|
227
|
+
}
|
|
228
|
+
sent={<PdfAttachment.Sent items={STACK_PDFS} />}
|
|
229
|
+
received={<PdfAttachment.Received items={STACK_PDFS} />}
|
|
230
|
+
/>
|
|
231
|
+
</StoryGrid>
|
|
232
|
+
</StoryPage>
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
/** Stacked PDFs with an accompanying caption.
|
|
236
|
+
* Sent + Received only — see `Stacked` for the rationale. */
|
|
237
|
+
export const StackedWithText: StoryFn = () => (
|
|
238
|
+
<StoryPage>
|
|
239
|
+
<StoryHeading
|
|
240
|
+
title="Stacked PDFs with caption"
|
|
241
|
+
description="Caption sits inside the same bubble, below the file list. Composer column is intentionally blank — the composer accepts a single attachment at a time."
|
|
242
|
+
/>
|
|
243
|
+
<StoryGrid>
|
|
244
|
+
<StoryRow
|
|
245
|
+
label="3 PDFs + caption"
|
|
246
|
+
composer={
|
|
247
|
+
<PdfAttachment.Composer
|
|
248
|
+
src={PDF_SRC}
|
|
249
|
+
filename="ESOP-summary.pdf"
|
|
250
|
+
fileSize={388_658}
|
|
251
|
+
onDismiss={handleDismiss}
|
|
252
|
+
/>
|
|
253
|
+
}
|
|
254
|
+
sent={
|
|
255
|
+
<PdfAttachment.Sent
|
|
256
|
+
items={STACK_PDFS.slice(0, 3)}
|
|
257
|
+
text="Here are the docs from the call — review when you can"
|
|
258
|
+
/>
|
|
259
|
+
}
|
|
260
|
+
received={
|
|
261
|
+
<PdfAttachment.Received
|
|
262
|
+
items={STACK_PDFS.slice(0, 3)}
|
|
263
|
+
text="Here are the docs from the call — review when you can"
|
|
264
|
+
/>
|
|
265
|
+
}
|
|
266
|
+
/>
|
|
267
|
+
<StoryRow
|
|
268
|
+
label="4 PDFs + caption"
|
|
269
|
+
composer={
|
|
270
|
+
<PdfAttachment.Composer
|
|
271
|
+
src={PDF_SRC}
|
|
272
|
+
filename="ESOP-summary.pdf"
|
|
273
|
+
fileSize={388_658}
|
|
274
|
+
onDismiss={handleDismiss}
|
|
275
|
+
/>
|
|
276
|
+
}
|
|
277
|
+
sent={
|
|
278
|
+
<PdfAttachment.Sent
|
|
279
|
+
items={STACK_PDFS}
|
|
280
|
+
text="Full ESOP package + the cap table snapshot for tonight's meeting."
|
|
281
|
+
/>
|
|
282
|
+
}
|
|
283
|
+
received={
|
|
284
|
+
<PdfAttachment.Received
|
|
285
|
+
items={STACK_PDFS}
|
|
286
|
+
text="Full ESOP package + the cap table snapshot for tonight's meeting."
|
|
287
|
+
/>
|
|
288
|
+
}
|
|
289
|
+
/>
|
|
290
|
+
</StoryGrid>
|
|
291
|
+
</StoryPage>
|
|
292
|
+
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import Bubble from '../_shared/Bubble'
|
|
4
|
+
import CompactDocumentRow from '../_shared/CompactDocumentRow'
|
|
5
|
+
import DismissButton from '../_shared/DismissButton'
|
|
6
|
+
import DownloadAction from '../_shared/DownloadAction'
|
|
7
|
+
import { filenameFromUrl } from '../_shared/fileMeta'
|
|
8
|
+
import PdfViewer from '../_shared/PdfViewer'
|
|
9
|
+
import { useViewer } from '../_shared/useViewer'
|
|
10
|
+
import {
|
|
11
|
+
bubbleVariantForState,
|
|
12
|
+
type BubbleVariant,
|
|
13
|
+
type ComposerExtras,
|
|
14
|
+
type MessageAttachmentBaseProps,
|
|
15
|
+
type MessageAttachmentState,
|
|
16
|
+
type PdfItem,
|
|
17
|
+
} from '../types'
|
|
18
|
+
|
|
19
|
+
export interface PdfAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
20
|
+
/** Single PDF — convenience for the most common case. */
|
|
21
|
+
src?: string
|
|
22
|
+
/** Filename — drives the title + the meta line + the viewer toolbar. */
|
|
23
|
+
filename?: string
|
|
24
|
+
/** File size in bytes — formatted into the `EXT · SIZE` meta line. */
|
|
25
|
+
fileSize?: number
|
|
26
|
+
/**
|
|
27
|
+
* Override displayed title (defaults to `filename`). Useful when a
|
|
28
|
+
* sender re-titles a generated PDF before sending.
|
|
29
|
+
*/
|
|
30
|
+
title?: string
|
|
31
|
+
/**
|
|
32
|
+
* Stacked PDFs. Takes precedence over `src` when set. Each item
|
|
33
|
+
* renders as its own row inside the bubble; clicking a row opens
|
|
34
|
+
* that PDF in the viewer. Sent + Received only — the composer
|
|
35
|
+
* surface accepts a single attachment at a time.
|
|
36
|
+
*/
|
|
37
|
+
items?: PdfItem[]
|
|
38
|
+
/**
|
|
39
|
+
* Forwarded to the viewer trigger. When omitted the click still
|
|
40
|
+
* opens the built-in `PdfViewer`. Supply this to fire analytics, or
|
|
41
|
+
* return `false` to intercept the open (e.g. swap to a route-based
|
|
42
|
+
* viewer / confirmation modal). Any other return value (including
|
|
43
|
+
* `void`/`undefined`) lets the built-in viewer open. For stacked
|
|
44
|
+
* attachments the `index` argument identifies the row.
|
|
45
|
+
*/
|
|
46
|
+
onClick?: (index: number) => boolean | void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolveItems = ({
|
|
50
|
+
src,
|
|
51
|
+
filename,
|
|
52
|
+
fileSize,
|
|
53
|
+
title,
|
|
54
|
+
items,
|
|
55
|
+
}: {
|
|
56
|
+
src?: string
|
|
57
|
+
filename?: string
|
|
58
|
+
fileSize?: number
|
|
59
|
+
title?: string
|
|
60
|
+
items?: PdfItem[]
|
|
61
|
+
}): PdfItem[] => {
|
|
62
|
+
if (items && items.length > 0) return items
|
|
63
|
+
if (src) return [{ src, filename, fileSize, title }]
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface PdfRowButtonProps {
|
|
68
|
+
variant: BubbleVariant
|
|
69
|
+
item: PdfItem
|
|
70
|
+
index: number
|
|
71
|
+
onActivate: (index: number) => void
|
|
72
|
+
trailingAction?: React.ReactNode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Single tappable PDF row — used both for the lone PDF case and for
|
|
77
|
+
* each child of a stacked attachment. The icon + filename area opens
|
|
78
|
+
* the viewer; the trailing slot hosts a separate, sibling button
|
|
79
|
+
* (download or dismiss) so each affordance owns its own click target.
|
|
80
|
+
*/
|
|
81
|
+
const PdfRowButton: React.FC<PdfRowButtonProps> = ({
|
|
82
|
+
variant,
|
|
83
|
+
item,
|
|
84
|
+
index,
|
|
85
|
+
onActivate,
|
|
86
|
+
trailingAction,
|
|
87
|
+
}) => {
|
|
88
|
+
const resolvedFilename = item.filename ?? filenameFromUrl(item.src)
|
|
89
|
+
return (
|
|
90
|
+
<CompactDocumentRow
|
|
91
|
+
variant={variant}
|
|
92
|
+
filename={resolvedFilename}
|
|
93
|
+
title={item.title}
|
|
94
|
+
mimeType="application/pdf"
|
|
95
|
+
fileSize={item.fileSize}
|
|
96
|
+
onActivate={() => onActivate(index)}
|
|
97
|
+
activateLabel={`Open ${resolvedFilename}`}
|
|
98
|
+
trailingAction={trailingAction}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface InternalPdfRowProps extends PdfAttachmentSharedProps {
|
|
104
|
+
state: MessageAttachmentState
|
|
105
|
+
onDismiss?: () => void
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const PdfAttachmentRow: React.FC<InternalPdfRowProps> = ({
|
|
109
|
+
state,
|
|
110
|
+
src,
|
|
111
|
+
filename,
|
|
112
|
+
fileSize,
|
|
113
|
+
title,
|
|
114
|
+
items,
|
|
115
|
+
text,
|
|
116
|
+
groupPosition,
|
|
117
|
+
onClick,
|
|
118
|
+
onDismiss,
|
|
119
|
+
}) => {
|
|
120
|
+
const variant = bubbleVariantForState(state)
|
|
121
|
+
const resolvedItems = resolveItems({ src, filename, fileSize, title, items })
|
|
122
|
+
const { viewerOpen, viewerIndex, handleActivate, closeViewer } = useViewer(
|
|
123
|
+
onClick
|
|
124
|
+
)
|
|
125
|
+
const showDismiss = state === 'composer' && !!onDismiss
|
|
126
|
+
|
|
127
|
+
if (resolvedItems.length === 0) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Clamp the viewer index defensively: if `items` shrinks between
|
|
132
|
+
// the open click and the next render (server push, optimistic
|
|
133
|
+
// update, etc.) `viewerIndex` may point past the new array.
|
|
134
|
+
// Dereferencing `undefined` would crash the whole bubble, so fall
|
|
135
|
+
// back to the last available row instead.
|
|
136
|
+
const safeIndex = Math.min(viewerIndex, resolvedItems.length - 1)
|
|
137
|
+
const activeItem = resolvedItems[safeIndex]
|
|
138
|
+
const activeFilename =
|
|
139
|
+
activeItem.filename ?? filenameFromUrl(activeItem.src)
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Bubble
|
|
143
|
+
variant={variant}
|
|
144
|
+
text={text}
|
|
145
|
+
groupPosition={groupPosition}
|
|
146
|
+
data-testid="pdf-attachment"
|
|
147
|
+
>
|
|
148
|
+
{/* Stacked rows get a small vertical gap so each PDF reads as
|
|
149
|
+
a discrete attachment rather than one merged blob. Single
|
|
150
|
+
row falls through to a no-op `gap-0`. */}
|
|
151
|
+
<div className="flex flex-col gap-2">
|
|
152
|
+
{resolvedItems.map((item, index) => {
|
|
153
|
+
const itemFilename = item.filename ?? filenameFromUrl(item.src)
|
|
154
|
+
// Composer only supports a single attachment so the dismiss
|
|
155
|
+
// control sits on the only row. Sent / Received rows expose
|
|
156
|
+
// a download icon button so the user can grab the PDF
|
|
157
|
+
// without opening the viewer first (the viewer also has a
|
|
158
|
+
// download in its toolbar — this is the quick-grab shortcut).
|
|
159
|
+
const trailingAction =
|
|
160
|
+
showDismiss && index === 0 ? (
|
|
161
|
+
<DismissButton onClick={onDismiss!} variant="inline" />
|
|
162
|
+
) : state === 'composer' ? undefined : (
|
|
163
|
+
<DownloadAction
|
|
164
|
+
url={item.src}
|
|
165
|
+
filename={itemFilename}
|
|
166
|
+
variant="inline"
|
|
167
|
+
label={`Download ${itemFilename}`}
|
|
168
|
+
tone={variant}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
return (
|
|
172
|
+
<PdfRowButton
|
|
173
|
+
key={`${item.src}-${index}`}
|
|
174
|
+
variant={variant}
|
|
175
|
+
item={item}
|
|
176
|
+
index={index}
|
|
177
|
+
onActivate={handleActivate}
|
|
178
|
+
trailingAction={trailingAction}
|
|
179
|
+
/>
|
|
180
|
+
)
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<PdfViewer
|
|
185
|
+
open={viewerOpen}
|
|
186
|
+
src={activeItem.src}
|
|
187
|
+
filename={activeFilename}
|
|
188
|
+
onClose={closeViewer}
|
|
189
|
+
/>
|
|
190
|
+
</Bubble>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Composer-only props. Single PDF (`src`) only — the composer surface
|
|
196
|
+
* accepts a single attachment at a time, so `items` is not supported.
|
|
197
|
+
* Captions (`text`) and `groupPosition` are also dropped: the composer
|
|
198
|
+
* renders a standalone draft, not part of a same-author run, and
|
|
199
|
+
* captions live in the message-input textarea, not inside the draft
|
|
200
|
+
* attachment preview.
|
|
201
|
+
*/
|
|
202
|
+
export interface PdfComposerProps extends ComposerExtras {
|
|
203
|
+
src?: string
|
|
204
|
+
filename?: string
|
|
205
|
+
fileSize?: number
|
|
206
|
+
title?: string
|
|
207
|
+
onClick?: (index: number) => boolean | void
|
|
208
|
+
}
|
|
209
|
+
export type PdfSentProps = PdfAttachmentSharedProps
|
|
210
|
+
export type PdfReceivedProps = PdfAttachmentSharedProps
|
|
211
|
+
|
|
212
|
+
const PdfComposer: React.FC<PdfComposerProps> = (props) => (
|
|
213
|
+
<PdfAttachmentRow {...props} state="composer" />
|
|
214
|
+
)
|
|
215
|
+
const PdfSent: React.FC<PdfSentProps> = (props) => (
|
|
216
|
+
<PdfAttachmentRow {...props} state="sent" />
|
|
217
|
+
)
|
|
218
|
+
const PdfReceived: React.FC<PdfReceivedProps> = (props) => (
|
|
219
|
+
<PdfAttachmentRow {...props} state="received" />
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const PdfAttachment = {
|
|
223
|
+
Composer: PdfComposer,
|
|
224
|
+
Sent: PdfSent,
|
|
225
|
+
Received: PdfReceived,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default PdfAttachment
|