@linktr.ee/messaging-react 3.37.1 → 3.38.0-rc-1786342246

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 (39) hide show
  1. package/dist/{Card-DVMzYquF.js → Card-C40UQcWN.js} +2 -2
  2. package/dist/{Card-DVMzYquF.js.map → Card-C40UQcWN.js.map} +1 -1
  3. package/dist/{Card-Dxk_Ggxw.cjs → Card-C8glaNwz.cjs} +2 -2
  4. package/dist/{Card-Dxk_Ggxw.cjs.map → Card-C8glaNwz.cjs.map} +1 -1
  5. package/dist/assets/index.css +1 -1
  6. package/dist/index-Byt_hX2r.cjs +3 -0
  7. package/dist/index-Byt_hX2r.cjs.map +1 -0
  8. package/dist/{index-BOBIiAuD.js → index-fk5ihkQY.js} +2240 -2277
  9. package/dist/index-fk5ihkQY.js.map +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.d.ts +1 -34
  12. package/dist/index.js +27 -29
  13. package/package.json +4 -3
  14. package/src/components/ChannelView.tsx +1 -6
  15. package/src/components/CustomMessage/CustomMessage.test.tsx +0 -12
  16. package/src/components/CustomMessage/MessageTag.test.tsx +9 -18
  17. package/src/components/CustomMessage/MessageTag.tsx +16 -17
  18. package/src/components/CustomMessage/MessageTail.tsx +45 -0
  19. package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +9 -2
  20. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +18 -4
  21. package/src/components/CustomMessage/index.tsx +23 -23
  22. package/src/components/LinkAttachment/components/_shared/BubbleTail.tsx +5 -6
  23. package/src/components/MessageAttachment/_shared/CarouselNav.tsx +5 -6
  24. package/src/components/MessageAttachment/_shared/DownloadAction.tsx +5 -8
  25. package/src/components/MessageAttachment/_shared/ImageViewer.tsx +8 -12
  26. package/src/components/MessageAttachment/_shared/PdfViewer.tsx +1 -1
  27. package/src/components/MessageAttachment/_shared/VideoViewer.tsx +1 -1
  28. package/src/components/MessageAttachment/_shared/ViewerShell.tsx +16 -11
  29. package/src/components/MessageAttachment/_shared/viewerChromeClasses.ts +16 -0
  30. package/src/components/MessagingShell/index.tsx +0 -2
  31. package/src/index.ts +0 -3
  32. package/src/styles.css +56 -377
  33. package/src/types.ts +0 -8
  34. package/dist/index-BOBIiAuD.js.map +0 -1
  35. package/dist/index-D7A8U8GQ.cjs +0 -3
  36. package/dist/index-D7A8U8GQ.cjs.map +0 -1
  37. package/src/components/CustomMessage/MessageVoteButtons.stories.tsx +0 -95
  38. package/src/components/CustomMessage/MessageVoteButtons.tsx +0 -47
  39. package/src/hooks/useMessageVote.ts +0 -77
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-D7A8U8GQ.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.MessageBubble=e.MessageBubble;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;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-Byt_hX2r.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.MessageBubble=e.MessageBubble;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -215,7 +215,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
215
215
  * Props that MessagingShell passes through to ChannelView.
216
216
  * ChannelViewProps is the source of truth for these props.
217
217
  */
218
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onBroadcastClick' | 'resolveBroadcastName' | 'showChannelInfo' | 'composerInput'>;
218
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onBroadcastClick' | 'resolveBroadcastName' | 'showChannelInfo' | 'composerInput'>;
219
219
 
220
220
  /**
221
221
  * ChannelView component props
@@ -337,13 +337,6 @@ export declare interface ChannelViewProps {
337
337
  * participant name; it now solely gates the actions menu.
338
338
  */
339
339
  showChannelInfo?: boolean;
