@linktr.ee/messaging-react 3.20.0 → 3.21.0-rc-1785406654

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/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-DSZ8Rxnl.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.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-cDF7EDso.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.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;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -82,6 +82,11 @@ export declare interface AvatarProps {
82
82
  dmAgentEnabled?: boolean;
83
83
  }
84
84
 
85
+ export declare type BroadcastClickHandler = (broadcast: {
86
+ id: string;
87
+ name?: string;
88
+ }) => void;
89
+
85
90
  /**
86
91
  * Build the meta line shown under the title in compact document / file
87
92
  * attachments — `EXT · SIZE` (`PDF · 379.5 KB`). Either part is dropped
@@ -205,7 +210,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
205
210
  * Props that MessagingShell passes through to ChannelView.
206
211
  * ChannelViewProps is the source of truth for these props.
207
212
  */
208
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo' | 'composerInput'>;
213
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onBroadcastClick' | 'showChannelInfo' | 'composerInput'>;
209
214
 
210
215
  /**
211
216
  * ChannelView component props
@@ -391,6 +396,11 @@ export declare interface ChannelViewProps {
391
396
  * onMessageLinkClick={(url) => trackChannelLinkClicked(url)}
392
397
  */
393
398
  onMessageLinkClick?: MessageLinkClickHandler;
399
+ /**
400
+ * Fired when a broadcast label is clicked on an outgoing broadcast message.
401
+ * Hosts own navigation so this shared package never assumes an admin URL.
402
+ */
403
+ onBroadcastClick?: BroadcastClickHandler;
394
404
  /**
395
405
  * Passed to Stream `Channel` as `SendButton`. Required for hosts that replace
396
406
  * the send control: `Channel` merges this into `ComponentContext` and an
@@ -1373,6 +1383,8 @@ export declare interface MessageMetadata {
1373
1383
  * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
1374
1384
  */
1375
1385
  sent_from?: 'stack_detail';
1386
+ broadcast_id?: string;
1387
+ broadcast_name?: string;
1376
1388
  }
1377
1389
 
1378
1390
  export declare const MessageVoteButtons: default_2.FC<MessageVoteButtonsProps>;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
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 L, k as h, l as p, m as d, n as v, p as C, q as A, s as k, t as F, u as b, v as P, w as f, x, o as y, y as z, z as B, B as S, D as q, E as D, G as E, H as V, I as w } from "./index-BuqmY6ae.js";
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 L, k as h, l as p, m as d, n as v, p as C, q as A, s as k, t as F, u as b, v as P, w as f, x, o as y, y as z, z as B, B as S, D as q, E as D, G as E, H as V, I as w } from "./index-D1mXghgf.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/dist/testing.d.ts CHANGED
@@ -55,6 +55,8 @@ export declare interface MessageMetadata {
55
55
  * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
56
56
  */
57
57
  sent_from?: 'stack_detail';
58
+ broadcast_id?: string;
59
+ broadcast_name?: string;
58
60
  }
59
61
 
60
62
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.20.0",
3
+ "version": "3.21.0-rc-1785406654",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@linktr.ee/component-library": "11.8.6",
53
- "@linktr.ee/messaging-core": "^2.2.0",
53
+ "@linktr.ee/messaging-core": "2.3.0-rc-1785406654",
54
54
  "@linktr.ee/messaging-taxonomy": "^0.3.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
