@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.
Files changed (58) hide show
  1. package/dist/{Card-CsJvUF_b.js → Card-BdTueeyk.js} +2 -2
  2. package/dist/{Card-CsJvUF_b.js.map → Card-BdTueeyk.js.map} +1 -1
  3. package/dist/{Card-DlMSDSdm.js → Card-ChR37pLZ.js} +2 -2
  4. package/dist/{Card-DlMSDSdm.js.map → Card-ChR37pLZ.js.map} +1 -1
  5. package/dist/{Card-CFFNq49v.js → Card-EKxCn56j.js} +3 -3
  6. package/dist/{Card-CFFNq49v.js.map → Card-EKxCn56j.js.map} +1 -1
  7. package/dist/{LockedThumbnail-DpJx169C.js → LockedThumbnail-B16qP3eH.js} +2 -2
  8. package/dist/{LockedThumbnail-DpJx169C.js.map → LockedThumbnail-B16qP3eH.js.map} +1 -1
  9. package/dist/index-Dn7BC9xK.js +4748 -0
  10. package/dist/index-Dn7BC9xK.js.map +1 -0
  11. package/dist/index.d.ts +591 -25
  12. package/dist/index.js +24 -19
  13. package/package.json +1 -1
  14. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +841 -0
  15. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +7 -92
  16. package/src/components/LinkAttachment/LinkAttachment.test.tsx +69 -0
  17. package/src/components/LinkAttachment/components/Received/Card.tsx +10 -30
  18. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +5 -1
  19. package/src/components/LinkAttachment/index.tsx +24 -50
  20. package/src/components/LinkAttachment/types.ts +12 -5
  21. package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +203 -0
  22. package/src/components/MessageAttachment/Audio/index.tsx +189 -0
  23. package/src/components/MessageAttachment/File/FileAttachment.stories.tsx +352 -0
  24. package/src/components/MessageAttachment/File/index.tsx +240 -0
  25. package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +288 -0
  26. package/src/components/MessageAttachment/Image/index.tsx +257 -0
  27. package/src/components/MessageAttachment/MessageAttachment.test.tsx +783 -0
  28. package/src/components/MessageAttachment/Pdf/PdfAttachment.stories.tsx +292 -0
  29. package/src/components/MessageAttachment/Pdf/index.tsx +228 -0
  30. package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +272 -0
  31. package/src/components/MessageAttachment/Video/index.tsx +281 -0
  32. package/src/components/MessageAttachment/_shared/Bubble.tsx +173 -0
  33. package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +152 -0
  34. package/src/components/MessageAttachment/_shared/DismissButton.tsx +39 -0
  35. package/src/components/MessageAttachment/_shared/DownloadAction.tsx +175 -0
  36. package/src/components/MessageAttachment/_shared/ImageViewer.tsx +314 -0
  37. package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +139 -0
  38. package/src/components/MessageAttachment/_shared/PdfViewer.tsx +100 -0
  39. package/src/components/MessageAttachment/_shared/VideoViewer.tsx +171 -0
  40. package/src/components/MessageAttachment/_shared/ViewerShell.tsx +159 -0
  41. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +82 -0
  42. package/src/components/MessageAttachment/_shared/fileMeta.ts +95 -0
  43. package/src/components/MessageAttachment/_shared/triggerDownload.ts +54 -0
  44. package/src/components/MessageAttachment/_shared/useViewer.ts +53 -0
  45. package/src/components/MessageAttachment/index.tsx +149 -0
  46. package/src/components/MessageAttachment/stories/StoryTable.tsx +72 -0
  47. package/src/components/MessageAttachment/types.ts +178 -0
  48. package/src/index.ts +32 -0
  49. package/dist/Card-D32U6KfZ.js +0 -85
  50. package/dist/Card-D32U6KfZ.js.map +0 -1
  51. package/dist/Card-DlSSJPip.js +0 -60
  52. package/dist/Card-DlSSJPip.js.map +0 -1
  53. package/dist/Card-zGbhRBwv.js +0 -48
  54. package/dist/Card-zGbhRBwv.js.map +0 -1
  55. package/dist/CardThumbnail-DTBuRQHF.js +0 -239
  56. package/dist/CardThumbnail-DTBuRQHF.js.map +0 -1
  57. package/dist/index-DfcRe-Hj.js +0 -3103
  58. package/dist/index-DfcRe-Hj.js.map +0 -1
