@linktr.ee/messaging-react 2.4.0 → 2.4.2

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 (35) hide show
  1. package/dist/{Card-3s62K02i.js → Card-B8CGfnPN.js} +16 -16
  2. package/dist/{Card-3s62K02i.js.map → Card-B8CGfnPN.js.map} +1 -1
  3. package/dist/{Card-LQWaY4ng.cjs → Card-BFTHMj8n.cjs} +2 -2
  4. package/dist/{Card-LQWaY4ng.cjs.map → Card-BFTHMj8n.cjs.map} +1 -1
  5. package/dist/{Card-CPl5CfCo.cjs → Card-CwFkVePn.cjs} +2 -2
  6. package/dist/{Card-CPl5CfCo.cjs.map → Card-CwFkVePn.cjs.map} +1 -1
  7. package/dist/{Card-pMjhg42K.js → Card-D8Z8WnS6.js} +6 -6
  8. package/dist/{Card-pMjhg42K.js.map → Card-D8Z8WnS6.js.map} +1 -1
  9. package/dist/{Card-Cp6OyEWg.cjs → Card-sogfIdhf.cjs} +2 -2
  10. package/dist/{Card-Cp6OyEWg.cjs.map → Card-sogfIdhf.cjs.map} +1 -1
  11. package/dist/{Card-CkyEIXm3.js → Card-tGCMHgKp.js} +19 -19
  12. package/dist/{Card-CkyEIXm3.js.map → Card-tGCMHgKp.js.map} +1 -1
  13. package/dist/{LockedThumbnail-DbGrvo6R.cjs → LockedThumbnail-DLtrbU8H.cjs} +2 -2
  14. package/dist/{LockedThumbnail-DbGrvo6R.cjs.map → LockedThumbnail-DLtrbU8H.cjs.map} +1 -1
  15. package/dist/{LockedThumbnail-CjznOfiC.js → LockedThumbnail-WG8CysWx.js} +8 -8
  16. package/dist/{LockedThumbnail-CjznOfiC.js.map → LockedThumbnail-WG8CysWx.js.map} +1 -1
  17. package/dist/index-C541s_4h.js +4740 -0
  18. package/dist/index-C541s_4h.js.map +1 -0
  19. package/dist/index-DYLdOjSj.cjs +2 -0
  20. package/dist/index-DYLdOjSj.cjs.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.ts +17 -2
  23. package/dist/index.js +1 -1
  24. package/package.json +1 -1
  25. package/src/components/ChannelView.test.tsx +58 -2
  26. package/src/components/ChannelView.tsx +10 -3
  27. package/src/components/CustomMessage/index.tsx +1 -1
  28. package/src/components/LinkAttachment/LinkAttachment.test.tsx +35 -1
  29. package/src/components/LinkAttachment/components/Sent/Card.tsx +66 -25
  30. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +4 -3
  31. package/src/types.ts +8 -2
  32. package/dist/index-BX305h7q.cjs +0 -18
  33. package/dist/index-BX305h7q.cjs.map +0 -1
  34. package/dist/index-DVh2uNBI.js +0 -8142
  35. package/dist/index-DVh2uNBI.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BX305h7q.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-DYLdOjSj.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -610,6 +610,18 @@ export declare interface LinkAttachmentReceivedCardProps extends LinkAttachmentB
610
610
  }
611
611
 
612
612
  export declare interface LinkAttachmentSentCardProps extends LinkAttachmentBaseProps {
613
+ /**
614
+ * Fired when the sender activates their own posted card. Mirrors the
615
+ * `Received` variant so analytics fires symmetrically on both sides
616
+ * of the conversation. Behavior depends on configuration:
617
+ * - **Link app with a URL**: the card chrome is an `<a target="_blank">`
618
+ * opening `url`; `onClick` fires alongside the navigation.
619
+ * - **Link app with a CTA**: the CTA owns navigation; `onClick` is
620
+ * ignored (the CTA's own `onClick` is the analytics hook).
621
+ * - **Video / audio link previews**: the shell stays non-interactive
622
+ * so the native media controls remain operable; `onClick` is ignored.
623
+ */
624
+ onClick?: () => void;
613
625
  }
614
626
 