340
- /**
341
- * Enable thumbs up/down voting on chatbot messages.
342
- * When true, vote buttons render below chatbot (DM Agent) messages.
343
- * Votes are persisted as Stream Chat reactions (vote_up / vote_down).
344
- * Defaults to false.
345
- */
346
- chatbotVotingEnabled?: boolean;
347
340
  /**
348
341
  * Language code used to pick translated message text from Stream Chat i18n.
349
342
  * Falls back to message.text when no matching translation exists.
@@ -1420,14 +1413,6 @@ export declare interface MessageMetadata {
1420
1413
  broadcast_name?: string;
1421
1414
  }
1422
1415
 
1423
- export declare const MessageVoteButtons: default_2.FC<MessageVoteButtonsProps>;
1424
-
1425
- declare interface MessageVoteButtonsProps {
1426
- selected: VoteSelection;
1427
- onVoteUp: () => void;
1428
- onVoteDown: () => void;
1429
- }
1430
-
1431
1416
  declare type MessageWithI18n = Pick<LocalMessage, 'text'> & {
1432
1417
  i18n?: Record<string, string> | null;
1433
1418
  };
@@ -1820,22 +1805,6 @@ export declare const useComposerLocked: () => boolean;
1820
1805
 
1821
1806
  export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K];
1822
1807
 
1823
- /**
1824
- * Hook that wraps Stream Chat reactions to provide toggle-style
1825
- * upvote/downvote behavior on a message.
1826
- *
1827
- * Uses enforce_unique so sending a new reaction type automatically
1828
- * removes the previous one. Uses skip_push to avoid notifying the
1829
- * Linker on every vote.
1830
- */
1831
- export declare function useMessageVote(message: LocalMessage): UseMessageVoteResult;
1832
-
1833
- declare interface UseMessageVoteResult {
1834
- selected: VoteSelection;
1835
- voteUp: () => void;
1836
- voteDown: () => void;
1837
- }
1838
-
1839
1808
  /**
1840
1809
  * Hook to access messaging service and state
1841
1810
  */
@@ -1878,8 +1847,6 @@ declare interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps
1878
1847
  onClick?: (index: number) => boolean | void;
1879
1848
  }
1880
1849
 
1881
- export declare type VoteSelection = 'up' | 'down' | null;
1882
-
1883
1850
  export { }
1884
1851
 
1885
1852
 
package/dist/index.js CHANGED
@@ -1,39 +1,37 @@
1
- import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as h, k as L, l as d, m as p, R as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, x as y, o as z, y as B, z as S, B as q, D, E, G as R, H as V, I as w } from "./index-BOBIiAuD.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as l, L as g, h as c, M, i as h, j as L, k as u, l as d, R as p, m as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, o as y, x as z, y as S, z as q, B, D, E, G as R } from "./index-fk5ihkQY.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
5
5
  i as ChannelEmptyState,
6
- o as ChannelList,
7
- n as ChannelView,
6
+ n as ChannelList,
7
+ o as ChannelView,
8
8
  m as CustomMessageProvider,
9
9
  r as FaqList,
10
- g as FaqListItem,
11
- l as LinkAttachment,
12
- M as LockedAttachment,
13
- c as MediaMessage,
14
- u as MessageAttachment,
15
- h as MessageBubble,
16
- L as MessageVoteButtons,
17
- d as MessagingProvider,
18
- p as MessagingShell,
19
- C as RichCard,
20
- v as buildCompactMetaLabel,
21
- A as formatFileSize,
22
- k as formatRelativeTime,
23
- F as getFileExtensionLabel,
24
- b as getMessageDisplayText,
25
- P as isLinkAttachment,
26
- f as isUuidLike,
27
- x as messageAttachmentGroupPositionFromStream,
28
- y as normalizeLanguageCode,
29
- z as optimizeMessagingAttachmentUrl,
30
- B as resolveConversationParticipant,
10
+ l as FaqListItem,
11
+ g as LinkAttachment,
12
+ c as LockedAttachment,
13
+ M as MediaMessage,
14
+ h as MessageAttachment,
15
+ L as MessageBubble,
16
+ u as MessagingProvider,
17
+ d as MessagingShell,
18
+ p as RichCard,
19
+ C as buildCompactMetaLabel,
20
+ v as formatFileSize,
21
+ A as formatRelativeTime,
22
+ k as getFileExtensionLabel,
23
+ F as getMessageDisplayText,
24
+ b as isLinkAttachment,
25
+ P as isUuidLike,
26
+ f as messageAttachmentGroupPositionFromStream,
27
+ x as normalizeLanguageCode,
28
+ y as optimizeMessagingAttachmentUrl,
29
+ z as resolveConversationParticipant,
31
30
  S as resolveLinkAttachment,