@@ -56,6 +56,7 @@ const ChannelViewInner: React.FC<{
56
56
  viewerLanguage?: string
57
57
  showChannelInfo?: boolean
58
58
  onParticipantNameClick?: () => void
59
+ onBroadcastClick?: ChannelViewProps['onBroadcastClick']
59
60
  composerInput?: ChannelViewProps['composerInput']
60
61
  }> = ({
61
62
  onBack,
@@ -82,6 +83,7 @@ const ChannelViewInner: React.FC<{
82
83
  viewerLanguage,
83
84
  showChannelInfo = true,
84
85
  onParticipantNameClick,
86
+ onBroadcastClick,
85
87
  composerInput,
86
88
  }) => {
87
89
  const { channel } = useChannelStateContext()
@@ -125,6 +127,7 @@ const ChannelViewInner: React.FC<{
125
127
  {...props}
126
128
  chatbotVotingEnabled={chatbotVotingEnabled}
127
129
  viewerLanguage={viewerLanguage}
130
+ onBroadcastClick={onBroadcastClick}
128
131
  />
129
132
  )
130
133
 
@@ -134,7 +137,7 @@ const ChannelViewInner: React.FC<{
134
137
 
135
138
  return renderMessage(messageNode, message)
136
139
  },
137
- [chatbotVotingEnabled, renderMessage, viewerLanguage]
140
+ [chatbotVotingEnabled, onBroadcastClick, renderMessage, viewerLanguage]
138
141
  )
139
142
 
140
143
  return (
@@ -292,6 +295,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
292
295
  viewerLanguage,
293
296
  showChannelInfo = true,
294
297
  onParticipantNameClick,
298
+ onBroadcastClick,
295
299
  composerInput,
296
300
  }) => {
297
301
  // Custom send message handler that:
@@ -405,6 +409,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
405
409
  viewerLanguage={viewerLanguage}
406
410
  showChannelInfo={showChannelInfo}
407
411
  onParticipantNameClick={onParticipantNameClick}
412
+ onBroadcastClick={onBroadcastClick}
408
413
  composerInput={composerInput}
409
414
  />
410
415
  </Channel>
@@ -120,6 +120,7 @@ interface TemplateProps {
120
120
  | 'MESSAGE_PAID'
121
121
  | 'MESSAGE_CHATBOT'
122
122
  | 'MESSAGE_ATTACHMENT'
123
+ | 'MESSAGE_OUTBOUND'
123
124
  payment_status?: 'pending' | 'paid' | 'failed' | 'refunded'
124
125
  amount_text?: string
125
126
  attachment_title?: string
@@ -127,6 +128,8 @@ interface TemplateProps {
127
128
  attachment_thumbnail?: string
128
129
  attachment_detail?: string
129
130
  attachment_content_type?: 'text' | 'media'
131
+ broadcast_id?: string
132
+ broadcast_name?: string
130
133
  }
131
134
  }>
132
135
  }
@@ -257,6 +260,43 @@ DeliveredStatus.args = {
257
260
  ],
258
261
  }
259
262
 
263
+ export const BroadcastStatus: StoryFn<TemplateProps> = Template.bind({})
264
+ BroadcastStatus.args = {
265
+ messages: [
266
+ {
267
+ id: 'msg-1',
268
+ text: 'July class details',
269
+ user: storyUsers.creator,
270
+ metadata: {
271
+ custom_type: 'MESSAGE_OUTBOUND',
272
+ broadcast_id: 'broadcast-1',
273
+ broadcast_name: 'July 9th Class attendees',
274
+ },
275
+ },
276
+ ],
277
+ }
278
+
279
+ export const BroadcastStatusNonLatest: StoryFn<TemplateProps> = Template.bind({})
280
+ BroadcastStatusNonLatest.args = {
281
+ messages: [
282
+ {
283
+ id: 'msg-1',
284
+ text: 'July class details',
285
+ user: storyUsers.creator,
286
+ metadata: {
287
+ custom_type: 'MESSAGE_OUTBOUND',
288
+ broadcast_id: 'broadcast-1',
289
+ broadcast_name: 'July 9th Class attendees',
290
+ },
291
+ },
292
+ {
293
+ id: 'msg-2',
294
+ text: 'Let me know if you have any questions.',
295
+ user: storyUsers.creator,
296
+ },
297
+ ],
298
+ }
299
+
260
300
  export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})