615
627
  export declare const LockedAttachment: {
@@ -1054,10 +1066,13 @@ declare type MessageCustomType = 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATB
1054
1066
 
1055
1067
  /**
1056
1068
  * Callback invoked when a user clicks a link inside the channel view.
1057
- * Receives the clicked URL. Fires alongside the native navigation,
1069
+ * Receives the clicked URL and, when resolvable, the message the link
1070
+ * was rendered in (so consumers can filter on sender or metadata).
1071
+ * `message` is `undefined` when the click happens on a link outside a
1072
+ * resolvable message wrapper. Fires alongside the native navigation,
1058
1073
  * not in place of it.
1059
1074
  */
1060
- export declare type MessageLinkClickHandler = (url: string) => void;
1075
+ export declare type MessageLinkClickHandler = (url: string, message: LocalMessage | undefined) => void;
1061
1076
 
1062
1077
  export declare interface MessageMetadata {
1063
1078
  custom_type?: MessageCustomType;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-DVh2uNBI.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-C541s_4h.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -357,7 +357,7 @@ describe('ChannelView', () => {
357
357
  expect(screen.getAllByText('Custom Label').length).toBeGreaterThan(0)
358
358
  })
359
359
 
360
- it('fires onMessageLinkClick with the href when a link inside the view is clicked', () => {
360
+ it('fires onMessageLinkClick with the href and undefined message when the link is not inside a message wrapper', () => {
361
361
  const handler = vi.fn()
362
362
  const { container } = renderWithProviders(
363
363
  <ChannelView channel={createChannel()} onMessageLinkClick={handler} />
@@ -372,7 +372,63 @@ describe('ChannelView', () => {
372
372
  anchor.click()
373
373
 
374
374
  expect(handler).toHaveBeenCalledOnce()
375
- expect(handler).toHaveBeenCalledWith('https://example.com/clicked')
375
+ expect(handler).toHaveBeenCalledWith(
376
+ 'https://example.com/clicked',
377
+ undefined
378
+ )
379
+ })
380
+
381
+ it('resolves the surrounding message from channel state and passes it to onMessageLinkClick', () => {
382
+ const handler = vi.fn()
383
+ const message = {
384
+ id: 'message-42',
385
+ text: 'check this out https://example.com/clicked',
386
+ metadata: { custom_type: 'MESSAGE_CHATBOT' },
387
+ } as unknown as ReturnType<
388
+ Channel['state']['formatMessage']
389
+ >
390
+ const channel = createChannel()
391
+ ;(
392
+ channel.state as unknown as { messages: typeof message[] }
393
+ ).messages = [message]
394
+
395
+ const { container } = renderWithProviders(
396
+ <ChannelView channel={channel} onMessageLinkClick={handler} />
397
+ )
398
+
399
+ const wrapper = document.createElement('div')
400
+ wrapper.setAttribute('data-message-id', 'message-42')
401
+ const anchor = document.createElement('a')
402
+ anchor.href = 'https://example.com/clicked'
403
+ anchor.addEventListener('click', (e) => e.preventDefault())
404
+ wrapper.appendChild(anchor)
405
+ container.querySelector('.messaging-channel-view')?.appendChild(wrapper)
406
+ anchor.click()
407
+
408
+ expect(handler).toHaveBeenCalledOnce()
409
+ expect(handler).toHaveBeenCalledWith('https://example.com/clicked', message)
410
+ })
411
+
412
+ it('passes undefined when the wrapper data-message-id does not match any message in state', () => {
413
+ const handler = vi.fn()
414
+ const { container } = renderWithProviders(
415
+ <ChannelView channel={createChannel()} onMessageLinkClick={handler} />
416
+ )
417
+
418
+ const wrapper = document.createElement('div')
419
+ wrapper.setAttribute('data-message-id', 'unknown-id')
420
+ const anchor = document.createElement('a')
421
+ anchor.href = 'https://example.com/clicked'
422
+ anchor.addEventListener('click', (e) => e.preventDefault())
423
+ wrapper.appendChild(anchor)
424
+ container.querySelector('.messaging-channel-view')?.appendChild(wrapper)
425
+ anchor.click()
426
+
427
+ expect(handler).toHaveBeenCalledOnce()
428
+ expect(handler).toHaveBeenCalledWith(
429
+ 'https://example.com/clicked',
430
+ undefined
431
+ )
376
432
  })
377
433
 
378
434
  it('does not throw when a link is clicked without onMessageLinkClick set', () => {
@@ -538,13 +538,20 @@ export const ChannelView = React.memo<ChannelViewProps>(
538
538
  const container = containerRef.current
539
539
  if (!container) return
540
540
  const handler = (event: MouseEvent) => {
541
- const anchor = (event.target as HTMLElement | null)?.closest('a[href]')
541
+ const target = event.target as HTMLElement | null
542
+ const anchor = target?.closest('a[href]')
542
543
  const href = anchor?.getAttribute('href')
543
- if (href) onMessageLinkClick(href)
544
+ if (!href) return
545
+ const messageWrapper = anchor?.closest<HTMLElement>('[data-message-id]')
546
+ const messageId = messageWrapper?.getAttribute('data-message-id')
547
+ const message = messageId
548
+ ? channel.state.messages.find((m) => m.id === messageId)
549
+ : undefined
550
+ onMessageLinkClick(href, message)
544
551
  }
545
552
  container.addEventListener('click', handler)
546
553
  return () => container.removeEventListener('click', handler)
547
- }, [onMessageLinkClick])
554
+ }, [onMessageLinkClick, channel])
548
555
 
549
556
  return (
550
557
  <div
@@ -194,7 +194,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
194
194
  open={isBounceDialogOpen}
195
195
  />
196
196
  )}
197
- <div className={rootClassName} key={message.id}>
197
+ <div className={rootClassName} key={message.id} data-message-id={message.id}>
198
198
  {PinIndicator && <PinIndicator />}
199
199
  {!!reminder && <ReminderNotification reminder={reminder} />}
200
200
  {message.user && (
@@ -54,16 +54,50 @@ describe('LinkAttachment.Received (link previews)', () => {
54
54
  })
55
55
 
56
56
  describe('LinkAttachment.Sent', () => {
57
- it('renders the dark variant non-interactively', async () => {
57
+ it('renders the dark variant as an external link when url is set', async () => {
58
+ const handleClick = vi.fn()
58
59
  renderWithProviders(
59
60
  <LinkAttachment.Sent
60
61
  title="My Playlist"
61
62
  description="A handpicked workout mix"
62
63
  url="tr.ee/briemix"
64
+ onClick={handleClick}
65
+ />
66
+ )
67
+ const link = await screen.findByRole('link', { name: /My Playlist/i })
68
+ expect(link.tagName).toBe('A')
69
+ expect(link.getAttribute('href')).toBe('https://tr.ee/briemix')
70
+ expect(link.getAttribute('target')).toBe('_blank')
71
+ expect(link.getAttribute('rel')).toBe('noopener noreferrer')
72
+ fireEvent.click(link)
73
+ expect(handleClick).toHaveBeenCalledTimes(1)
74
+ })
75
+
76
+ it('stays non-interactive when no url is set', async () => {
77
+ renderWithProviders(
78
+ <LinkAttachment.Sent
79
+ title="My Playlist"
80
+ description="A handpicked workout mix"
63
81
  />
64
82
  )
65
83
  expect(await screen.findByText('My Playlist')).toBeInTheDocument()
66
84
  expect(screen.queryByRole('button')).toBeNull()
67
85
  expect(screen.queryByRole('link')).toBeNull()
68
86
  })
87
+
88
+ it('lets the CTA own navigation when one is set (shell stays non-interactive)', async () => {
89
+ renderWithProviders(
90
+ <LinkAttachment.Sent
91
+ title="FAQ"
92
+ description="Get answers"
93
+ url="tr.ee/briemix"
94
+ cta={{ label: 'View FAQs', href: 'tr.ee/faq' }}
95
+ />
96
+ )
97
+ expect(await screen.findByText('FAQ')).toBeInTheDocument()
98
+ const links = screen.getAllByRole('link')
99
+ expect(links).toHaveLength(1)
100
+ expect(links[0].getAttribute('href')).toBe('https://tr.ee/faq')
101
+ expect(links[0]).toHaveTextContent('View FAQs')
102
+ })
69
103
  })
@@ -6,14 +6,32 @@ import CardShell from '../_shared/CardShell'
6
6
  import CardThumbnail, {
7
7
  AUDIO_BG_CLASS,
8
8
  isPlayableAudio,
9
+ isPlayableMedia,
9
10
  } from '../_shared/CardThumbnail'
11
+ import { normalizeExternalHref } from '../_shared/normalizeExternalHref'
10
12
 
11
- export interface SentCardProps extends LinkAttachmentBaseProps {}
13
+ export interface SentCardProps extends LinkAttachmentBaseProps {
14
+ /**
15
+ * Fired when the sender activates their own posted card. Mirrors the
16
+ * `Received` variant so analytics fires symmetrically on both sides
17
+ * of the conversation. Behavior depends on configuration:
18
+ * - **Link app with a URL**: the card chrome is an `<a target="_blank">`
19
+ * opening `url`; `onClick` fires alongside the navigation.
20
+ * - **Link app with a CTA**: the CTA owns navigation; `onClick` is
21
+ * ignored (the CTA's own `onClick` is the analytics hook).
22
+ * - **Video / audio link previews**: the shell stays non-interactive
23
+ * so the native media controls remain operable; `onClick` is ignored.
24
+ */
25
+ onClick?: () => void
26
+ }
12
27
 
13
28
  /**
14
29
  * The card the sender sees in chat after a link attachment has been posted.
15
30
  * Matches the Sent column of the messaging design system in Figma — same
16
31
  * dark chrome as the Composer card minus the dismiss / edit affordances.
32
+ *
33
+ * The sender's own copy is clickable when a `url` is set so they can open
34
+ * the link they just sent — symmetric with `LinkAttachment.Received`.
17
35
  */
18
36
  const SentCard: React.FC<SentCardProps> = ({
19
37
  title,
@@ -26,32 +44,55 @@ const SentCard: React.FC<SentCardProps> = ({
26
44
  layout = 'featured',
27
45
  appIcon,
28
46
  cta,
29
- }) => (
30
- <CardShell
31
- variant="dark"
32
- bgClassName={
33
- isPlayableAudio(mimeType, sourceUrl) ? AUDIO_BG_CLASS : undefined
34
- }
35
- >
36
- {layout === 'featured' && (
37
- <CardThumbnail
47
+ onClick,
48
+ }) => {
49
+ // Video / audio link previews wrap the native media element — render a
50
+ // plain non-interactive shell and let the media controls own clicks so
51
+ // taps on play/pause/scrubber don't fire the outer card action. Same
52
+ // rationale as `ReceivedCard`.
53
+ const isPlayingMedia = isPlayableMedia(mimeType, sourceUrl)
54
+ // Normalize the URL so a bare hostname like `tr.ee/briemix` is treated
55
+ // as an external link instead of resolving against the current host.
56
+ // Returns `undefined` for empty / whitespace-only values, so those fall
57
+ // through to a non-navigational shell instead of producing an empty
58
+ // `href` on the anchor.
59
+ const normalizedUrl = normalizeExternalHref(url)
60
+ const shellHref =
61
+ cta == null && normalizedUrl != null && !isPlayingMedia
62
+ ? normalizedUrl
63
+ : undefined
64
+ const shellOnClick =
65
+ cta == null && !isPlayingMedia && shellHref != null ? onClick : undefined
66
+
67
+ return (
68
+ <CardShell
69
+ variant="dark"
70
+ href={shellHref}
71
+ onClick={shellOnClick}
72
+ bgClassName={
73
+ isPlayableAudio(mimeType, sourceUrl) ? AUDIO_BG_CLASS : undefined
74
+ }
75
+ >
76
+ {layout === 'featured' && (
77
+ <CardThumbnail
78
+ variant="dark"
79
+ thumbnailUrl={thumbnailUrl}
80
+ sourceUrl={sourceUrl}
81
+ title={title}
82
+ mimeType={mimeType}
83
+ />
84
+ )}
85
+ <CardBody
38
86
  variant="dark"
39
- thumbnailUrl={thumbnailUrl}
40
- sourceUrl={sourceUrl}
41
87
  title={title}
42
- mimeType={mimeType}
88
+ placeholderTitle={placeholderTitle}
89
+ description={description}
90
+ url={url}
91
+ appIcon={appIcon}
92
+ cta={cta}
43
93
  />
44
- )}
45
- <CardBody
46
- variant="dark"
47
- title={title}
48
- placeholderTitle={placeholderTitle}
49
- description={description}
50
- url={url}
51
- appIcon={appIcon}
52
- cta={cta}
53
- />
54
- </CardShell>
55
- )
94
+ </CardShell>
95
+ )
96
+ }
56
97
 
57
98
  export default SentCard
@@ -7,9 +7,10 @@ export interface CardShellProps {
7
7
  variant: LinkAttachmentVariant
8
8
  children: React.ReactNode
9
9
  /**
10
- * When provided, the entire card chrome is rendered as an anchor (used by
11
- * the Received card to open the link target on click). Falls back to a
12
- * `<div>` when omitted so Composer / Sent cards stay non-navigational.
10
+ * When provided, the entire card chrome is rendered as an anchor (used
11
+ * by Sent / Received cards to open the link target on click). Falls
12
+ * back to a `<div>` when omitted so Composer cards stay
13
+ * non-navigational while drafting.
13
14
  */
14
15
  href?: string
15
16
  /**
package/src/types.ts CHANGED
@@ -22,10 +22,16 @@ export type { LockedAttachmentSource } from './components/LockedAttachment'
22
22
 
23
23
  /**
24
24
  * Callback invoked when a user clicks a link inside the channel view.
25
- * Receives the clicked URL. Fires alongside the native navigation,
25
+ * Receives the clicked URL and, when resolvable, the message the link
26
+ * was rendered in (so consumers can filter on sender or metadata).
27
+ * `message` is `undefined` when the click happens on a link outside a
28
+ * resolvable message wrapper. Fires alongside the native navigation,
26
29
  * not in place of it.
27
30
  */
28
- export type MessageLinkClickHandler = (url: string) => void
31
+ export type MessageLinkClickHandler = (
32
+ url: string,
33
+ message: LocalMessage | undefined
34
+ ) => void
29
35
 
30
36
  /**
31
37
  * Generic participant interface for different host environments