@linktr.ee/messaging-react 1.32.1 → 1.33.0-rc-1777504230

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 (33) hide show
  1. package/dist/{Card-ClE_iExA.js → Card-BsqYzZt1.js} +55 -55
  2. package/dist/Card-BsqYzZt1.js.map +1 -0
  3. package/dist/{Card-1CQEn-OT.js → Card-Cnn9V-W7.js} +44 -44
  4. package/dist/Card-Cnn9V-W7.js.map +1 -0
  5. package/dist/assets/index.css +1 -1
  6. package/dist/index-BMfupE8K.js +3130 -0
  7. package/dist/index-BMfupE8K.js.map +1 -0
  8. package/dist/index.d.ts +19 -1
  9. package/dist/index.js +20 -2477
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/components/ChannelInfoDialog/index.tsx +3 -1
  13. package/src/components/ChannelView.stories.tsx +38 -0
  14. package/src/components/ChannelView.test.tsx +25 -6
  15. package/src/components/ChannelView.tsx +26 -6
  16. package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +180 -0
  17. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +63 -1
  18. package/src/components/CustomMessageInput/index.tsx +24 -5
  19. package/src/components/LockedAttachment/components/Creator/Card.tsx +11 -11
  20. package/src/components/LockedAttachment/components/MediaPlayer.tsx +10 -1
  21. package/src/components/LockedAttachment/components/Visitor/Card.tsx +9 -9
  22. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +2 -2
  23. package/src/components/MediaMessage/MediaMessage.stories.tsx +233 -0
  24. package/src/components/MediaMessage/MediaMessage.test.tsx +520 -0
  25. package/src/components/MediaMessage/index.tsx +476 -0
  26. package/src/components/MessagingShell/index.tsx +2 -0
  27. package/src/index.ts +2 -0
  28. package/src/styles.css +49 -0
  29. package/src/types.ts +13 -0
  30. package/dist/Card-1CQEn-OT.js.map +0 -1
  31. package/dist/Card-ClE_iExA.js.map +0 -1
  32. package/dist/MediaPlayer-B9Ws2NeE.js +0 -292
  33. package/dist/MediaPlayer-B9Ws2NeE.js.map +0 -1