261
301
  WithTipTag.args = {
262
302
  messages: [
@@ -0,0 +1,141 @@
1
+ import React from 'react'
2
+ import { useChannelStateContext, useMessageContext } from 'stream-chat-react'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { renderWithProviders, screen } from '../../test/utils'
6
+
7
+ import { CustomMessage } from './index'
8
+
9
+ vi.mock('stream-chat-react', () => ({
10
+ Attachment: () => null,
11
+ EditMessageModal: () => null,
12
+ MessageBounceModal: () => null,
13
+ MessageBouncePrompt: () => null,
14
+ MessageBlocked: () => null,
15
+ MessageDeleted: () => null,
16
+ MessageErrorIcon: () => null,
17
+ MessageIsThreadReplyInChannelButtonIndicator: () => null,
18
+ MessageRepliesCountButton: () => null,
19
+ MessageStatus: ({
20
+ MessageSentStatus,
21
+ }: {
22
+ MessageSentStatus: React.ComponentType
23
+ }) => <MessageSentStatus />,
24
+ MessageText: () => <span>Message</span>,
25
+ Poll: () => null,
26
+ ReminderNotification: () => null,
27
+ StreamedMessageText: () => null,
28
+ areMessageUIPropsEqual: () => true,
29
+ isDateSeparatorMessage: () => false,
30
+ isMessageBlocked: () => false,
31
+ isMessageBounced: () => false,
32
+ messageHasAttachments: () => false,
33
+ messageHasReactions: () => false,
34
+ useChannelStateContext: vi.fn(),
35
+ useChatContext: () => ({ client: { polls: { fromState: vi.fn() } } }),
36
+ useComponentContext: () => ({
37
+ Attachment: () => null,
38
+ MessageRepliesCountButton: () => null,
39
+ }),
40
+ useMessageContext: vi.fn(),
41
+ useMessageReminder: () => undefined,
42
+ }))
43
+
44
+ vi.mock('../../hooks/useMessageVote', () => ({
45
+ useMessageVote: () => ({
46
+ selected: undefined,
47
+ voteUp: vi.fn(),
48
+ voteDown: vi.fn(),
49
+ }),
50
+ }))
51
+
52
+ vi.mock('./context', () => ({
53
+ useCustomMessage: () => ({
54
+ isUnlocking: () => false,
55
+ }),
56
+ }))
57
+
58
+ vi.mock('../Avatar', () => ({
59
+ Avatar: () => null,
60
+ }))
61
+
62
+ vi.mock('./MessageTag', () => ({
63
+ MessageTag: () => null,
64
+ isAttachmentMessage: () => false,
65
+ isChatbotMessage: () => false,
66
+ isMediaAttachmentMessage: () => false,
67
+ isTextAttachmentMessage: () => false,
68
+ isTipOnlyMessage: () => false,
69
+ }))
70
+
71
+ vi.mock('./MessageVoteButtons', () => ({
72
+ MessageVoteButtons: () => null,
73
+ }))
74
+
75
+ vi.mock('./LockedAttachment', () => ({
76
+ default: () => null,
77
+ }))
78
+
79
+ vi.mock('./StreamAttachmentMessage', () => ({
80
+ default: () => null,
81
+ buildOrderedAttachmentSegments: () => [],
82
+ }))
83
+
84
+ vi.mock('../MediaMessage', () => ({
85
+ isLinkAttachment: () => false,
86
+ }))
87
+
88
+ const mockedUseChannelStateContext = vi.mocked(useChannelStateContext)
89
+ const mockedUseMessageContext = vi.mocked(useMessageContext)
90
+
91
+ describe('CustomMessage', () => {
92
+ beforeEach(() => {
93
+ mockedUseChannelStateContext.mockReturnValue({
94
+ messages: [{ id: 'message-1', user: { id: 'me' } }],
95
+ } as never)
96
+ mockedUseMessageContext.mockReturnValue({
97
+ message: {
98
+ id: 'message-1',
99
+ text: 'Hello',
100
+ type: 'regular',
101
+ status: 'received',
102
+ user: { id: 'me', name: 'Me' },
103
+ metadata: {
104
+ broadcast_id: 'broadcast-1',
105
+ broadcast_name: 'July 9th Class attendees',
106
+ },
107
+ },
108
+ isMyMessage: () => true,
109
+ threadList: false,
110
+ editing: false,
111
+ endOfGroup: true,
112
+ firstOfGroup: true,
113
+ groupedByUser: true,
114
+ handleAction: vi.fn(),
115
+ handleOpenThread: vi.fn(),
116
+ handleRetry: vi.fn(),
117
+ highlighted: false,
118
+ renderText: undefined,
119
+ isMessageAIGenerated: undefined,
120
+ } as never)
121
+ })
122
+
123
+ it('uses the latest broadcast click handler after rerendering', () => {
124
+ const firstHandler = vi.fn()
125
+ const secondHandler = vi.fn()
126
+ const { rerender } = renderWithProviders(
127
+ <CustomMessage onBroadcastClick={firstHandler} />
128
+ )
129
+
130
+ rerender(<CustomMessage onBroadcastClick={secondHandler} />)
131
+ screen.getByRole('button', {
132
+ name: 'View broadcast July 9th Class attendees',
133
+ }).click()
134
+
135
+ expect(firstHandler).not.toHaveBeenCalled()
136
+ expect(secondHandler).toHaveBeenCalledWith({
137
+ id: 'broadcast-1',
138
+ name: 'July 9th Class attendees',
139
+ })
140
+ })
141
+ })
@@ -1,8 +1,5 @@
1
1
  import React from 'react'
2
- import {
3
- useChannelStateContext,
4
- useMessageContext,
5
- } from 'stream-chat-react'
2
+ import { useChannelStateContext, useMessageContext } from 'stream-chat-react'
6
3
  import { beforeEach, describe, expect, it, vi } from 'vitest'
7
4
 
8
5
  import { renderWithProviders, screen, within } from '../../test/utils'
@@ -69,6 +66,7 @@ describe('SentMessageDeliveryStatus', () => {
69
66
  // avatars.
70
67
  mockedUseMessageContext.mockReturnValue({
71
68
  message: { id: 'own-2' },
69
+ isMyMessage: () => true,
72
70
  } as never)
73
71
 
74
72
  renderWithProviders(<SentMessageDeliveryStatus />)
@@ -83,6 +81,7 @@ describe('SentMessageDeliveryStatus', () => {
83
81
  it('renders nothing for an earlier own message once a later message exists', () => {
84
82
  mockedUseMessageContext.mockReturnValue({
85
83
  message: { id: 'own-1' },
84
+ isMyMessage: () => true,
86
85
  } as never)
87
86
 
88
87
  renderWithProviders(<SentMessageDeliveryStatus />)
@@ -104,6 +103,7 @@ describe('SentMessageDeliveryStatus', () => {
104
103
  } as never)
105
104
  mockedUseMessageContext.mockReturnValue({
106
105
  message: { id: 'own-2' },
106
+ isMyMessage: () => true,
107
107
  } as never)
108
108
 
109
109
  renderWithProviders(<SentMessageDeliveryStatus />)
@@ -124,6 +124,7 @@ describe('SentMessageDeliveryStatus', () => {
124
124
  } as never)
125
125
  mockedUseMessageContext.mockReturnValue({
126
126
  message: { id: 'own-2' },
127
+ isMyMessage: () => true,
127
128
  } as never)
128
129
 
129
130
  renderWithProviders(<SentMessageDeliveryStatus />)
@@ -138,6 +139,7 @@ describe('SentMessageDeliveryStatus', () => {
138
139
  it('renders "Sending" in the sending slot for the latest message', () => {
139
140
  mockedUseMessageContext.mockReturnValue({
140
141
  message: { id: 'own-2', status: 'sending' },
142
+ isMyMessage: () => true,
141
143
  threadList: false,
142
144
  } as never)
143
145
 
@@ -149,6 +151,7 @@ describe('SentMessageDeliveryStatus', () => {
149
151
  it('does not render "Sending" for an earlier message', () => {
150
152
  mockedUseMessageContext.mockReturnValue({
151
153
  message: { id: 'own-1', status: 'sending' },
154
+ isMyMessage: () => true,
152
155
  threadList: false,
153
156
  } as never)
154
157
 
@@ -162,6 +165,7 @@ describe('SentMessageDeliveryStatus', () => {
162
165
  // threadList is true; we suppress the stamp ourselves.
163
166
  mockedUseMessageContext.mockReturnValue({
164
167
  message: { id: 'own-2', status: 'sending' },
168
+ isMyMessage: () => true,
165
169
  threadList: true,
166
170
  } as never)
167
171
 
@@ -173,6 +177,74 @@ describe('SentMessageDeliveryStatus', () => {
173
177
  ).not.toBeInTheDocument()
174
178
  })
175
179
 
180
+ it('renders a broadcast label for every own broadcast message, including non-latest messages', () => {
181
+ mockedUseMessageContext.mockReturnValue({
182
+ message: {
183
+ id: 'own-1',
184
+ metadata: {
185
+ broadcast_id: 'broadcast-1',
186
+ broadcast_name: 'July 9th Class attendees',
187
+ },
188
+ },
189
+ isMyMessage: () => true,
190
+ threadList: false,
191
+ } as never)
192
+
193
+ renderWithProviders(<SentMessageDeliveryStatus />)
194
+
195
+ expect(
196
+ screen.getByTestId('sent-message-broadcast-status')
197
+ ).toHaveTextContent('July 9th Class attendees')
198
+ expect(
199
+ screen.queryByTestId('sent-message-delivery-status')
200
+ ).not.toBeInTheDocument()
201
+ expect(screen.queryByText('•')).not.toBeInTheDocument()
202
+ })
203
+
204
+ it('uses a generic label and invokes the broadcast callback when the name is missing', () => {
205
+ const onBroadcastClick = vi.fn()
206
+ mockedUseMessageContext.mockReturnValue({
207
+ message: { id: 'own-2', metadata: { broadcast_id: 'broadcast-1' } },
208
+ isMyMessage: () => true,
209
+ threadList: false,
210
+ } as never)
211
+
212
+ renderWithProviders(
213
+ <SentMessageDeliveryStatus onBroadcastClick={onBroadcastClick} />
214
+ )
215
+
216
+ expect(
217
+ screen.getByTestId('sent-message-broadcast-status')
218
+ ).toHaveTextContent('Broadcast')
219
+ expect(
220
+ screen.getByRole('button', { name: 'View broadcast Broadcast' })
221
+ ).toBeInTheDocument()
222
+ screen.getByRole('button', { name: 'View broadcast Broadcast' }).click()
223
+ expect(onBroadcastClick).toHaveBeenCalledWith({ id: 'broadcast-1' })
224
+ })
225
+
226
+ it('does not render a broadcast label for a visitor viewing a broadcast message', () => {
227
+ mockedUseMessageContext.mockReturnValue({
228
+ message: {
229
+ id: 'broadcast-1',
230
+ metadata: {
231
+ broadcast_id: 'broadcast-1',
232
+ broadcast_name: 'July 9th Class attendees',
233
+ },
234
+ },
235
+ isMyMessage: () => false,
236
+ threadList: false,
237
+ } as never)
238
+
239
+ renderWithProviders(
240
+ <SentMessageDeliveryStatus onBroadcastClick={vi.fn()} />
241
+ )
242
+
243
+ expect(
244
+ screen.queryByTestId('sent-message-broadcast-status')
245
+ ).not.toBeInTheDocument()
246
+ })
247
+
176
248
  it('renders "Failed to send" for every own failed message', () => {
177
249
  mockedUseMessageContext.mockReturnValue({
178
250
  message: { id: 'own-1', status: 'failed', type: 'regular' },
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ChecksIcon,
3
3
  CircleNotchIcon,
4
+ MegaphoneIcon,
4
5
  WarningCircleIcon,
5
6
  } from '@phosphor-icons/react'
6
7
  import {
@@ -93,6 +94,44 @@ const SendingStatus = () => {
93
94
  )
94
95
  }
95
96
 
97
+ const BroadcastStatus = ({
98
+ broadcastId,
99
+ broadcastName,
100
+ onBroadcastClick,
101
+ }: {
102
+ broadcastId: string
103
+ broadcastName?: string
104
+ onBroadcastClick?: (broadcast: { id: string; name?: string }) => void
105
+ }) => {
106
+ const name = broadcastName || 'Broadcast'
107
+ const isLatestRealMessage = useIsLatestRealMessage()
108
+ const content = (
109
+ <>
110
+ {isLatestRealMessage && <span aria-hidden="true">•</span>}
111
+ <MegaphoneIcon weight="bold" aria-hidden="true" />
112
+ <span data-testid="sent-message-broadcast-status">{name}</span>
113
+ </>
114
+ )
115
+
116
+ return onBroadcastClick ? (
117
+ <button
118
+ type="button"
119
+ className="sent-message-broadcast-link"
120
+ aria-label={`View broadcast ${name}`}
121
+ onClick={() =>
122
+ onBroadcastClick({
123
+ id: broadcastId,
124
+ ...(broadcastName ? { name: broadcastName } : {}),
125
+ })
126
+ }
127
+ >
128
+ {content}
129
+ </button>
130
+ ) : (
131
+ content
132
+ )
133
+ }
134
+
96
135
  const FailedStatus = () => (
97
136
  <span className="str-chat__message-status sent-message-status--failed">
98
137
  <WarningCircleIcon weight="bold" aria-hidden="true" />
@@ -114,10 +153,15 @@ const FailedStatus = () => (
114
153
  * because it has no failed slot and are shown on every own, non-thread
115
154
  * failed message.
116
155
  */
117
- export const SentMessageDeliveryStatus = () => {
156
+ export const SentMessageDeliveryStatus = ({
157
+ onBroadcastClick,
158
+ }: {
159
+ onBroadcastClick?: (broadcast: { id: string; name?: string }) => void
160
+ }) => {
118
161
  const { isMyMessage, message, threadList } = useMessageContext(
119
162
  'SentMessageDeliveryStatus'
120
163
  )
164
+ const isOwnMessage = isMyMessage()
121
165
 
122
166
  // Stream's MessageStatus still mounts MessageSendingStatus when
123
167
  // threadList is true; keep all of our stamps out of thread replies.
@@ -125,15 +169,28 @@ export const SentMessageDeliveryStatus = () => {
125
169
  return null
126
170
  }
127
171
 
128
- if (
129
- message.status === 'failed' &&
130
- isMyMessage() &&
131
- message.type !== 'error'
132
- ) {
172
+ if (message.status === 'failed' && isOwnMessage && message.type !== 'error') {
133
173
  return <FailedStatus />
134
174
  }
135
175
 
136
- return (
176
+ if (!isOwnMessage) {
177
+ return null
178
+ }
179
+
180
+ const broadcastId = message.metadata?.broadcast_id
181
+ const broadcastName = message.metadata?.broadcast_name
182
+ const broadcastStatus =
183
+ message.status !== 'sending' &&
184
+ message.status !== 'failed' &&
185
+ broadcastId ? (
186
+ <BroadcastStatus
187
+ broadcastId={broadcastId}
188
+ broadcastName={broadcastName}
189
+ onBroadcastClick={onBroadcastClick}
190
+ />
191
+ ) : null
192
+
193
+ const messageStatus = (
137
194
  <MessageStatus
138
195
  MessageSentStatus={DeliveredStatus}
139
196
  MessageDeliveredStatus={DeliveredStatus}
@@ -141,4 +198,15 @@ export const SentMessageDeliveryStatus = () => {
141
198
  MessageSendingStatus={SendingStatus}
142
199
  />
143
200
  )
201
+
202
+ if (!broadcastStatus) {
203
+ return messageStatus
204
+ }
205
+
206
+ return (
207
+ <span className="sent-message-status-row">
208
+ {messageStatus}
209
+ {broadcastStatus}
210
+ </span>
211
+ )
144
212
  }
@@ -54,11 +54,13 @@ import StreamAttachmentMessage, {
54
54
  type CustomMessageUIComponentProps = MessageUIComponentProps & {
55
55
  chatbotVotingEnabled?: boolean
56
56
  viewerLanguage?: string
57
+ onBroadcastClick?: (broadcast: { id: string; name?: string }) => void
57
58
  }
58
59
 
59
60
  type CustomMessageWithContextProps = MessageContextValue & {
60
61
  chatbotVotingEnabled?: boolean
61
62
  viewerLanguage?: string
63
+ onBroadcastClick?: (broadcast: { id: string; name?: string }) => void
62
64
  }
63
65
 
64
66
  const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
@@ -79,6 +81,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
79
81
  renderText,
80
82
  threadList,
81
83
  viewerLanguage,
84
+ onBroadcastClick,
82
85
  } = props
83
86
 
84
87
  const { client } = useChatContext('CustomMessage')
@@ -423,7 +426,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
423
426
  )}
424
427
  </div>
425
428
  )}
426
- <SentMessageDeliveryStatus />
429
+ <SentMessageDeliveryStatus onBroadcastClick={onBroadcastClick} />
427
430
  {showReplyCountButton && (
428
431
  <MessageRepliesCountButton
429
432
  onClick={handleOpenThread}
@@ -443,6 +446,7 @@ const MemoizedCustomMessage = React.memo(
443
446
  (prev, next) => {
444
447
  if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
445
448
  if (prev.viewerLanguage !== next.viewerLanguage) return false
449
+ if (prev.onBroadcastClick !== next.onBroadcastClick) return false
446
450
  return areMessageUIPropsEqual(prev, next)
447
451
  }
448
452
  ) as typeof CustomMessageWithContext
@@ -38,6 +38,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
38
38
  customChannelActions,
39
39
  renderChannelActions,
40
40
  onParticipantNameClick,
41
+ onBroadcastClick,
41
42
  renderMessage,
42
43
  onMessageLinkClick,
43
44
  showChannelInfo,
@@ -263,6 +264,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
263
264
  customChannelActions={customChannelActions}
264
265
  renderChannelActions={renderChannelActions}
265
266
  onParticipantNameClick={onParticipantNameClick}
267
+ onBroadcastClick={onBroadcastClick}
266
268
  renderMessage={renderMessage}
267
269
  onMessageLinkClick={onMessageLinkClick}
268
270
  showChannelInfo={showChannelInfo}
package/src/index.ts CHANGED
@@ -60,6 +60,7 @@ export type {
60
60
  MessagingProviderProps,
61
61
  MessagingCapabilities,
62
62
  MessageLinkClickHandler,
63
+ BroadcastClickHandler,
63
64
  Participant,
64
65
  LockedAttachmentSource,
65
66
  } from './types'
@@ -54,6 +54,8 @@ export interface MessageMetadata {
54
54
  * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
55
55
  */
56
56
  sent_from?: 'stack_detail'
57
+ broadcast_id?: string
58
+ broadcast_name?: string
57
59
  }
58
60
 
59
61
  /**