@linktr.ee/messaging-react 4.2.0-rc-1787325521 → 4.2.0-rc-1787599724
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-BYJyA83X.js → Card-D7UPq5-g.js} +2 -2
- package/dist/{Card-BYJyA83X.js.map → Card-D7UPq5-g.js.map} +1 -1
- package/dist/{Card-BplJtZao.cjs → Card-DcBZqMBt.cjs} +2 -2
- package/dist/{Card-BplJtZao.cjs.map → Card-DcBZqMBt.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BUC9vqhJ.js → index-CSkr_CO5.js} +2089 -1952
- package/dist/index-CSkr_CO5.js.map +1 -0
- package/dist/index-CwkY7QwT.cjs +3 -0
- package/dist/index-CwkY7QwT.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +59 -78
- package/dist/index.js +26 -30
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +13 -11
- package/src/components/AttachmentCard/utils/icons.ts +7 -6
- package/src/components/Avatar/Avatar.test.tsx +0 -126
- package/src/components/Avatar/getAvatarEmoji.test.ts +0 -39
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +3 -410
- package/src/components/ChannelList/index.test.tsx +1 -132
- package/src/components/ChannelView.stories.tsx +0 -4
- package/src/components/ChannelView.test.tsx +3 -9
- package/src/components/CustomMessage/CustomMessage.stories.tsx +314 -6
- package/src/components/CustomMessage/CustomMessage.test.tsx +2 -56
- package/src/components/CustomMessage/MessageTag.stories.tsx +53 -17
- package/src/components/CustomMessage/MessageTag.test.tsx +40 -7
- package/src/components/CustomMessage/MessageTag.tsx +39 -7
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +7 -8
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -2
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +12 -5
- package/src/components/CustomMessage/index.tsx +19 -22
- package/src/components/CustomMessageInput/index.tsx +1 -1
- package/src/components/CustomSystemMessage/index.tsx +2 -3
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +0 -327
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +153 -0
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +450 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.test.ts +24 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.ts +16 -0
- package/src/components/MessageAttachment/Audio/index.tsx +83 -77
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
- package/src/components/MessageAttachment/_shared/fileMeta.test.ts +0 -44
- package/src/components/MessageAttachment/_shared/useIsClient.ts +23 -0
- package/src/components/MessageAttachment/index.tsx +25 -9
- package/src/components/MessageAttachment/types.ts +19 -9
- package/src/components/MessagingShell/MessagingShell.test.tsx +0 -20
- package/src/components/MessagingShell/index.tsx +0 -2
- package/src/components/RichCard/RichCard.test.tsx +1 -3
- package/src/index.ts +0 -4
- package/src/providers/MessagingProvider.test.tsx +2 -130
- package/src/providers/MessagingProvider.tsx +15 -26
- package/src/styles.css +210 -57
- package/src/testing/createMockMessagingClient.ts +2 -2
- package/src/types.ts +0 -9
- package/dist/index-BUC9vqhJ.js.map +0 -1
- package/dist/index-BkqyKPJP.cjs +0 -3
- package/dist/index-BkqyKPJP.cjs.map +0 -1
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +0 -346
- package/src/components/AttachmentCard/Thumbnail.test.tsx +0 -215
- package/src/components/AttachmentCard/index.test.tsx +0 -160
- package/src/components/ChannelList/ChannelListContext.test.tsx +0 -87
- package/src/components/CustomMessage/Locked.stories.tsx +0 -108
- package/src/components/CustomMessage/PaidMessage.stories.tsx +0 -42
- package/src/components/CustomMessage/Tip.stories.tsx +0 -48
- package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +0 -51
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +0 -65
- package/src/components/CustomMessage/TipMessage/index.tsx +0 -27
- package/src/components/FaqList/FaqList.test.tsx +0 -204
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +0 -76
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +0 -248
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +0 -123
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +0 -193
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +0 -242
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +0 -37
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +0 -72
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +0 -162
- package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +0 -182
- package/src/components/MessageAttachment/_shared/Bubble.test.tsx +0 -111
- package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +0 -36
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +0 -114
- package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +0 -42
- package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +0 -168
- package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +0 -52
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +0 -69
- package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +0 -45
- package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +0 -55
- package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +0 -246
- package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +0 -113
- package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +0 -68
- package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +0 -16
- package/src/logging/index.test.tsx +0 -136
- package/src/logging/index.tsx +0 -48
- package/src/stories/customMessageStoryTemplate.tsx +0 -202
|
@@ -483,330 +483,3 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
483
483
|
)
|
|
484
484
|
})
|
|
485
485
|
})
|
|
486
|
-
|
|
487
|
-
const AUDIO = {
|
|
488
|
-
mimeType: 'audio/mpeg',
|
|
489
|
-
sourceUrl: 'https://cdn.example.com/clip.mp3',
|
|
490
|
-
}
|
|
491
|
-
const VIDEO = {
|
|
492
|
-
mimeType: 'video/mp4',
|
|
493
|
-
sourceUrl: 'https://cdn.example.com/clip.mp4',
|
|
494
|
-
}
|
|
495
|
-
const HERO = 'https://cdn.example.com/yoga.jpg'
|
|
496
|
-
|
|
497
|
-
// Sent / Composer cards carry no test hook of their own; the shell is the
|
|
498
|
-
// element that owns the card chrome classes.
|
|
499
|
-
const shellOf = (container: HTMLElement) =>
|
|
500
|
-
container.querySelector('.messaging-link-card') as HTMLElement
|
|
501
|
-
|
|
502
|
-
describe('LinkAttachment.Composer chrome', () => {
|
|
503
|
-
it('anchors the dismiss control to the hero on a featured card', () => {
|
|
504
|
-
const onDismiss = vi.fn()
|
|
505
|
-
const { container } = renderWithProviders(
|
|
506
|
-
<LinkAttachment.Composer
|
|
507
|
-
title="Brie’s FAQ"
|
|
508
|
-
thumbnailUrl={HERO}
|
|
509
|
-
onDismiss={onDismiss}
|
|
510
|
-
/>
|
|
511
|
-
)
|
|
512
|
-
const dismiss = screen.getByLabelText('Dismiss attachment')
|
|
513
|
-
expect(dismiss.parentElement?.className).toContain('absolute right-3 top-3')
|
|
514
|
-
// Sits inside the hero region, not the shell corner.
|
|
515
|
-
expect(container.querySelector('div.flex-1')?.contains(dismiss)).toBe(true)
|
|
516
|
-
// 44x44 touch target without growing the 24px circle.
|
|
517
|
-
expect(dismiss.className).toContain('before:-inset-[10px]')
|
|
518
|
-
fireEvent.click(dismiss)
|
|
519
|
-
expect(onDismiss).toHaveBeenCalledTimes(1)
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
it('moves the dismiss control to the shell corner on a classic card', () => {
|
|
523
|
-
const { container } = renderWithProviders(
|
|
524
|
-
<LinkAttachment.Composer
|
|
525
|
-
title="Brie’s FAQ"
|
|
526
|
-
thumbnailUrl={HERO}
|
|
527
|
-
layout="classic"
|
|
528
|
-
onDismiss={() => {}}
|
|
529
|
-
/>
|
|
530
|
-
)
|
|
531
|
-
expect(container.querySelector('img')).toBeNull()
|
|
532
|
-
const shell = shellOf(container)
|
|
533
|
-
expect(shell.contains(screen.getByLabelText('Dismiss attachment'))).toBe(
|
|
534
|
-
true
|
|
535
|
-
)
|
|
536
|
-
// Classic cards size to their content.
|
|
537
|
-
expect(shell.className).not.toContain('h-[250px]')
|
|
538
|
-
expect(shell.className).toContain('block')
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
it('defaults to the featured layout with its hero and fixed box', () => {
|
|
542
|
-
const { container } = renderWithProviders(
|
|
543
|
-
<LinkAttachment.Composer title="Brie’s FAQ" thumbnailUrl={HERO} />
|
|
544
|
-
)
|
|
545
|
-
expect(container.querySelector('img')?.getAttribute('src')).toBe(HERO)
|
|
546
|
-
expect(shellOf(container).className).toContain('h-[250px]')
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
it('collapses an audio composer card to the player plus an inline dismiss', () => {
|
|
550
|
-
const { container } = renderWithProviders(
|
|
551
|
-
<LinkAttachment.Composer
|
|
552
|
-
title="My track"
|
|
553
|
-
url="tr.ee/briemix"
|
|
554
|
-
{...AUDIO}
|
|
555
|
-
onDismiss={() => {}}
|
|
556
|
-
/>
|
|
557
|
-
)
|
|
558
|
-
const shell = shellOf(container)
|
|
559
|
-
expect(shell.className).toContain(AUDIO_BG_CLASS)
|
|
560
|
-
expect(container.querySelector('audio')).not.toBeNull()
|
|
561
|
-
// No chin on this path, so the URL line goes with it.
|
|
562
|
-
expect(screen.queryByTestId('link-attachment-url')).toBeNull()
|
|
563
|
-
const dismiss = screen.getByLabelText('Dismiss attachment')
|
|
564
|
-
// Inline sibling of the native controls: no overlay halo on top of them.
|
|
565
|
-
expect(dismiss.className).not.toContain('before:-inset-[10px]')
|
|
566
|
-
expect(dismiss.parentElement?.className).not.toContain('absolute')
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
it('lets the consumer name the dismiss control', () => {
|
|
570
|
-
renderWithProviders(
|
|
571
|
-
<LinkAttachment.Composer
|
|
572
|
-
title="Brie’s FAQ"
|
|
573
|
-
onDismiss={() => {}}
|
|
574
|
-
dismissLabel="Remove link"
|
|
575
|
-
/>
|
|
576
|
-
)
|
|
577
|
-
expect(screen.getByLabelText('Remove link')).toBeInTheDocument()
|
|
578
|
-
expect(screen.queryByLabelText('Dismiss attachment')).toBeNull()
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
it('renders the edit affordance only when a handler is given', () => {
|
|
582
|
-
const onEditClick = vi.fn()
|
|
583
|
-
renderWithProviders(
|
|
584
|
-
<LinkAttachment.Composer title="Brie’s FAQ" onEditClick={onEditClick} />
|
|
585
|
-
)
|
|
586
|
-
fireEvent.click(screen.getByLabelText('Edit attachment'))
|
|
587
|
-
expect(onEditClick).toHaveBeenCalledTimes(1)
|
|
588
|
-
expect(screen.queryByLabelText('Dismiss attachment')).toBeNull()
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
it('renders no affordances when the composer passes no handlers', () => {
|
|
592
|
-
renderWithProviders(<LinkAttachment.Composer title="Brie’s FAQ" />)
|
|
593
|
-
expect(screen.queryByRole('button')).toBeNull()
|
|
594
|
-
})
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
describe('LinkAttachment interactivity contract', () => {
|
|
598
|
-
it('keeps the Received shell non-interactive around playable media', () => {
|
|
599
|
-
const onClick = vi.fn()
|
|
600
|
-
renderWithProviders(
|
|
601
|
-
<LinkAttachment.Received
|
|
602
|
-
title="Clip"
|
|
603
|
-
url="tr.ee/briemix"
|
|
604
|
-
thumbnailUrl={HERO}
|
|
605
|
-
{...VIDEO}
|
|
606
|
-
onClick={onClick}
|
|
607
|
-
/>
|
|
608
|
-
)
|
|
609
|
-
// The native controls own clicks, so the shell is neither anchor nor button.
|
|
610
|
-
const root = screen.getByTestId('link-attachment')
|
|
611
|
-
expect(root.tagName).toBe('DIV')
|
|
612
|
-
fireEvent.click(root)
|
|
613
|
-
expect(onClick).not.toHaveBeenCalled()
|
|
614
|
-
})
|
|
615
|
-
|
|
616
|
-
it('keeps the Received shell non-interactive when a CTA owns the click', () => {
|
|
617
|
-
renderWithProviders(
|
|
618
|
-
<LinkAttachment.Received
|
|
619
|
-
title="FAQ"
|
|
620
|
-
url="tr.ee/briemix"
|
|
621
|
-
cta={{ label: 'View FAQs' }}
|
|
622
|
-
/>
|
|
623
|
-
)
|
|
624
|
-
expect(screen.getByTestId('link-attachment').tagName).toBe('DIV')
|
|
625
|
-
})
|
|
626
|
-
|
|
627
|
-
it('treats a whitespace-only Received url as no url at all', () => {
|
|
628
|
-
renderWithProviders(
|
|
629
|
-
<LinkAttachment.Received title="Brie’s FAQ" url=" " />
|
|
630
|
-
)
|
|
631
|
-
const root = screen.getByTestId('link-attachment')
|
|
632
|
-
expect(root.tagName).toBe('DIV')
|
|
633
|
-
expect(root.hasAttribute('href')).toBe(false)
|
|
634
|
-
expect(screen.queryByTestId('link-attachment-url')).toBeNull()
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
it('names the Received preview button after the title, then falls back', () => {
|
|
638
|
-
const { unmount } = renderWithProviders(
|
|
639
|
-
<LinkAttachment.Received
|
|
640
|
-
title="Featured image"
|
|
641
|
-
thumbnailUrl={HERO}
|
|
642
|
-
onClick={() => {}}
|
|
643
|
-
/>
|
|
644
|
-
)
|
|
645
|
-
expect(screen.getByTestId('link-attachment')).toHaveAttribute(
|
|
646
|
-
'aria-label',
|
|
647
|
-
'Featured image'
|
|
648
|
-
)
|
|
649
|
-
unmount()
|
|
650
|
-
|
|
651
|
-
renderWithProviders(
|
|
652
|
-
<LinkAttachment.Received thumbnailUrl={HERO} onClick={() => {}} />
|
|
653
|
-
)
|
|
654
|
-
expect(screen.getByTestId('link-attachment')).toHaveAttribute(
|
|
655
|
-
'aria-label',
|
|
656
|
-
'Open attachment preview'
|
|
657
|
-
)
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
it('ignores the Sent onClick when there is nothing to open', () => {
|
|
661
|
-
const onClick = vi.fn()
|
|
662
|
-
const { container } = renderWithProviders(
|
|
663
|
-
<LinkAttachment.Sent title="My Playlist" onClick={onClick} />
|
|
664
|
-
)
|
|
665
|
-
const shell = shellOf(container)
|
|
666
|
-
expect(shell.tagName).toBe('DIV')
|
|
667
|
-
fireEvent.click(shell)
|
|
668
|
-
expect(onClick).not.toHaveBeenCalled()
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
it('ignores the Sent onClick around playable media', () => {
|
|
672
|
-
const onClick = vi.fn()
|
|
673
|
-
const { container } = renderWithProviders(
|
|
674
|
-
<LinkAttachment.Sent
|
|
675
|
-
title="Clip"
|
|
676
|
-
url="tr.ee/briemix"
|
|
677
|
-
thumbnailUrl={HERO}
|
|
678
|
-
{...VIDEO}
|
|
679
|
-
onClick={onClick}
|
|
680
|
-
/>
|
|
681
|
-
)
|
|
682
|
-
const shell = shellOf(container)
|
|
683
|
-
expect(shell.tagName).toBe('DIV')
|
|
684
|
-
fireEvent.click(shell)
|
|
685
|
-
expect(onClick).not.toHaveBeenCalled()
|
|
686
|
-
})
|
|
687
|
-
})
|
|
688
|
-
|
|
689
|
-
describe('LinkAttachment bubble tail grouping', () => {
|
|
690
|
-
it.each([
|
|
691
|
-
['single', true],
|
|
692
|
-
['end', true],
|
|
693
|
-
['first', false],
|
|
694
|
-
['middle', false],
|
|
695
|
-
] as const)(
|
|
696
|
-
'paints the Received tail for groupPosition %s: %s',
|
|
697
|
-
(groupPosition, expected) => {
|
|
698
|
-
renderWithProviders(
|
|
699
|
-
<LinkAttachment.Received
|
|
700
|
-
title="Brie’s FAQ"
|
|
701
|
-
url="tr.ee/briemix"
|
|
702
|
-
groupPosition={groupPosition}
|
|
703
|
-
/>
|
|
704
|
-
)
|
|
705
|
-
expect(screen.queryByTestId('link-card-tail') != null).toBe(expected)
|
|
706
|
-
}
|
|
707
|
-
)
|
|
708
|
-
|
|
709
|
-
it.each([
|
|
710
|
-
['single', true],
|
|
711
|
-
['end', true],
|
|
712
|
-
['first', false],
|
|
713
|
-
] as const)(
|
|
714
|
-
'paints the Sent tail for groupPosition %s: %s',
|
|
715
|
-
(groupPosition, expected) => {
|
|
716
|
-
renderWithProviders(
|
|
717
|
-
<LinkAttachment.Sent
|
|
718
|
-
title="Brie’s FAQ"
|
|
719
|
-
url="tr.ee/briemix"
|
|
720
|
-
groupPosition={groupPosition}
|
|
721
|
-
/>
|
|
722
|
-
)
|
|
723
|
-
expect(screen.queryByTestId('link-card-tail') != null).toBe(expected)
|
|
724
|
-
}
|
|
725
|
-
)
|
|
726
|
-
|
|
727
|
-
it('defaults both chat cards to the tail-bearing single position', () => {
|
|
728
|
-
const { unmount } = renderWithProviders(
|
|
729
|
-
<LinkAttachment.Received title="Brie’s FAQ" url="tr.ee/briemix" />
|
|
730
|
-
)
|
|
731
|
-
expect(screen.getByTestId('link-card-tail')).toHaveAttribute(
|
|
732
|
-
'data-variant',
|
|
733
|
-
'light'
|
|
734
|
-
)
|
|
735
|
-
unmount()
|
|
736
|
-
|
|
737
|
-
renderWithProviders(
|
|
738
|
-
<LinkAttachment.Sent title="Brie’s FAQ" url="tr.ee/briemix" />
|
|
739
|
-
)
|
|
740
|
-
expect(screen.getByTestId('link-card-tail')).toHaveAttribute(
|
|
741
|
-
'data-variant',
|
|
742
|
-
'dark'
|
|
743
|
-
)
|
|
744
|
-
})
|
|
745
|
-
|
|
746
|
-
it('gives the audio player card no tail — it is not a bubble', () => {
|
|
747
|
-
const { unmount } = renderWithProviders(
|
|
748
|
-
<LinkAttachment.Sent title="My track" {...AUDIO} groupPosition="end" />
|
|
749
|
-
)
|
|
750
|
-
expect(screen.queryByTestId('link-card-tail')).toBeNull()
|
|
751
|
-
unmount()
|
|
752
|
-
|
|
753
|
-
renderWithProviders(
|
|
754
|
-
<LinkAttachment.Received
|
|
755
|
-
title="My track"
|
|
756
|
-
{...AUDIO}
|
|
757
|
-
groupPosition="end"
|
|
758
|
-
/>
|
|
759
|
-
)
|
|
760
|
-
expect(screen.queryByTestId('link-card-tail')).toBeNull()
|
|
761
|
-
})
|
|
762
|
-
})
|
|
763
|
-
|
|
764
|
-
describe('LinkAttachment layout and surface', () => {
|
|
765
|
-
it.each([
|
|
766
|
-
['Sent', LinkAttachment.Sent],
|
|
767
|
-
['Received', LinkAttachment.Received],
|
|
768
|
-
] as const)(
|
|
769
|
-
'drops the %s hero and the fixed box for classic',
|
|
770
|
-
(_side, Card) => {
|
|
771
|
-
const { container } = renderWithProviders(
|
|
772
|
-
<Card
|
|
773
|
-
title="Brie’s FAQ"
|
|
774
|
-
url="tr.ee/briemix"
|
|
775
|
-
thumbnailUrl={HERO}
|
|
776
|
-
layout="classic"
|
|
777
|
-
/>
|
|
778
|
-
)
|
|
779
|
-
expect(container.querySelector('img')).toBeNull()
|
|
780
|
-
const shell = shellOf(container)
|
|
781
|
-
expect(shell.className).not.toContain('h-[250px]')
|
|
782
|
-
expect(shell.className).toContain('block')
|
|
783
|
-
}
|
|
784
|
-
)
|
|
785
|
-
|
|
786
|
-
it.each([
|
|
787
|
-
['Sent', LinkAttachment.Sent],
|
|
788
|
-
['Received', LinkAttachment.Received],
|
|
789
|
-
] as const)('locks the featured %s card to the 250px box', (_side, Card) => {
|
|
790
|
-
const { container } = renderWithProviders(
|
|
791
|
-
<Card title="Brie’s FAQ" url="tr.ee/briemix" thumbnailUrl={HERO} />
|
|
792
|
-
)
|
|
793
|
-
expect(container.querySelector('img')?.getAttribute('src')).toBe(HERO)
|
|
794
|
-
expect(shellOf(container).className).toContain('h-[250px]')
|
|
795
|
-
})
|
|
796
|
-
|
|
797
|
-
it.each([
|
|
798
|
-
['Sent', LinkAttachment.Sent],
|
|
799
|
-
['Received', LinkAttachment.Received],
|
|
800
|
-
] as const)(
|
|
801
|
-
'sizes the %s audio card to its content on the audio surface',
|
|
802
|
-
(_side, Card) => {
|
|
803
|
-
const { container } = renderWithProviders(
|
|
804
|
-
<Card title="My track" url="tr.ee/briemix" {...AUDIO} />
|
|
805
|
-
)
|
|
806
|
-
const shell = shellOf(container)
|
|
807
|
-
expect(shell.className).toContain(AUDIO_BG_CLASS)
|
|
808
|
-
expect(shell.className).not.toContain('h-[250px]')
|
|
809
|
-
expect(container.querySelector('audio')).not.toBeNull()
|
|
810
|
-
}
|
|
811
|
-
)
|
|
812
|
-
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from '@storybook/react'
|
|
1
|
+
import type { Decorator, Meta, StoryFn } from '@storybook/react'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { Chat, WithAudioPlayback } from 'stream-chat-react'
|
|
3
4
|
|
|
5
|
+
import { createMockStreamChatClient } from '../../../testing/createMockStreamChatClient'
|
|
4
6
|
import {
|
|
5
7
|
StoryGrid,
|
|
6
8
|
StoryHeading,
|
|
@@ -14,27 +16,63 @@ import AudioAttachment from '.'
|
|
|
14
16
|
const AUDIO_SRC =
|
|
15
17
|
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
|
16
18
|
|
|
19
|
+
/** Resolves nothing, so the metadata probe never fills the duration slot. */
|
|
20
|
+
const UNREACHABLE_SRC = 'https://cdn.example.invalid/unreachable.mp3'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The exact amplitudes Figma draws (`2754:17710` → `sound-wave`
|
|
24
|
+
* `…;2754:8065`), as bar heights on the 30px track normalised to `0..1`.
|
|
25
|
+
* Rendering the design's own values is what makes these stories a usable
|
|
26
|
+
* pixel reference.
|
|
27
|
+
*/
|
|
28
|
+
const FIGMA_WAVEFORM = [
|
|
29
|
+
4, 4, 4, 4, 24, 20, 16, 21, 8, 9, 13, 15, 16, 21, 24, 18, 14, 11, 11, 11, 11,
|
|
30
|
+
11, 18, 24, 24, 30, 25, 21, 19, 16, 11, 12, 8, 4, 4,
|
|
31
|
+
].map((height) => height / 30)
|
|
32
|
+
|
|
17
33
|
const STACK_AUDIO: AudioItem[] = [
|
|
18
34
|
{
|
|
19
35
|
src: AUDIO_SRC,
|
|
20
36
|
mimeType: 'audio/mpeg',
|
|
21
37
|
filename: 'Take 1 — full mix.mp3',
|
|
38
|
+
durationSeconds: 12,
|
|
39
|
+
waveformData: FIGMA_WAVEFORM,
|
|
22
40
|
},
|
|
23
41
|
{
|
|
24
42
|
src: AUDIO_SRC,
|
|
25
43
|
mimeType: 'audio/mpeg',
|
|
26
44
|
filename: 'Take 2 — vocals isolated.mp3',
|
|
45
|
+
durationSeconds: 95,
|
|
46
|
+
waveformData: [...FIGMA_WAVEFORM].reverse(),
|
|
27
47
|
},
|
|
28
48
|
{
|
|
29
49
|
src: AUDIO_SRC,
|
|
30
50
|
mimeType: 'audio/mpeg',
|
|
31
51
|
filename: 'Take 3 — instrumental only.mp3',
|
|
52
|
+
durationSeconds: 3812,
|
|
32
53
|
},
|
|
33
54
|
]
|
|
34
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The card's playback comes from `stream-chat-react`'s pooled
|
|
58
|
+
* `AudioPlayer`, which `<Channel>` mounts in production. Mounting the
|
|
59
|
+
* pool directly gives the stories the same contract — real playback,
|
|
60
|
+
* one player at a time across the page — without needing a channel that
|
|
61
|
+
* can `watch()` against a backend. Without it every play / seek control
|
|
62
|
+
* renders disabled.
|
|
63
|
+
*/
|
|
64
|
+
const withAudioPlayback: Decorator = (Story) => (
|
|
65
|
+
<Chat client={createMockStreamChatClient({ id: 'story-user' })}>
|
|
66
|
+
<WithAudioPlayback>
|
|
67
|
+
<Story />
|
|
68
|
+
</WithAudioPlayback>
|
|
69
|
+
</Chat>
|
|
70
|
+
)
|
|
71
|
+
|
|
35
72
|
const meta: Meta = {
|
|
36
73
|
title: 'MessageAttachment/Audio',
|
|
37
74
|
parameters: { layout: 'fullscreen' },
|
|
75
|
+
decorators: [withAudioPlayback],
|
|
38
76
|
}
|
|
39
77
|
export default meta
|
|
40
78
|
|
|
@@ -43,16 +81,16 @@ const handleDismiss = () => {
|
|
|
43
81
|
}
|
|
44
82
|
|
|
45
83
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
84
|
+
* The design's waveform player card: a 299×72 bubble holding a 32px
|
|
85
|
+
* play toggle, a 35-bar waveform with a progress fill and an invisible
|
|
86
|
+
* native seek control over it, the duration, and a 32px trailing action
|
|
87
|
+
* — Remove in the composer, Download once sent.
|
|
50
88
|
*/
|
|
51
89
|
export const Single: StoryFn = () => (
|
|
52
90
|
<StoryPage>
|
|
53
91
|
<StoryHeading
|
|
54
92
|
title="Audio attachment"
|
|
55
|
-
description="
|
|
93
|
+
description="Play to watch the fill advance bar by bar and the duration count up; click or drag the waveform to seek, or Tab to it and use the arrow keys. Starting a second card pauses the first — one player at a time across the whole thread."
|
|
56
94
|
/>
|
|
57
95
|
<StoryGrid>
|
|
58
96
|
<StoryRow
|
|
@@ -62,6 +100,8 @@ export const Single: StoryFn = () => (
|
|
|
62
100
|
src={AUDIO_SRC}
|
|
63
101
|
mimeType="audio/mpeg"
|
|
64
102
|
filename="Morning meditation.mp3"
|
|
103
|
+
durationSeconds={12}
|
|
104
|
+
waveformData={FIGMA_WAVEFORM}
|
|
65
105
|
onDismiss={handleDismiss}
|
|
66
106
|
/>
|
|
67
107
|
}
|
|
@@ -70,6 +110,8 @@ export const Single: StoryFn = () => (
|
|
|
70
110
|
src={AUDIO_SRC}
|
|
71
111
|
mimeType="audio/mpeg"
|
|
72
112
|
filename="Morning meditation.mp3"
|
|
113
|
+
durationSeconds={12}
|
|
114
|
+
waveformData={FIGMA_WAVEFORM}
|
|
73
115
|
/>
|
|
74
116
|
}
|
|
75
117
|
received={
|
|
@@ -77,6 +119,8 @@ export const Single: StoryFn = () => (
|
|
|
77
119
|
src={AUDIO_SRC}
|
|
78
120
|
mimeType="audio/mpeg"
|
|
79
121
|
filename="Morning meditation.mp3"
|
|
122
|
+
durationSeconds={12}
|
|
123
|
+
waveformData={FIGMA_WAVEFORM}
|
|
80
124
|
/>
|
|
81
125
|
}
|
|
82
126
|
/>
|
|
@@ -84,20 +128,28 @@ export const Single: StoryFn = () => (
|
|
|
84
128
|
</StoryPage>
|
|
85
129
|
)
|
|
86
130
|
|
|
87
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Every case where no amplitudes can be produced — a decode failure, an
|
|
133
|
+
* unsupported codec, a file over the composer's decode ceiling, a clip
|
|
134
|
+
* re-staged from *Recently shared* (whose `File` is a zero-byte
|
|
135
|
+
* placeholder), or anything sent before `waveform_data` was persisted
|
|
136
|
+
* (legacy web, the mobile app, a visitor).
|
|
137
|
+
*/
|
|
138
|
+
export const NoWaveform: StoryFn = () => (
|
|
88
139
|
<StoryPage>
|
|
89
140
|
<StoryHeading
|
|
90
|
-
title="
|
|
91
|
-
description="
|
|
141
|
+
title="Degraded states"
|
|
142
|
+
description="The waveform falls back to every bar at the design's 4px minimum, so the card keeps exactly the same silhouette and the progress fill still tracks playback. An unknown duration holds its slot as `--:--` until a metadata probe resolves it."
|
|
92
143
|
/>
|
|
93
144
|
<StoryGrid>
|
|
94
145
|
<StoryRow
|
|
95
|
-
label="
|
|
146
|
+
label="No waveform"
|
|
96
147
|
composer={
|
|
97
148
|
<AudioAttachment.Composer
|
|
98
149
|
src={AUDIO_SRC}
|
|
99
150
|
mimeType="audio/mpeg"
|
|
100
|
-
filename="
|
|
151
|
+
filename="Voice note.m4a"
|
|
152
|
+
durationSeconds={12}
|
|
101
153
|
onDismiss={handleDismiss}
|
|
102
154
|
/>
|
|
103
155
|
}
|
|
@@ -105,87 +157,100 @@ export const SingleWithText: StoryFn = () => (
|
|
|
105
157
|
<AudioAttachment.Sent
|
|
106
158
|
src={AUDIO_SRC}
|
|
107
159
|
mimeType="audio/mpeg"
|
|
108
|
-
filename="
|
|
109
|
-
|
|
160
|
+
filename="Voice note.m4a"
|
|
161
|
+
durationSeconds={12}
|
|
110
162
|
/>
|
|
111
163
|
}
|
|
112
164
|
received={
|
|
113
165
|
<AudioAttachment.Received
|
|
114
166
|
src={AUDIO_SRC}
|
|
115
167
|
mimeType="audio/mpeg"
|
|
116
|
-
filename="
|
|
117
|
-
|
|
168
|
+
filename="Voice note.m4a"
|
|
169
|
+
durationSeconds={12}
|
|
118
170
|
/>
|
|
119
171
|
}
|
|
120
172
|
/>
|
|
121
|
-
</StoryGrid>
|
|
122
|
-
</StoryPage>
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Stacked audio attachments — multiple clips inside one bubble. Each
|
|
127
|
-
* clip renders its own native `<audio controls>` player, vertically
|
|
128
|
-
* stacked with a hairline separator.
|
|
129
|
-
*
|
|
130
|
-
* Sent + Received only — the composer surface accepts a single
|
|
131
|
-
* attachment at a time.
|
|
132
|
-
*/
|
|
133
|
-
export const Stacked: StoryFn = () => (
|
|
134
|
-
<StoryPage>
|
|
135
|
-
<StoryHeading
|
|
136
|
-
title="Stacked audio"
|
|
137
|
-
description="Each clip plays independently with its own native controls. Composer column is intentionally blank — the composer accepts a single attachment at a time."
|
|
138
|
-
/>
|
|
139
|
-
<StoryGrid>
|
|
140
173
|
<StoryRow
|
|
141
|
-
label="
|
|
174
|
+
label="No waveform, no duration"
|
|
142
175
|
composer={
|
|
143
176
|
<AudioAttachment.Composer
|
|
144
|
-
src={
|
|
145
|
-
|
|
146
|
-
filename="Morning meditation.mp3"
|
|
177
|
+
src={UNREACHABLE_SRC}
|
|
178
|
+
filename="Unreachable.mp3"
|
|
147
179
|
onDismiss={handleDismiss}
|
|
148
180
|
/>
|
|
149
181
|
}
|
|
150
|
-
sent={
|
|
151
|
-
|
|
182
|
+
sent={
|
|
183
|
+
<AudioAttachment.Sent
|
|
184
|
+
src={UNREACHABLE_SRC}
|
|
185
|
+
filename="Unreachable.mp3"
|
|
186
|
+
/>
|
|
187
|
+
}
|
|
188
|
+
received={
|
|
189
|
+
<AudioAttachment.Received
|
|
190
|
+
src={UNREACHABLE_SRC}
|
|
191
|
+
filename="Unreachable.mp3"
|
|
192
|
+
/>
|
|
193
|
+
}
|
|
152
194
|
/>
|
|
153
195
|
<StoryRow
|
|
154
|
-
label="
|
|
196
|
+
label="Unplayable codec"
|
|
155
197
|
composer={
|
|
156
198
|
<AudioAttachment.Composer
|
|
157
199
|
src={AUDIO_SRC}
|
|
158
|
-
mimeType="audio/
|
|
159
|
-
filename="
|
|
200
|
+
mimeType="audio/x-made-up"
|
|
201
|
+
filename="Master.aiff"
|
|
202
|
+
durationSeconds={12}
|
|
203
|
+
waveformData={FIGMA_WAVEFORM}
|
|
160
204
|
onDismiss={handleDismiss}
|
|
161
205
|
/>
|
|
162
206
|
}
|
|
163
|
-
sent={
|
|
164
|
-
|
|
207
|
+
sent={
|
|
208
|
+
<AudioAttachment.Sent
|
|
209
|
+
src={AUDIO_SRC}
|
|
210
|
+
mimeType="audio/x-made-up"
|
|
211
|
+
filename="Master.aiff"
|
|
212
|
+
durationSeconds={12}
|
|
213
|
+
waveformData={FIGMA_WAVEFORM}
|
|
214
|
+
/>
|
|
215
|
+
}
|
|
216
|
+
received={
|
|
217
|
+
<AudioAttachment.Received
|
|
218
|
+
src={AUDIO_SRC}
|
|
219
|
+
mimeType="audio/x-made-up"
|
|
220
|
+
filename="Master.aiff"
|
|
221
|
+
durationSeconds={12}
|
|
222
|
+
waveformData={FIGMA_WAVEFORM}
|
|
223
|
+
/>
|
|
224
|
+
}
|
|
165
225
|
/>
|
|
166
226
|
</StoryGrid>
|
|
167
227
|
</StoryPage>
|
|
168
228
|
)
|
|
169
229
|
|
|
170
|
-
/**
|
|
171
|
-
*
|
|
172
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Stacked audio — multiple clips clustered into one bubble, as
|
|
232
|
+
* `StreamAttachmentMessage` does for consecutive same-kind attachments.
|
|
233
|
+
* Sent + Received only: the composer accepts one attachment at a time.
|
|
234
|
+
*/
|
|
235
|
+
export const Stacked: StoryFn = () => (
|
|
173
236
|
<StoryPage>
|
|
174
237
|
<StoryHeading
|
|
175
|
-
title="Stacked audio
|
|
176
|
-
description="
|
|
238
|
+
title="Stacked audio"
|
|
239
|
+
description="Each clip is its own row and its own player, stacked with an 8px gap. Only one can play at a time. The third row has no waveform, so it shows the flat fallback beside two real ones."
|
|
177
240
|
/>
|
|
178
241
|
<StoryGrid>
|
|
242
|
+
<StoryRow
|
|
243
|
+
label="2 clips"
|
|
244
|
+
sent={<AudioAttachment.Sent items={STACK_AUDIO.slice(0, 2)} />}
|
|
245
|
+
received={<AudioAttachment.Received items={STACK_AUDIO.slice(0, 2)} />}
|
|
246
|
+
/>
|
|
247
|
+
<StoryRow
|
|
248
|
+
label="3 clips"
|
|
249
|
+
sent={<AudioAttachment.Sent items={STACK_AUDIO} />}
|
|
250
|
+
received={<AudioAttachment.Received items={STACK_AUDIO} />}
|
|
251
|
+
/>
|
|
179
252
|
<StoryRow
|
|
180
253
|
label="3 clips + caption"
|
|
181
|
-
composer={
|
|
182
|
-
<AudioAttachment.Composer
|
|
183
|
-
src={AUDIO_SRC}
|
|
184
|
-
mimeType="audio/mpeg"
|
|
185
|
-
filename="Morning meditation.mp3"
|
|
186
|
-
onDismiss={handleDismiss}
|
|
187
|
-
/>
|
|
188
|
-
}
|
|
189
254
|
sent={
|
|
190
255
|
<AudioAttachment.Sent
|
|
191
256
|
items={STACK_AUDIO}
|
|
@@ -202,3 +267,45 @@ export const StackedWithText: StoryFn = () => (
|
|
|
202
267
|
</StoryGrid>
|
|
203
268
|
</StoryPage>
|
|
204
269
|
)
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The card keeps a uniform 20px radius in every grouping position — a
|
|
273
|
+
* 32px control tucked into a 4px flattened corner would collide with it
|
|
274
|
+
* — so a run of audio bubbles reads as a stack of identical cards.
|
|
275
|
+
*/
|
|
276
|
+
export const Grouped: StoryFn = () => (
|
|
277
|
+
<StoryPage>
|
|
278
|
+
<StoryHeading
|
|
279
|
+
title="Same-author run"
|
|
280
|
+
description="`groupPosition` still serialises onto the bubble, but the audio card opts out of the corner flattening the text bubbles apply."
|
|
281
|
+
/>
|
|
282
|
+
<StoryGrid>
|
|
283
|
+
{(['first', 'middle', 'end'] as const).map((groupPosition) => (
|
|
284
|
+
<StoryRow
|
|
285
|
+
key={groupPosition}
|
|
286
|
+
label={groupPosition}
|
|
287
|
+
sent={
|
|
288
|
+
<AudioAttachment.Sent
|
|
289
|
+
src={AUDIO_SRC}
|
|
290
|
+
mimeType="audio/mpeg"
|
|
291
|
+
filename="Morning meditation.mp3"
|
|
292
|
+
durationSeconds={12}
|
|
293
|
+
waveformData={FIGMA_WAVEFORM}
|
|
294
|
+
groupPosition={groupPosition}
|
|
295
|
+
/>
|
|
296
|
+
}
|
|
297
|
+
received={
|
|
298
|
+
<AudioAttachment.Received
|
|
299
|
+
src={AUDIO_SRC}
|
|
300
|
+
mimeType="audio/mpeg"
|
|
301
|
+
filename="Morning meditation.mp3"
|
|
302
|
+
durationSeconds={12}
|
|
303
|
+
waveformData={FIGMA_WAVEFORM}
|
|
304
|
+
groupPosition={groupPosition}
|
|
305
|
+
/>
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
308
|
+
))}
|
|
309
|
+
</StoryGrid>
|
|
310
|
+
</StoryPage>
|
|
311
|
+
)
|