32
31
  q as resolveMediaFromMessage,
33
- D as resolveParticipantDisplayName,
34
- E as useComposerLocked,
35
- R as useCustomMessage,
36
- V as useMessageVote,
37
- w as useMessaging
32
+ B as resolveParticipantDisplayName,
33
+ D as useComposerLocked,
34
+ E as useCustomMessage,
35
+ R as useMessaging
38
36
  };
39
37
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.37.1",
3
+ "version": "3.38.0-rc-1786342246",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -46,10 +46,11 @@
46
46
  "storybook": "storybook dev -p 6006",
47
47
  "storybook:build": "storybook build --stats-json",
48
48
  "chromatic:ci:feature": "chromatic --ci --exit-zero-on-changes --exit-once-uploaded",
49
- "chromatic:ci:main": "chromatic --ci --auto-accept-changes"
49
+ "chromatic:ci:main": "chromatic --ci --auto-accept-changes",
50
+ "chromatic:local": "yarn storybook:build && chromatic"
50
51
  },
51
52
  "dependencies": {
52
- "@linktr.ee/messaging-core": "^2.4.0",
53
+ "@linktr.ee/messaging-core": "2.4.0-rc-1786342246",
53
54
  "@linktr.ee/messaging-taxonomy": "^0.9.0",
54
55
  "@phosphor-icons/react": "^2.1.10"
55
56
  },