@@ -0,0 +1,520 @@
1
+ import React from 'react'
2
+ import type { LocalMessage } from 'stream-chat'
3
+ import { beforeAll, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { renderWithProviders, screen, fireEvent } from '../../test/utils'
6
+
7
+ import { MediaMessage } from '.'
8
+
9
+ beforeAll(() => {
10
+ HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) {
11
+ this.setAttribute('open', '')
12
+ })
13
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
14
+ this.removeAttribute('open')
15
+ })
16
+ })
17
+
18
+ vi.mock('../Avatar', () => ({
19
+ Avatar: ({ id }: { id: string }) => (
20
+ <div data-testid="avatar" data-user-id={id} />
21
+ ),
22
+ }))
23
+
24
+ vi.mock('../LockedAttachment/components/MediaPlayer', () => ({
25
+ default: ({
26
+ source,
27
+ mimeType,
28
+ onContainerClick,
29
+ }: {
30
+ source: string
31
+ mimeType: string
32
+ onContainerClick?: (e: React.MouseEvent) => void
33
+ }) => (
34
+ <div
35
+ role="button"
36
+ tabIndex={0}
37
+ data-testid="media-player"
38
+ data-source={source}
39
+ data-mime-type={mimeType}
40
+ onClick={onContainerClick}
41
+ onKeyDown={undefined}
42
+ />
43
+ ),
44
+ }))
45
+
46
+ vi.mock('../LockedAttachment/utils/icons', () => ({
47
+ renderTypeIcon: () => <span data-testid="type-icon" />,
48
+ }))
49
+
50
+ vi.mock('../LockedAttachment/utils/mimeType', () => ({
51
+ getSourceType: (mimeType: string) => {
52
+ if (mimeType.startsWith('video/')) return 'video'
53
+ if (mimeType.startsWith('image/')) return 'image'
54
+ if (mimeType.startsWith('audio/')) return 'audio'
55
+ return 'document'
56
+ },
57
+ }))
58
+
59
+ const msg = (overrides: Record<string, unknown> = {}): LocalMessage =>
60
+ ({
61
+ id: 'msg-1',
62
+ text: '',
63
+ type: 'regular',
64
+ created_at: new Date(),
65
+ updated_at: new Date(),
66
+ deleted_at: null,
67
+ pinned_at: null,
68
+ status: 'received',
69
+ user: { id: 'user-1', name: 'Alice' },
70
+ attachments: [],
71
+ ...overrides,
72
+ }) as unknown as LocalMessage
73
+
74
+ describe('MediaMessage', () => {
75
+ it('renders nothing when no media URL is resolvable', () => {
76
+ const { container } = renderWithProviders(<MediaMessage message={msg()} />)
77
+ expect(container.firstChild).toBeNull()
78
+ })
79
+
80
+ it('renders MediaPlayer immediately for a video attachment', () => {
81
+ renderWithProviders(
82
+ <MediaMessage
83
+ message={msg({
84
+ attachments: [
85
+ {
86
+ type: 'video',
87
+ asset_url: 'https://cdn.example.com/clip.mp4',
88
+ mime_type: 'video/mp4',
89
+ title: 'My Clip',
90
+ },
91
+ ],
92
+ })}
93
+ />
94
+ )
95
+
96
+ const player = screen.getByTestId('media-player')
97
+ expect(player).toBeInTheDocument()
98
+ expect(player).toHaveAttribute('data-source', 'https://cdn.example.com/clip.mp4')
99
+ })
100
+
101
+ it('opens viewer when the video player container is clicked', () => {
102
+ renderWithProviders(
103
+ <MediaMessage
104
+ message={msg({
105
+ attachments: [
106
+ {
107
+ type: 'video',
108
+ asset_url: 'https://cdn.example.com/clip.mp4',
109
+ mime_type: 'video/mp4',
110
+ title: 'My Clip',
111
+ },
112
+ ],
113
+ })}
114
+ />
115
+ )
116
+
117
+ fireEvent.click(screen.getByTestId('media-player'))
118
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
119
+ expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
120
+ })
121
+
122
+ it('renders MediaPlayer for an audio attachment', () => {
123
+ renderWithProviders(
124
+ <MediaMessage
125
+ message={msg({
126
+ attachments: [
127
+ {
128
+ type: 'audio',
129
+ asset_url: 'https://cdn.example.com/track.mp3',
130
+ mime_type: 'audio/mpeg',
131
+ },
132
+ ],
133
+ })}
134
+ />
135
+ )
136
+
137
+ expect(screen.getByTestId('media-player')).toHaveAttribute(
138
+ 'data-source',
139
+ 'https://cdn.example.com/track.mp3'
140
+ )
141
+ })
142
+
143
+ it('does not show viewer or expand button for audio', () => {
144
+ renderWithProviders(
145
+ <MediaMessage
146
+ message={msg({
147
+ attachments: [
148
+ {
149
+ type: 'audio',
150
+ asset_url: 'https://cdn.example.com/track.mp3',
151
+ mime_type: 'audio/mpeg',
152
+ title: 'My Track',
153
+ },
154
+ ],
155
+ })}
156
+ />
157
+ )
158
+
159
+ expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
160
+ expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
161
+ })
162
+
163
+ it('renders an img for an image attachment and opens viewer on click', () => {
164
+ renderWithProviders(
165
+ <MediaMessage
166
+ message={msg({
167
+ attachments: [
168
+ {
169
+ type: 'image',
170
+ image_url: 'https://cdn.example.com/photo.jpg',
171
+ mime_type: 'image/jpeg',
172
+ title: 'My Photo',
173
+ },
174
+ ],
175
+ })}
176
+ />
177
+ )
178
+
179
+ const image = screen.getByRole('img', { name: 'My Photo' })
180
+ expect(image).toHaveAttribute('src', 'https://cdn.example.com/photo.jpg')
181
+
182
+ // Clicking the image button opens the viewer
183
+ fireEvent.click(screen.getByRole('button', { name: 'My Photo' }))
184
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
185
+ const viewerImages = screen.getAllByRole('img', { name: 'My Photo' })
186
+ expect(viewerImages.some((img) => img.classList.contains('rounded-2xl'))).toBe(true)
187
+ })
188
+
189
+ it('does not show expand button in chin for images, only download', () => {
190
+ renderWithProviders(
191
+ <MediaMessage
192
+ message={msg({
193
+ attachments: [
194
+ {
195
+ type: 'image',
196
+ image_url: 'https://cdn.example.com/photo.jpg',
197
+ mime_type: 'image/jpeg',
198
+ title: 'My Photo',
199
+ },
200
+ ],
201
+ })}
202
+ />
203
+ )
204
+
205
+ // Thumbnail button + download button — no separate expand/fullscreen button
206
+ expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
207
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
208
+ })
209
+
210
+ it('renders PDF card with a viewer button', () => {
211
+ renderWithProviders(
212
+ <MediaMessage
213
+ message={msg({
214
+ attachments: [
215
+ {
216
+ type: 'file',
217
+ asset_url: 'https://cdn.example.com/report.pdf',
218
+ mime_type: 'application/pdf',
219
+ title: 'Annual Report',
220
+ },
221
+ ],
222
+ })}
223
+ />
224
+ )
225
+
226
+ expect(screen.getByRole('button', { name: 'Open PDF viewer' })).toBeInTheDocument()
227
+ expect(screen.getByText('Annual Report')).toBeInTheDocument()
228
+ })
229
+
230
+ it('opens PDF viewer when PDF thumbnail is clicked', () => {
231
+ renderWithProviders(
232
+ <MediaMessage
233
+ message={msg({
234
+ attachments: [
235
+ {
236
+ type: 'file',
237
+ asset_url: 'https://cdn.example.com/report.pdf',
238
+ mime_type: 'application/pdf',
239
+ title: 'Annual Report',
240
+ },
241
+ ],
242
+ })}
243
+ />
244
+ )
245
+
246
+ fireEvent.click(screen.getByRole('button', { name: 'Open PDF viewer' }))
247
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
248
+ expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
249
+ })
250
+
251
+ it('renders unknown file as a link with no viewer button', () => {
252
+ renderWithProviders(
253
+ <MediaMessage
254
+ message={msg({
255
+ attachments: [
256
+ {
257
+ type: 'file',
258
+ asset_url: 'https://cdn.example.com/data.bin',
259
+ mime_type: 'application/octet-stream',
260
+ title: 'Unknown File',
261
+ },
262
+ ],
263
+ })}
264
+ />
265
+ )
266
+
267
+ // No viewer button for unknown files
268
+ expect(screen.queryByRole('button', { name: 'Open PDF viewer' })).not.toBeInTheDocument()
269
+ // Thumbnail is a plain link (opens in new tab)
270
+ const links = screen.getAllByRole('link')
271
+ expect(links.some((l) => l.getAttribute('href') === 'https://cdn.example.com/data.bin')).toBe(true)
272
+ expect(screen.getByText('Unknown File')).toBeInTheDocument()
273
+ })
274
+
275
+ it('shows title and file size in chin', () => {
276
+ renderWithProviders(
277
+ <MediaMessage
278
+ message={msg({
279
+ attachments: [
280
+ {
281
+ type: 'video',
282
+ asset_url: 'https://cdn.example.com/clip.mp4',
283
+ mime_type: 'video/mp4',
284
+ title: 'Clip',
285
+ file_size: 2048,
286
+ },
287
+ ],
288
+ })}
289
+ />
290
+ )
291
+
292
+ expect(screen.getByText('Clip')).toBeInTheDocument()
293
+ expect(screen.getByText('2.0 KB')).toBeInTheDocument()
294
+ })
295
+
296
+ it('renders Avatar for a message from another user', () => {
297
+ renderWithProviders(
298
+ <MediaMessage
299
+ isMyMessage={false}
300
+ message={msg({
301
+ attachments: [
302
+ {
303
+ type: 'image',
304
+ image_url: 'https://cdn.example.com/photo.jpg',
305
+ mime_type: 'image/jpeg',
306
+ },
307
+ ],
308
+ })}
309
+ />
310
+ )
311
+
312
+ expect(screen.getByTestId('avatar')).toBeInTheDocument()
313
+ expect(screen.getByTestId('avatar')).toHaveAttribute('data-user-id', 'user-1')
314
+ })
315
+
316
+ it('does not render Avatar for own messages', () => {
317
+ renderWithProviders(
318
+ <MediaMessage
319
+ isMyMessage={true}
320
+ message={msg({
321
+ attachments: [
322
+ {
323
+ type: 'image',
324
+ image_url: 'https://cdn.example.com/photo.jpg',
325
+ mime_type: 'image/jpeg',
326
+ },
327
+ ],
328
+ })}
329
+ />
330
+ )
331
+
332
+ expect(screen.queryByTestId('avatar')).not.toBeInTheDocument()
333
+ })
334
+
335
+ it('applies the --me class for own messages', () => {
336
+ const { container } = renderWithProviders(
337
+ <MediaMessage
338
+ isMyMessage={true}
339
+ message={msg({
340
+ attachments: [
341
+ {
342
+ type: 'image',
343
+ image_url: 'https://cdn.example.com/photo.jpg',
344
+ mime_type: 'image/jpeg',
345
+ },
346
+ ],
347
+ })}
348
+ />
349
+ )
350
+
351
+ expect(container.firstChild).toHaveClass('str-chat__message--me')
352
+ })
353
+
354
+ it('applies the --other class for messages from other users', () => {
355
+ const { container } = renderWithProviders(
356
+ <MediaMessage
357
+ isMyMessage={false}
358
+ message={msg({
359
+ attachments: [
360
+ {
361
+ type: 'image',
362
+ image_url: 'https://cdn.example.com/photo.jpg',
363
+ mime_type: 'image/jpeg',
364
+ },
365
+ ],
366
+ })}
367
+ />
368
+ )
369
+
370
+ expect(container.firstChild).toHaveClass('str-chat__message--other')
371
+ })
372
+
373
+ it('renders nothing when no attachments', () => {
374
+ const { container } = renderWithProviders(
375
+ <MediaMessage message={msg({ attachments: [] })} />
376
+ )
377
+
378
+ expect(container.firstChild).toBeNull()
379
+ })
380
+
381
+ it('renders a link card for a link attachment', () => {
382
+ renderWithProviders(
383
+ <MediaMessage
384
+ message={msg({
385
+ attachments: [
386
+ {
387
+ type: 'link',
388
+ og_scrape_url: 'https://linktr.ee/someone',
389
+ title: 'My Linktree',
390
+ text: 'Check out my links',
391
+ image_url: 'https://cdn.example.com/thumb.jpg',
392
+ },
393
+ ],
394
+ })}
395
+ />
396
+ )
397
+
398
+ const link = screen.getByRole('link')
399
+ expect(link).toHaveAttribute('href', 'https://linktr.ee/someone')
400
+ expect(screen.getByText('My Linktree')).toBeInTheDocument()
401
+ expect(screen.getByText('Check out my links')).toBeInTheDocument()
402
+ expect(screen.getByText('https://linktr.ee/someone')).toBeInTheDocument()
403
+ const img = screen.getByRole('img', { name: 'My Linktree' })
404
+ expect(img).toHaveAttribute('src', 'https://cdn.example.com/thumb.jpg')
405
+ })
406
+
407
+ it('renders a link fallback icon when link has no image', () => {
408
+ renderWithProviders(
409
+ <MediaMessage
410
+ message={msg({
411
+ attachments: [
412
+ {
413
+ type: 'link',
414
+ og_scrape_url: 'https://linktr.ee/someone',
415
+ title: 'My Linktree',
416
+ },
417
+ ],
418
+ })}
419
+ />
420
+ )
421
+
422
+ expect(screen.queryByRole('img')).not.toBeInTheDocument()
423
+ expect(screen.getByText('My Linktree')).toBeInTheDocument()
424
+ })
425
+
426
+ it('uses dark card background for sent messages', () => {
427
+ const { container } = renderWithProviders(
428
+ <MediaMessage
429
+ isMyMessage={true}
430
+ message={msg({
431
+ attachments: [
432
+ {
433
+ type: 'image',
434
+ image_url: 'https://cdn.example.com/photo.jpg',
435
+ mime_type: 'image/jpeg',
436
+ },
437
+ ],
438
+ })}
439
+ />
440
+ )
441
+
442
+ expect(container.querySelector('.bg-\\[\\#121110\\]')).toBeInTheDocument()
443
+ })
444
+
445
+ it('uses light card background for received messages', () => {
446
+ const { container } = renderWithProviders(
447
+ <MediaMessage
448
+ isMyMessage={false}
449
+ message={msg({
450
+ attachments: [
451
+ {
452
+ type: 'image',
453
+ image_url: 'https://cdn.example.com/photo.jpg',
454
+ mime_type: 'image/jpeg',
455
+ },
456
+ ],
457
+ })}
458
+ />
459
+ )
460
+
461
+ expect(container.querySelector('.bg-\\[\\#F3F3F1\\]')).toBeInTheDocument()
462
+ })
463
+
464
+ it('shows download button for image attachment', () => {
465
+ renderWithProviders(
466
+ <MediaMessage
467
+ message={msg({
468
+ attachments: [
469
+ {
470
+ type: 'image',
471
+ image_url: 'https://cdn.example.com/photo.jpg',
472
+ mime_type: 'image/jpeg',
473
+ title: 'My Photo',
474
+ },
475
+ ],
476
+ })}
477
+ />
478
+ )
479
+
480
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
481
+ })
482
+
483
+ it('shows download button for audio attachment', () => {
484
+ renderWithProviders(
485
+ <MediaMessage
486
+ message={msg({
487
+ attachments: [
488
+ {
489
+ type: 'audio',
490
+ asset_url: 'https://cdn.example.com/track.mp3',
491
+ mime_type: 'audio/mpeg',
492
+ title: 'My Track',
493
+ },
494
+ ],
495
+ })}
496
+ />
497
+ )
498
+
499
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
500
+ })
501
+
502
+ it('does not show download or expand buttons for link cards', () => {
503
+ renderWithProviders(
504
+ <MediaMessage
505
+ message={msg({
506
+ attachments: [
507
+ {
508
+ type: 'link',
509
+ og_scrape_url: 'https://linktr.ee/someone',
510
+ title: 'My Linktree',
511
+ },
512
+ ],
513
+ })}
514
+ />
515
+ )
516
+
517
+ expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
518
+ expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
519
+ })
520
+ })