@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/{Card-gNYP3vdJ.js → Card-BIP97Ybb.js} +2 -2
- package/dist/{Card-gNYP3vdJ.js.map → Card-BIP97Ybb.js.map} +1 -1
- package/dist/{Card-BOrp93Yo.cjs → Card-D9R7Qgk9.cjs} +2 -2
- package/dist/{Card-BOrp93Yo.cjs.map → Card-D9R7Qgk9.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BuqmY6ae.js → index-D1mXghgf.js} +1359 -1309
- package/dist/index-D1mXghgf.js.map +1 -0
- package/dist/index-cDF7EDso.cjs +2 -0
- package/dist/index-cDF7EDso.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1 -1
- package/dist/testing.d.ts +2 -0
- package/package.json +2 -2
- package/src/components/ChannelView.tsx +6 -1
- package/src/components/CustomMessage/CustomMessage.stories.tsx +40 -0
- package/src/components/CustomMessage/CustomMessage.test.tsx +141 -0
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +76 -4
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +75 -7
- package/src/components/CustomMessage/index.tsx +5 -1
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +1 -0
- package/src/stream-custom-data.ts +2 -0
- package/src/styles.css +48 -5
- package/src/types.ts +12 -0
- package/dist/index-BuqmY6ae.js.map +0 -1
- package/dist/index-DSZ8Rxnl.cjs +0 -2
- package/dist/index-DSZ8Rxnl.cjs.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-
|
|
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-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.
|
|
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": "
|
|
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
|
-
|
|
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