@@ -49,7 +49,6 @@ const ChannelViewInner: React.FC<{
49
49
  composerDisabled?: boolean
50
50
  composerDisabledReason?: string
51
51
  showStarButton?: boolean
52
- chatbotVotingEnabled?: boolean
53
52
  renderChannelBanner?: () => React.ReactNode
54
53
  customChannelActions?: React.ReactNode
55
54
  renderChannelActions?: React.ReactNode
@@ -80,7 +79,6 @@ const ChannelViewInner: React.FC<{
80
79
  composerDisabled = false,
81
80
  composerDisabledReason,
82
81
  showStarButton = false,
83
- chatbotVotingEnabled = false,
84
82
  renderChannelBanner,
85
83
  customChannelActions,
86
84
  renderChannelActions,
@@ -149,7 +147,6 @@ const ChannelViewInner: React.FC<{
149
147
  const messageNode = (
150
148
  <CustomMessage
151
149
  {...props}
152
- chatbotVotingEnabled={chatbotVotingEnabled}
153
150
  viewerLanguage={viewerLanguage}
154
151
  onBroadcastClick={messageBroadcastClick}
155
152
  />
@@ -161,7 +158,7 @@ const ChannelViewInner: React.FC<{
161
158
 
162
159
  return renderMessage(messageNode, message)
163
160
  },
164
- [chatbotVotingEnabled, messageBroadcastClick, renderMessage, viewerLanguage]
161
+ [messageBroadcastClick, renderMessage, viewerLanguage]
165
162
  )
166
163
 
167
164
  return (
@@ -308,7 +305,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
308
305
  messageMetadata,
309
306
  onMessageSent,
310
307
  showStarButton = false,
311
- chatbotVotingEnabled = false,
312
308
  renderChannelBanner,
313
309
  customChannelActions,
314
310
  renderChannelActions,
@@ -426,7 +422,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
426
422
  composerDisabled={composerDisabled}
427
423
  composerDisabledReason={composerDisabledReason}
428
424
  showStarButton={showStarButton}
429
- chatbotVotingEnabled={chatbotVotingEnabled}
430
425
  renderChannelBanner={renderChannelBanner}
431
426
  customChannelActions={customChannelActions}
432
427
  renderChannelActions={renderChannelActions}
@@ -43,14 +43,6 @@ vi.mock('stream-chat-react', () => ({
43
43
  useMessageReminder: () => undefined,
44
44
  }))
45
45
 
46
- vi.mock('../../hooks/useMessageVote', () => ({
47
- useMessageVote: () => ({
48
- selected: undefined,
49
- voteUp: vi.fn(),
50
- voteDown: vi.fn(),
51
- }),
52
- }))
53
-
54
46
  vi.mock('./context', () => ({
55
47
  useCustomMessage: () => ({
56
48
  isUnlocking: () => false,
@@ -70,10 +62,6 @@ vi.mock('./MessageTag', () => ({
70
62
  isTipOnlyMessage: () => false,
71
63
  }))
72
64
 
73
- vi.mock('./MessageVoteButtons', () => ({
74
- MessageVoteButtons: () => null,
75
- }))
76
-
77
65
  vi.mock('./LockedAttachment', () => ({
78
66
  default: () => null,
79
67
  }))
@@ -35,13 +35,10 @@ describe('MessageTag', () => {
35
35
 
36
36
  const indicator = screen.getByTestId('message-chatbot-indicator')
37
37
  expect(indicator).toHaveTextContent('Sent by AI Assistant')
38
- expect(indicator).toHaveClass('message-chatbot-indicator--receiver')
39
- expect(indicator).toHaveClass('message-chatbot-indicator--text')
40
38
 
41
- expect(indicator.children[0]).toHaveClass('message-chatbot-indicator__icon')
42
- expect(indicator.children[1]).toHaveClass(
43
- 'message-chatbot-indicator__label'
44
- )
39
+ // icon first, then label
40
+ expect(indicator.children[0].querySelector('svg')).not.toBeNull()
41
+ expect(indicator.children[1]).toHaveTextContent('Sent by AI Assistant')
45
42
  })
46
43
 
47
44
  it('renders sender chatbot text variant with mirrored order', () => {
@@ -56,13 +53,10 @@ describe('MessageTag', () => {
56
53
 
57
54
  const indicator = screen.getByTestId('message-chatbot-indicator')
58
55
  expect(indicator).toHaveTextContent('Sent by AI Assistant')
59
- expect(indicator).toHaveClass('message-chatbot-indicator--sender')
60
- expect(indicator).toHaveClass('message-chatbot-indicator--text')
61
56
 
62
- expect(indicator.children[0]).toHaveClass(
63
- 'message-chatbot-indicator__label'
64
- )
65
- expect(indicator.children[1]).toHaveClass('message-chatbot-indicator__icon')
57
+ // label first, then icon (mirrored)
58
+ expect(indicator.children[0]).toHaveTextContent('Sent by AI Assistant')
59
+ expect(indicator.children[1].querySelector('svg')).not.toBeNull()
66
60
  })
67
61
 
68
62
  it('renders sender attachment chatbot variant outside-footer copy', () => {
@@ -78,13 +72,10 @@ describe('MessageTag', () => {
78
72
 
79
73
  const indicator = screen.getByTestId('message-chatbot-indicator')
80
74
  expect(indicator).toHaveTextContent('Sent with AI')
81
- expect(indicator).toHaveClass('message-chatbot-indicator--sender')
82
- expect(indicator).toHaveClass('message-chatbot-indicator--attachment')
83
75
 
84
- expect(indicator.children[0]).toHaveClass('message-chatbot-indicator__icon')
85
- expect(indicator.children[1]).toHaveClass(
86
- 'message-chatbot-indicator__label'
87
- )
76
+ // icon first, then label
77
+ expect(indicator.children[0].querySelector('svg')).not.toBeNull()
78
+ expect(indicator.children[1]).toHaveTextContent('Sent with AI')
88
79
  })
89
80
 
90
81
  it('keeps tip tag behavior unchanged', () => {
@@ -90,8 +90,8 @@ export const MessageTag = ({
90
90
  if (!amountText) return null
91
91
 
92
92
  const className = standalone
93
- ? 'message-tip-standalone'
94
- : 'message-tag message-tag--tip'
93
+ ? 'message-tip-standalone inline-flex items-center gap-1.5 px-3.5 py-2.5 rounded-[18px] text-[14px] font-medium text-[#016e1a] bg-[#dbf0e0]'
94
+ : 'message-tag--tip inline-flex items-center gap-1 text-[12px] leading-[2] font-medium text-[#016630]'
95
95
 
96
96
  const amountLabel = isPaidMessage(message) ? 'message' : 'tip'
97
97
  const label = standalone
@@ -111,28 +111,27 @@ export const MessageTag = ({
111
111
  ? 'Sent with AI'
112
112
  : 'Sent by AI Assistant'
113
113
 
114
- const chatbotClassName = [
115
- 'message-chatbot-indicator',
116
- isMyMessage
117
- ? 'message-chatbot-indicator--sender'
118
- : 'message-chatbot-indicator--receiver',
119
- isSenderAttachmentVariant
120
- ? 'message-chatbot-indicator--attachment'
121
- : 'message-chatbot-indicator--text',
122
- ].join(' ')
123
-
124
- const label = (
125
- <span className="message-chatbot-indicator__label">{chatbotLabel}</span>
126
- )
114
+ const chatbotBase =
115
+ 'inline-flex items-center text-[10px] font-normal leading-4 tracking-[0.2px]'
116
+ const chatbotVariant = isSenderAttachmentVariant
117
+ ? 'justify-end gap-2 text-black/30'
118
+ : isMyMessage
119
+ ? 'w-full justify-start gap-1.5 mt-3 px-4 text-white/55'
120
+ : 'w-full justify-start gap-1.5 mt-3 px-4 text-black/30'
121
+
122
+ const label = <span className="text-left">{chatbotLabel}</span>
127
123
  const icon = (
128
- <span className="message-chatbot-indicator__icon">
124
+ <span className="inline-flex items-center leading-none">
129
125
  <SparkleIcon size={isSenderAttachmentVariant ? 12 : 15} />
130
126
  </span>
131
127
  )
132
128
 
133
129
  // Chatbot indicator variant
134
130
  return (
135
- <div className={chatbotClassName} data-testid="message-chatbot-indicator">
131
+ <div
132
+ className={`${chatbotBase} ${chatbotVariant}`}
133
+ data-testid="message-chatbot-indicator"
134
+ >
136
135
  {isMyMessage && !isSenderAttachmentVariant ? (
137
136
  <>
138
137
  {label}
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+
3
+ /**
4
+ * In-channel chat-bubble tail (20×23), rendered as a real element at the last
5
+ * bubble's bottom corner. Painted behind the bubble body (`z-index: -1`), so the
6
+ * host bubble must be `relative` with `isolation: isolate`.
7
+ *
8
+ * The silhouette is the same 20×23 mask path shared by the card tails
9
+ * (`.messaging-link-card-tail` in styles.css) and the standalone `MessageBubble`
10
+ * preview — one source of truth for the shape. It stays a CSS `mask` because the
11
+ * fill comes from the bubble-surface colour (so a plain `background-color` shows
12
+ * through the shape), and the data-URI path isn't expressible in Tailwind.
13
+ */
14
+ const TAIL_MASK =
15
+ "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='23' fill='none'%3E%3Cpath fill='black' d='M0 0H15C15 10.1934 15.859 15.2345 19.168 21.4893C19.527 22.168 19.039 22.9917 18.274 22.9178C5.176 21.6506 0.32 6.0737 0 0Z'/%3E%3C/svg%3E\") no-repeat center / 100% 100%"
16
+
17
+ /**
18
+ * Bubble surface colours, reused from the toolkit's Stream theme vars so the tail
19
+ * is tinted the same as its bubble. Those vars live on `.str-chat`; the `var()`
20
+ * fallbacks keep the tail correct if it ever renders outside a channel.
21
+ */
22
+ const BG_BY_SIDE = {
23
+ me: 'var(--str-chat__own-message-bubble-background-color, #1e2330)',
24
+ other: 'var(--str-chat__message-bubble-background-color, #f1f0ee)',
25
+ } as const
26
+
27
+ /**
28
+ * `me` tails hang off the right edge; `other` tails mirror to the left with
29
+ * `scaleX(-1)`, matching the in-channel convention.
30
+ */
31
+ export const MessageTail = ({ side }: { side: 'me' | 'other' }) => (
32
+ <span
33
+ aria-hidden="true"
34
+ className="pointer-events-none absolute bottom-0 z-[-1] h-[1.4375rem] w-5"
35
+ style={{
36
+ insetInlineEnd: side === 'me' ? 'calc(-1 * 0.3125rem)' : undefined,
37
+ insetInlineStart: side === 'other' ? 'calc(-1 * 0.3125rem)' : undefined,
38
+ transform: side === 'other' ? 'scaleX(-1)' : undefined,
39
+ transformOrigin: 'center',
40
+ backgroundColor: BG_BY_SIDE[side],
41
+ WebkitMask: TAIL_MASK,
42
+ mask: TAIL_MASK,
43
+ }}
44
+ />
45
+ )
@@ -4,6 +4,7 @@ import {
4
4
  MegaphoneIcon,
5
5
  WarningCircleIcon,
6
6
  } from '@phosphor-icons/react'
7
+ import classNames from 'classnames'
7
8
  import {
8
9
  MessageStatus,
9
10
  useChannelStateContext,
@@ -107,13 +108,19 @@ const BroadcastStatus = ({
107
108
  }) => {
108
109
  const campaignName = resolvedBroadcastName || broadcastName
109
110
  const name = campaignName || 'Broadcast'
111
+ // Underline the name only in the clickable variant (it reads as a link).
112
+ // The name's colour still comes from the `.str-chat`-scoped grid rule in
113
+ // styles.css; we only add the underline here.
114
+ const isLink = Boolean(onBroadcastClick)
110
115
  const content = (
111
116
  <>
112
117
  <span className="sent-message-broadcast-badge" aria-hidden="true">
113
118
  <MegaphoneIcon weight="fill" />
114
119
  </span>
115
120
  <span
116
- className="sent-message-broadcast-name"
121
+ className={classNames('sent-message-broadcast-name', {
122
+ 'underline [text-decoration-skip-ink:auto]': isLink,
123
+ })}
117
124
  data-testid="sent-message-broadcast-status"
118
125
  >
119
126
  {name}
@@ -124,7 +131,7 @@ const BroadcastStatus = ({
124
131
  return onBroadcastClick ? (
125
132
  <button
126
133
  type="button"
127
- className="sent-message-broadcast-link"
134
+ className="inline-flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-inherit [font:inherit]"
128
135
  aria-label={`View broadcast ${name}`}
129
136
  onClick={() =>
130
137
  onBroadcastClick({
@@ -1,3 +1,4 @@
1
+ import classNames from 'classnames'
1
2
  import React from 'react'
2
3
  import type { Attachment, LocalMessage } from 'stream-chat'
3
4
  import {
@@ -12,6 +13,8 @@ import { isPlayableMedia } from '../LinkAttachment/components/_shared/CardThumbn
12
13
  import MessageAttachment from '../MessageAttachment'
13
14
  import type { BubbleGroupPosition } from '../MessageAttachment/types'
14
15
 
16
+ import { MessageTail } from './MessageTail'
17
+
15
18
  type StreamMediaKind = 'image' | 'video' | 'audio' | 'pdf' | 'file'
16
19
 
17
20
  export type AttachmentSegment =
@@ -344,12 +347,16 @@ const StreamAttachmentMessage: React.FC<StreamAttachmentMessageProps> = ({
344
347
 
345
348
  // A link card paints its own accent-coloured tail only when it is the very
346
349
  // last thing this message renders. A caption bubble follows when there is
347
- // one, and that bubble already owns the tail via `styles.css`; and a message
348
- // mid-run has its tail on a later message. Anything else is a mid-run card,
349
- // which must stay tail-less.
350
+ // one, and that bubble owns the tail (rendered below via `MessageTail`); and a
351
+ // message mid-run has its tail on a later message. Anything else is a mid-run
352
+ // card, which must stay tail-less.
350
353
  const linkTailPosition: BubbleGroupPosition = caption
351
354
  ? 'middle'
352
355
  : groupPosition
356
+ // The trailing caption bubble carries the tail on single / last-in-run
357
+ // messages, matching the in-channel text bubbles.
358
+ const captionShowsTail =
359
+ groupPosition === 'single' || groupPosition === 'end'
353
360
 
354
361
  const pushLinkRow = (
355
362
  attachment: Attachment,
@@ -422,9 +429,16 @@ const StreamAttachmentMessage: React.FC<StreamAttachmentMessageProps> = ({
422
429
  {!caption && <MessageErrorIcon />}
423
430
  </div>
424
431
  {caption ? (
425
- <div className="str-chat__message-bubble">
432
+ <div
433
+ className={classNames('str-chat__message-bubble', {
434
+ 'relative isolate': captionShowsTail,
435
+ })}
436
+ >
426
437
  <MessageText message={message} renderText={renderText} />
427
438
  <MessageErrorIcon />
439
+ {captionShowsTail && (
440
+ <MessageTail side={isMyMessage ? 'me' : 'other'} />
441
+ )}
428
442
  </div>
429
443
  ) : null}
430
444
  </div>
@@ -29,7 +29,6 @@ import {
29
29
  type MessageUIComponentProps,
30
30
  } from 'stream-chat-react'
31
31
 
32
- import { useMessageVote } from '../../hooks/useMessageVote'
33
32
  import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
34
33
  import { Avatar } from '../Avatar'
35
34
  import { isLinkAttachment } from '../MediaMessage'
@@ -46,20 +45,18 @@ import {
46
45
  isTextAttachmentMessage,
47
46
  isTipOnlyMessage,
48
47
  } from './MessageTag'
49
- import { MessageVoteButtons } from './MessageVoteButtons'
48
+ import { MessageTail } from './MessageTail'
50
49
  import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
51
50
  import StreamAttachmentMessage, {
52
51
  buildOrderedAttachmentSegments,
53
52
  } from './StreamAttachmentMessage'
54
53
 
55
54
  type CustomMessageUIComponentProps = MessageUIComponentProps & {
56
- chatbotVotingEnabled?: boolean
57
55
  viewerLanguage?: string
58
56
  onBroadcastClick?: (broadcast: { id: string; name?: string }) => void
59
57
  }
60
58
 
61
59
  type CustomMessageWithContextProps = MessageContextValue & {
62
- chatbotVotingEnabled?: boolean
63
60
  viewerLanguage?: string
64
61
  onBroadcastClick?: (broadcast: { id: string; name?: string }) => void
65
62
  resolvedBroadcastName?: string
@@ -68,7 +65,6 @@ type CustomMessageWithContextProps = MessageContextValue & {
68
65
  const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
69
66
  const {
70
67
  additionalMessageInputProps,
71
- chatbotVotingEnabled,
72
68
  editing,
73
69
  endOfGroup,
74
70
  firstOfGroup,
@@ -93,7 +89,6 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
93
89
  useCustomMessage('LockedAttachment')
94
90
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
95
91
  const reminder = useMessageReminder(message.id)
96
- const { selected: voteState, voteUp, voteDown } = useMessageVote(message)
97
92
 
98
93
  const {
99
94
  Attachment = DefaultAttachment,
@@ -151,12 +146,15 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
151
146
  (total, segment) => total + segment.attachments.length,
152
147
  0
153
148
  ) === (finalAttachments?.length ?? 0)
154
- const streamAttachmentGroupPosition =
155
- messageAttachmentGroupPositionFromStream({
156
- endOfGroup,
157
- firstOfGroup,
158
- groupedByUser,
159
- })
149
+ const bubbleGroupPosition = messageAttachmentGroupPositionFromStream({
150
+ endOfGroup,
151
+ firstOfGroup,
152
+ groupedByUser,
153
+ })
154
+ // The trailing bubble of a same-author run carries the tail (matches Stream's
155
+ // `--single`/`--bottom` grouping the card tails already key off).
156
+ const showTail =
157
+ bubbleGroupPosition === 'single' || bubbleGroupPosition === 'end'
160
158
 
161
159
  if (isDateSeparatorMessage(message)) {
162
160
  return null
@@ -353,17 +351,22 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
353
351
  {!isMine && MessageActions && <MessageActions />}
354
352
  </div>
355
353
  {message.text && (
356
- <div className="str-chat__message-bubble">
354
+ <div
355
+ className={classNames('str-chat__message-bubble', {
356
+ 'relative isolate': showTail,
357
+ })}
358
+ >
357
359
  <MessageText
358
360
  message={displayMessage}
359
361
  renderText={renderText}
360
362
  />
363
+ {showTail && <MessageTail side={isMine ? 'me' : 'other'} />}
361
364
  </div>
362
365
  )}
363
366
  </div>
364
367
  ) : canRenderAttachmentsInToolkit ? (
365
368
  <StreamAttachmentMessage
366
- groupPosition={streamAttachmentGroupPosition}
369
+ groupPosition={bubbleGroupPosition}
367
370
  isMyMessage={isMine}
368
371
  message={{
369
372
  ...displayMessage,
@@ -377,7 +380,11 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
377
380
  />
378
381
  ) : (
379
382
  <div className="str-chat__message-bubble-wrapper">
380
- <div className="str-chat__message-bubble">
383
+ <div
384
+ className={classNames('str-chat__message-bubble', {
385
+ 'relative isolate': showTail,
386
+ })}
387
+ >
381
388
  {isChatbot && !useAttachmentFooterChatbotTag && (
382
389
  <MessageTag
383
390
  message={message}
@@ -404,6 +411,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
404
411
  />
405
412
  )}
406
413
  <MessageErrorIcon />
414
+ {showTail && <MessageTail side={isMine ? 'me' : 'other'} />}
407
415
  </div>
408
416
  </div>
409
417
  )}
@@ -417,13 +425,6 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
417
425
  isMyMessage={isMine}
418
426
  />
419
427
  )}
420
- {chatbotVotingEnabled && isChatbot && (
421
- <MessageVoteButtons
422
- selected={voteState}
423
- onVoteUp={voteUp}
424
- onVoteDown={voteDown}
425
- />
426
- )}
427
428
  </div>
428
429
  )}
429
430
  <SentMessageDeliveryStatus
@@ -447,7 +448,6 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
447
448
  const MemoizedCustomMessage = React.memo(
448
449
  CustomMessageWithContext,
449
450
  (prev, next) => {
450
- if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
451
451
  if (prev.viewerLanguage !== next.viewerLanguage) return false
452
452
  if (prev.onBroadcastClick !== next.onBroadcastClick) return false
453
453
  if (prev.resolvedBroadcastName !== next.resolvedBroadcastName) return false
@@ -24,12 +24,11 @@ export interface BubbleTailProps {
24
24
  * Wraps a `LinkAttachment` card in the chat bubble tail from the messaging
25
25
  * design system, painted to match the card's own bottom edge.
26
26
  *
27
- * The geometry and mask live in `styles.css` alongside the text-bubble tails
28
- * they mirror. They cannot be reused directly: those rules key off
29
- * `.str-chat__message-bubble:last-child`, which only exists when the card is
30
- * rendered inside a Stream thread — and `StreamAttachmentMessage` marks that
31
- * bubble `data-attachment-bubble` precisely to opt out of them, since a tail
32
- * tinted from the text-bubble surface would clash with a coloured card.
27
+ * The geometry and mask live in `styles.css` (`.messaging-link-card-tail`),
28
+ * mirroring the text-bubble tail (`MessageTail`) they share a silhouette with.
29
+ * A card carries its own accent-tinted tail rather than the surface-tinted
30
+ * `MessageTail`, so `StreamAttachmentMessage` renders `MessageTail` only on the
31
+ * trailing caption bubble, never on the card bubble itself.
33
32
  */
34
33
  const BubbleTail: React.FC<BubbleTailProps> = ({
35
34
  accentHex,