@@ -0,0 +1,240 @@
1
+ import { DownloadSimpleIcon } from '@phosphor-icons/react'
2
+ import classNames from 'classnames'
3
+ import React from 'react'
4
+
5
+ import Bubble from '../_shared/Bubble'
6
+ import CompactDocumentRow from '../_shared/CompactDocumentRow'
7
+ import DismissButton from '../_shared/DismissButton'
8
+ import { filenameFromUrl } from '../_shared/fileMeta'
9
+ import { triggerDownload } from '../_shared/triggerDownload'
10
+ import {
11
+ bubbleVariantForState,
12
+ type BubbleVariant,
13
+ type ComposerExtras,
14
+ type FileItem,
15
+ type MessageAttachmentBaseProps,
16
+ type MessageAttachmentState,
17
+ } from '../types'
18
+
19
+ export interface FileAttachmentSharedProps extends MessageAttachmentBaseProps {
20
+ /** Source URL of the file (used as the download target). */
21
+ src?: string
22
+ /** Filename — drives the title + the meta line + the download default name. */
23
+ filename?: string
24
+ fileSize?: number
25
+ /** MIME type — drives the type icon. Defaults to `application/octet-stream`. */
26
+ mimeType?: string
27
+ /**
28
+ * Override displayed title (defaults to `filename`). Useful when a
29
+ * sender renames the attachment before sending.
30
+ */
31
+ title?: string
32
+ /**
33
+ * Stacked files. Takes precedence over `src` when set. Each item
34
+ * renders as its own row inside the bubble; clicking a row downloads
35
+ * that file. Sent + Received only — the composer surface accepts a
36
+ * single attachment at a time.
37
+ */
38
+ items?: FileItem[]
39
+ /**
40
+ * Forwarded to the row trigger. When omitted the click still
41
+ * downloads the file. Supply this for analytics, or return `false`
42
+ * to fully intercept the download (e.g. show a confirmation modal,
43
+ * route to a custom preview, throttle large files). Any other
44
+ * return value — including `void`/`undefined` — lets the built-in
45
+ * download proceed. For stacked attachments the `index` argument
46
+ * identifies the row.
47
+ */
48
+ onClick?: (index: number) => boolean | void
49
+ }
50
+
51
+ const resolveItems = ({
52
+ src,
53
+ filename,
54
+ fileSize,
55
+ mimeType,
56
+ title,
57
+ items,
58
+ }: {
59
+ src?: string
60
+ filename?: string
61
+ fileSize?: number
62
+ mimeType?: string
63
+ title?: string
64
+ items?: FileItem[]
65
+ }): FileItem[] => {
66
+ if (items && items.length > 0) return items
67
+ if (src) return [{ src, filename, fileSize, mimeType, title }]
68
+ return []
69
+ }
70
+
71
+ interface FileRowButtonProps {
72
+ variant: BubbleVariant
73
+ item: FileItem
74
+ index: number
75
+ onActivate: (index: number) => void
76
+ trailingAction: React.ReactNode
77
+ }
78
+
79
+ /**
80
+ * Single tappable file row — used both for the lone file case and
81
+ * for each child of a stacked attachment. The icon + filename area
82
+ * is the click target (downloads the asset); the trailing slot is
83
+ * decorative on Sent / Received and hosts the dismiss control on
84
+ * Composer.
85
+ */
86
+ const FileRowButton: React.FC<FileRowButtonProps> = ({
87
+ variant,
88
+ item,
89
+ index,
90
+ onActivate,
91
+ trailingAction,
92
+ }) => {
93
+ const resolvedFilename = item.filename ?? filenameFromUrl(item.src)
94
+ return (
95
+ <CompactDocumentRow
96
+ variant={variant}
97
+ filename={resolvedFilename}
98
+ title={item.title}
99
+ mimeType={item.mimeType ?? 'application/octet-stream'}
100
+ fileSize={item.fileSize}
101
+ onActivate={() => onActivate(index)}
102
+ activateLabel={`Download ${resolvedFilename}`}
103
+ trailingAction={trailingAction}
104
+ />
105
+ )
106
+ }
107
+
108
+ interface InternalFileRowProps extends FileAttachmentSharedProps {
109
+ state: MessageAttachmentState
110
+ onDismiss?: () => void
111
+ }
112
+
113
+ const FileAttachmentRow: React.FC<InternalFileRowProps> = ({
114
+ state,
115
+ src,
116
+ filename,
117
+ fileSize,
118
+ mimeType,
119
+ title,
120
+ items,
121
+ text,
122
+ groupPosition,
123
+ onClick,
124
+ onDismiss,
125
+ }) => {
126
+ const variant = bubbleVariantForState(state)
127
+ const showDismiss = state === 'composer' && !!onDismiss
128
+ const resolvedItems = resolveItems({
129
+ src,
130
+ filename,
131
+ fileSize,
132
+ mimeType,
133
+ title,
134
+ items,
135
+ })
136
+
137
+ // `useCallback` would be pointless here — `resolvedItems` is rebuilt
138
+ // every render, so memoizing on it produces a fresh function every
139
+ // render anyway. Keep this as a plain function.
140
+ const handleActivate = (index: number) => {
141
+ // `onClick` returning `false` cancels the built-in download so
142
+ // consumers can route the click to a confirmation modal, custom
143
+ // preview, etc. Any other return value (including void) lets the
144
+ // default download proceed.
145
+ if (onClick?.(index) === false) return
146
+ const item = resolvedItems[index]
147
+ if (!item) return
148
+ const resolvedFilename = item.filename ?? filenameFromUrl(item.src)
149
+ void triggerDownload(item.src, resolvedFilename)
150
+ }
151
+
152
+ if (resolvedItems.length === 0) {
153
+ return null
154
+ }
155
+
156
+ const downloadIcon = (
157
+ <span
158
+ className={classNames(
159
+ 'flex size-8 items-center justify-center rounded-full',
160
+ variant === 'dark' ? 'text-white/70' : 'text-black/70'
161
+ )}
162
+ aria-hidden
163
+ >
164
+ <DownloadSimpleIcon className="size-5" weight="bold" />
165
+ </span>
166
+ )
167
+
168
+ return (
169
+ <Bubble
170
+ variant={variant}
171
+ text={text}
172
+ groupPosition={groupPosition}
173
+ data-testid="file-attachment"
174
+ >
175
+ {/* Stacked rows get a small vertical gap so each file reads as
176
+ a discrete attachment rather than one merged blob. Single
177
+ row falls through to a no-op `gap-0`. */}
178
+ <div className="flex flex-col gap-2">
179
+ {resolvedItems.map((item, index) => {
180
+ // Composer only supports a single attachment so the dismiss
181
+ // control sits on the only row; otherwise every row gets
182
+ // the trailing download icon hint.
183
+ const trailingAction =
184
+ showDismiss && index === 0 ? (
185
+ <DismissButton onClick={onDismiss!} variant="inline" />
186
+ ) : (
187
+ downloadIcon
188
+ )
189
+ return (
190
+ <FileRowButton
191
+ key={`${item.src}-${index}`}
192
+ variant={variant}
193
+ item={item}
194
+ index={index}
195
+ onActivate={handleActivate}
196
+ trailingAction={trailingAction}
197
+ />
198
+ )
199
+ })}
200
+ </div>
201
+ </Bubble>
202
+ )
203
+ }
204
+
205
+ /**
206
+ * Composer-only props. Single file (`src`) only — the composer surface
207
+ * accepts a single attachment at a time, so `items` is not supported.
208
+ * Captions (`text`) and `groupPosition` are also dropped: the composer
209
+ * renders a standalone draft, not part of a same-author run, and
210
+ * captions live in the message-input textarea, not inside the draft
211
+ * attachment preview.
212
+ */
213
+ export interface FileComposerProps extends ComposerExtras {
214
+ src?: string
215
+ filename?: string
216
+ fileSize?: number
217
+ mimeType?: string
218
+ title?: string
219
+ onClick?: (index: number) => boolean | void
220
+ }
221
+ export type FileSentProps = FileAttachmentSharedProps
222
+ export type FileReceivedProps = FileAttachmentSharedProps
223
+
224
+ const FileComposer: React.FC<FileComposerProps> = (props) => (
225
+ <FileAttachmentRow {...props} state="composer" />
226
+ )
227
+ const FileSent: React.FC<FileSentProps> = (props) => (
228
+ <FileAttachmentRow {...props} state="sent" />
229
+ )
230
+ const FileReceived: React.FC<FileReceivedProps> = (props) => (
231
+ <FileAttachmentRow {...props} state="received" />
232
+ )
233
+
234
+ const FileAttachment = {
235
+ Composer: FileComposer,
236
+ Sent: FileSent,
237
+ Received: FileReceived,
238
+ }
239
+
240
+ export default FileAttachment
@@ -0,0 +1,288 @@
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 { ImageItem } from '../types'
11
+
12
+ import ImageAttachment from '.'
13
+
14
+ const SINGLE_IMAGE = 'https://picsum.photos/seed/portrait/720/720'
15
+ const STACK_IMAGES: ImageItem[] = [
16
+ { src: 'https://picsum.photos/seed/img-1/720/720', alt: 'Photo 1' },
17
+ { src: 'https://picsum.photos/seed/img-2/720/720', alt: 'Photo 2' },
18
+ { src: 'https://picsum.photos/seed/img-3/720/720', alt: 'Photo 3' },
19
+ { src: 'https://picsum.photos/seed/img-4/720/720', alt: 'Photo 4' },
20
+ { src: 'https://picsum.photos/seed/img-5/720/720', alt: 'Photo 5' },
21
+ { src: 'https://picsum.photos/seed/img-6/720/720', alt: 'Photo 6' },
22
+ ]
23
+
24
+ const meta: Meta = {
25
+ title: 'MessageAttachment/Image',
26
+ parameters: { layout: 'fullscreen' },
27
+ }
28
+ export default meta
29
+
30
+ const handleDismiss = () => {
31
+ // eslint-disable-next-line no-console
32
+ console.log('Dismissed image attachment')
33
+ }
34
+
35
+ /**
36
+ * Single image — renders inside a chat bubble with rounded corners.
37
+ * Composer / Sent / Received are all interactive: clicking opens the
38
+ * built-in `ImageViewer` lightbox with mouse-wheel zoom, drag-to-pan,
39
+ * double-click toggle, and a download action in the toolbar.
40
+ */
41
+ export const Single: StoryFn = () => (
42
+ <StoryPage>
43
+ <StoryHeading
44
+ title="Single image"
45
+ description="Click any bubble to open the lightbox viewer (mouse-wheel zoom, drag-to-pan, double-click toggle, download)."
46
+ />
47
+ <StoryGrid>
48
+ <StoryRow
49
+ label="Default"
50
+ composer={
51
+ <ImageAttachment.Composer
52
+ src={SINGLE_IMAGE}
53
+ alt="Portrait"
54
+ filename="portrait.jpg"
55
+ onDismiss={handleDismiss}
56
+ />
57
+ }
58
+ sent={
59
+ <ImageAttachment.Sent
60
+ src={SINGLE_IMAGE}
61
+ alt="Portrait"
62
+ filename="portrait.jpg"
63
+ />
64
+ }
65
+ received={
66
+ <ImageAttachment.Received
67
+ src={SINGLE_IMAGE}
68
+ alt="Portrait"
69
+ filename="portrait.jpg"
70
+ />
71
+ }
72
+ />
73
+ </StoryGrid>
74
+ </StoryPage>
75
+ )
76
+
77
+ /** Single image with an accompanying caption rendered inside the bubble.
78
+ * Sent + Received only — captions are not rendered on the composer
79
+ * surface (the user types them in the message input, not on the
80
+ * attachment preview). */
81
+ export const SingleWithText: StoryFn = () => (
82
+ <StoryPage>
83
+ <StoryHeading
84
+ title="Single image with caption"
85
+ description="Caption renders inside the same bubble, below the image — matches the mobile chat 'Here is the image' design. Composer column is intentionally blank: composer captions live in the message input, not the attachment preview."
86
+ />
87
+ <StoryGrid>
88
+ <StoryRow
89
+ label="Short caption"
90
+ composer={
91
+ <ImageAttachment.Composer
92
+ src={SINGLE_IMAGE}
93
+ alt="Portrait"
94
+ filename="portrait.jpg"
95
+ onDismiss={handleDismiss}
96
+ />
97
+ }
98
+ sent={
99
+ <ImageAttachment.Sent
100
+ src={SINGLE_IMAGE}
101
+ filename="portrait.jpg"
102
+ text="Here is the image"
103
+ />
104
+ }
105
+ received={
106
+ <ImageAttachment.Received
107
+ src={SINGLE_IMAGE}
108
+ filename="portrait.jpg"
109
+ text="Here is the image"
110
+ />
111
+ }
112
+ />
113
+ <StoryRow
114
+ label="Long caption"
115
+ composer={
116
+ <ImageAttachment.Composer
117
+ src={SINGLE_IMAGE}
118
+ alt="Portrait"
119
+ filename="portrait.jpg"
120
+ onDismiss={handleDismiss}
121
+ />
122
+ }
123
+ sent={
124
+ <ImageAttachment.Sent
125
+ src={SINGLE_IMAGE}
126
+ filename="portrait.jpg"
127
+ text="Just got this shot from the new photographer — let me know if you want to use it for the campaign or pick something different."
128
+ />
129
+ }
130
+ received={
131
+ <ImageAttachment.Received
132
+ src={SINGLE_IMAGE}
133
+ filename="portrait.jpg"
134
+ text="Just got this shot from the new photographer — let me know if you want to use it for the campaign or pick something different."
135
+ />
136
+ }
137
+ />
138
+ </StoryGrid>
139
+ </StoryPage>
140
+ )
141
+
142
+ /**
143
+ * Stacked image attachments — multiple images in one bubble laid out
144
+ * in a 1 / 2 / 3 / 4-tile grid. Five or more collapse into a `+N`
145
+ * overflow tile on the bottom-right. Click any tile to open the
146
+ * lightbox at that index, then arrow-navigate between siblings.
147
+ *
148
+ * Sent + Received only — the composer surface accepts a single image
149
+ * at a time, so the Composer column is intentionally blank.
150
+ */
151
+ export const Stacked: StoryFn = () => (
152
+ <StoryPage>
153
+ <StoryHeading
154
+ title="Stacked images"
155
+ description="Click any tile to open the lightbox at that index. Use arrow keys to navigate between siblings. Composer column is intentionally blank — the composer accepts a single image at a time."
156
+ />
157
+ <StoryGrid>
158
+ <StoryRow
159
+ label="2 photos"
160
+ composer={
161
+ <ImageAttachment.Composer
162
+ src={SINGLE_IMAGE}
163
+ alt="Portrait"
164
+ filename="portrait.jpg"
165
+ onDismiss={handleDismiss}
166
+ />
167
+ }
168
+ sent={
169
+ <ImageAttachment.Sent items={STACK_IMAGES.slice(0, 2)} filename="album" />
170
+ }
171
+ received={
172
+ <ImageAttachment.Received
173
+ items={STACK_IMAGES.slice(0, 2)}
174
+ filename="album"
175
+ />
176
+ }
177
+ />
178
+ <StoryRow
179
+ label="3 photos"
180
+ composer={
181
+ <ImageAttachment.Composer
182
+ src={SINGLE_IMAGE}
183
+ alt="Portrait"
184
+ filename="portrait.jpg"
185
+ onDismiss={handleDismiss}
186
+ />
187
+ }
188
+ sent={
189
+ <ImageAttachment.Sent items={STACK_IMAGES.slice(0, 3)} filename="album" />
190
+ }
191
+ received={
192
+ <ImageAttachment.Received
193
+ items={STACK_IMAGES.slice(0, 3)}
194
+ filename="album"
195
+ />
196
+ }
197
+ />
198
+ <StoryRow
199
+ label="4 photos"
200
+ composer={
201
+ <ImageAttachment.Composer
202
+ src={SINGLE_IMAGE}
203
+ alt="Portrait"
204
+ filename="portrait.jpg"
205
+ onDismiss={handleDismiss}
206
+ />
207
+ }
208
+ sent={
209
+ <ImageAttachment.Sent items={STACK_IMAGES.slice(0, 4)} filename="album" />
210
+ }
211
+ received={
212
+ <ImageAttachment.Received
213
+ items={STACK_IMAGES.slice(0, 4)}
214
+ filename="album"
215
+ />
216
+ }
217
+ />
218
+ <StoryRow
219
+ label="6 photos (+overflow)"
220
+ sent={<ImageAttachment.Sent items={STACK_IMAGES} filename="album" />}
221
+ received={<ImageAttachment.Received items={STACK_IMAGES} filename="album" />}
222
+ />
223
+ </StoryGrid>
224
+ </StoryPage>
225
+ )
226
+
227
+ /** Stacked image attachments with an accompanying caption.
228
+ * Sent + Received only — see `Stacked` for the rationale. */
229
+ export const StackedWithText: StoryFn = () => (
230
+ <StoryPage>
231
+ <StoryHeading
232
+ title="Stacked images with caption"
233
+ description="Caption sits inside the same bubble. The grid layout adapts to the tile count above it. Composer column is intentionally blank — the composer accepts a single image at a time."
234
+ />
235
+ <StoryGrid>
236
+ <StoryRow
237
+ label="3 photos + caption"
238
+ composer={
239
+ <ImageAttachment.Composer
240
+ src={SINGLE_IMAGE}
241
+ alt="Portrait"
242
+ filename="portrait.jpg"
243
+ onDismiss={handleDismiss}
244
+ />
245
+ }
246
+ sent={
247
+ <ImageAttachment.Sent
248
+ items={STACK_IMAGES.slice(0, 3)}
249
+ filename="album"
250
+ text="Some shots from the studio session"
251
+ />
252
+ }
253
+ received={
254
+ <ImageAttachment.Received
255
+ items={STACK_IMAGES.slice(0, 3)}
256
+ filename="album"
257
+ text="Some shots from the studio session"
258
+ />
259
+ }
260
+ />
261
+ <StoryRow
262
+ label="6 photos + caption"
263
+ composer={
264
+ <ImageAttachment.Composer
265
+ src={SINGLE_IMAGE}
266
+ alt="Portrait"
267
+ filename="portrait.jpg"
268
+ onDismiss={handleDismiss}
269
+ />
270
+ }
271
+ sent={
272
+ <ImageAttachment.Sent
273
+ items={STACK_IMAGES}
274
+ filename="album"
275
+ text="The whole album from yesterday"
276
+ />
277
+ }
278
+ received={
279
+ <ImageAttachment.Received
280
+ items={STACK_IMAGES}
281
+ filename="album"
282
+ text="The whole album from yesterday"
283
+ />
284
+ }
285
+ />
286
+ </StoryGrid>
287
+ </StoryPage>
288
+ )