@linktr.ee/messaging-react 3.20.0 → 3.22.0-rc-1785413501
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-BOrp93Yo.cjs → Card-BiZLzRIQ.cjs} +2 -2
- package/dist/{Card-BOrp93Yo.cjs.map → Card-BiZLzRIQ.cjs.map} +1 -1
- package/dist/{Card-gNYP3vdJ.js → Card-G28kYBkV.js} +2 -2
- package/dist/{Card-gNYP3vdJ.js.map → Card-G28kYBkV.js.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/index-BnBvT5FW.cjs +2 -0
- package/dist/index-BnBvT5FW.cjs.map +1 -0
- package/dist/{index-BuqmY6ae.js → index-DIKyFjPC.js} +1373 -1309
- package/dist/index-DIKyFjPC.js.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 +46 -1
- package/src/components/CustomMessage/CustomMessage.test.tsx +141 -0
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +122 -4
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +89 -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 +86 -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-BnBvT5FW.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-DIKyFjPC.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.22.0-rc-1785413501",
|
|
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-1785413501",
|
|
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
|
}
|
|
@@ -163,7 +166,12 @@ const TemplateInner: React.FC<{
|
|
|
163
166
|
|
|
164
167
|
const MessageComponent = React.useMemo(() => {
|
|
165
168
|
return function CustomMessageComponent(props: MessageUIComponentProps) {
|
|
166
|
-
return
|
|
169
|
+
return (
|
|
170
|
+
<CustomMessage
|
|
171
|
+
{...props}
|
|
172
|
+
onBroadcastClick={() => undefined}
|
|
173
|
+
/>
|
|
174
|
+
)
|
|
167
175
|
}
|
|
168
176
|
}, [])
|
|
169
177
|
|
|
@@ -257,6 +265,43 @@ DeliveredStatus.args = {
|
|
|
257
265
|
],
|
|
258
266
|
}
|
|
259
267
|
|
|
268
|
+
export const BroadcastStatus: StoryFn<TemplateProps> = Template.bind({})
|
|
269
|
+
BroadcastStatus.args = {
|
|
270
|
+
messages: [
|
|
271
|
+
{
|
|
272
|
+
id: 'msg-1',
|
|
273
|
+
text: 'July class details',
|
|
274
|
+
user: storyUsers.creator,
|
|
275
|
+
metadata: {
|
|
276
|
+
custom_type: 'MESSAGE_OUTBOUND',
|
|
277
|
+
broadcast_id: 'broadcast-1',
|
|
278
|
+
broadcast_name: 'July 9th Class attendees',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const BroadcastStatusNonLatest: StoryFn<TemplateProps> = Template.bind({})
|
|
285
|
+
BroadcastStatusNonLatest.args = {
|
|
286
|
+
messages: [
|
|
287
|
+
{
|
|
288
|
+
id: 'msg-1',
|
|
289
|
+
text: 'July class details',
|
|
290
|
+
user: storyUsers.creator,
|
|
291
|
+
metadata: {
|
|
292
|
+
custom_type: 'MESSAGE_OUTBOUND',
|
|
293
|
+
broadcast_id: 'broadcast-1',
|
|
294
|
+
broadcast_name: 'July 9th Class attendees',
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 'msg-2',
|
|
299
|
+
text: 'Let me know if you have any questions.',
|
|
300
|
+
user: storyUsers.creator,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
}
|
|
304
|
+
|
|
260
305
|
export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})
|
|
261
306
|
WithTipTag.args = {
|
|
262
307
|
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,120 @@ 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
|
+
const button = screen.getByRole('button', {
|
|
223
|
+
name: 'View broadcast Broadcast',
|
|
224
|
+
})
|
|
225
|
+
expect(
|
|
226
|
+
button.querySelector('.sent-message-broadcast-badge')
|
|
227
|
+
).toBeInTheDocument()
|
|
228
|
+
expect(
|
|
229
|
+
button.querySelector('.sent-message-broadcast-name')
|
|
230
|
+
).toHaveTextContent('Broadcast')
|
|
231
|
+
expect(button).not.toHaveTextContent('•')
|
|
232
|
+
expect(screen.getAllByText('•')).toHaveLength(1)
|
|
233
|
+
button.click()
|
|
234
|
+
expect(onBroadcastClick).toHaveBeenCalledWith({ id: 'broadcast-1' })
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('keeps the latest separator outside the button and underlines only the name', () => {
|
|
238
|
+
const onBroadcastClick = vi.fn()
|
|
239
|
+
mockedUseMessageContext.mockReturnValue({
|
|
240
|
+
message: {
|
|
241
|
+
id: 'own-2',
|
|
242
|
+
metadata: {
|
|
243
|
+
broadcast_id: 'broadcast-1',
|
|
244
|
+
broadcast_name: 'July 9th Class attendees',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
isMyMessage: () => true,
|
|
248
|
+
threadList: false,
|
|
249
|
+
} as never)
|
|
250
|
+
|
|
251
|
+
renderWithProviders(
|
|
252
|
+
<SentMessageDeliveryStatus onBroadcastClick={onBroadcastClick} />
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const button = screen.getByRole('button', {
|
|
256
|
+
name: 'View broadcast July 9th Class attendees',
|
|
257
|
+
})
|
|
258
|
+
const name = within(button).getByTestId('sent-message-broadcast-status')
|
|
259
|
+
|
|
260
|
+
expect(screen.getAllByText('•')).toHaveLength(1)
|
|
261
|
+
expect(button).not.toHaveTextContent('•')
|
|
262
|
+
expect(name).toHaveClass('sent-message-broadcast-name')
|
|
263
|
+
expect(button.querySelector('.sent-message-broadcast-badge')).toHaveAttribute(
|
|
264
|
+
'aria-hidden',
|
|
265
|
+
'true'
|
|
266
|
+
)
|
|
267
|
+
expect(screen.getAllByTestId('sent-message-delivery-status')).toHaveLength(
|
|
268
|
+
3
|
|
269
|
+
)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('does not render a broadcast label for a visitor viewing a broadcast message', () => {
|
|
273
|
+
mockedUseMessageContext.mockReturnValue({
|
|
274
|
+
message: {
|
|
275
|
+
id: 'broadcast-1',
|
|
276
|
+
metadata: {
|
|
277
|
+
broadcast_id: 'broadcast-1',
|
|
278
|
+
broadcast_name: 'July 9th Class attendees',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
isMyMessage: () => false,
|
|
282
|
+
threadList: false,
|
|
283
|
+
} as never)
|
|
284
|
+
|
|
285
|
+
renderWithProviders(
|
|
286
|
+
<SentMessageDeliveryStatus onBroadcastClick={vi.fn()} />
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
expect(
|
|
290
|
+
screen.queryByTestId('sent-message-broadcast-status')
|
|
291
|
+
).not.toBeInTheDocument()
|
|
292
|
+
})
|
|
293
|
+
|
|
176
294
|
it('renders "Failed to send" for every own failed message', () => {
|
|
177
295
|
mockedUseMessageContext.mockReturnValue({
|
|
178
296
|
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,49 @@ 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 content = (
|
|
108
|
+
<>
|
|
109
|
+
<span className="sent-message-broadcast-badge" aria-hidden="true">
|
|
110
|
+
<MegaphoneIcon weight="fill" />
|
|
111
|
+
</span>
|
|
112
|
+
<span
|
|
113
|
+
className="sent-message-broadcast-name"
|
|
114
|
+
data-testid="sent-message-broadcast-status"
|
|
115
|
+
>
|
|
116
|
+
{name}
|
|
117
|
+
</span>
|
|
118
|
+
</>
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return onBroadcastClick ? (
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
className="sent-message-broadcast-link"
|
|
125
|
+
aria-label={`View broadcast ${name}`}
|
|
126
|
+
onClick={() =>
|
|
127
|
+
onBroadcastClick({
|
|
128
|
+
id: broadcastId,
|
|
129
|
+
...(broadcastName ? { name: broadcastName } : {}),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
{content}
|
|
134
|
+
</button>
|
|
135
|
+
) : (
|
|
136
|
+
content
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
96
140
|
const FailedStatus = () => (
|
|
97
141
|
<span className="str-chat__message-status sent-message-status--failed">
|
|
98
142
|
<WarningCircleIcon weight="bold" aria-hidden="true" />
|
|
@@ -114,10 +158,16 @@ const FailedStatus = () => (
|
|
|
114
158
|
* because it has no failed slot and are shown on every own, non-thread
|
|
115
159
|
* failed message.
|
|
116
160
|
*/
|
|
117
|
-
export const SentMessageDeliveryStatus = (
|
|
161
|
+
export const SentMessageDeliveryStatus = ({
|
|
162
|
+
onBroadcastClick,
|
|
163
|
+
}: {
|
|
164
|
+
onBroadcastClick?: (broadcast: { id: string; name?: string }) => void
|
|
165
|
+
}) => {
|
|
118
166
|
const { isMyMessage, message, threadList } = useMessageContext(
|
|
119
167
|
'SentMessageDeliveryStatus'
|
|
120
168
|
)
|
|
169
|
+
const isOwnMessage = isMyMessage()
|
|
170
|
+
const isLatestRealMessage = useIsLatestRealMessage()
|
|
121
171
|
|
|
122
172
|
// Stream's MessageStatus still mounts MessageSendingStatus when
|
|
123
173
|
// threadList is true; keep all of our stamps out of thread replies.
|
|
@@ -125,15 +175,28 @@ export const SentMessageDeliveryStatus = () => {
|
|
|
125
175
|
return null
|
|
126
176
|
}
|
|
127
177
|
|
|
128
|
-
if (
|
|
129
|
-
message.status === 'failed' &&
|
|
130
|
-
isMyMessage() &&
|
|
131
|
-
message.type !== 'error'
|
|
132
|
-
) {
|
|
178
|
+
if (message.status === 'failed' && isOwnMessage && message.type !== 'error') {
|
|
133
179
|
return <FailedStatus />
|
|
134
180
|
}
|
|
135
181
|
|
|
136
|
-
|
|
182
|
+
if (!isOwnMessage) {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const broadcastId = message.metadata?.broadcast_id
|
|
187
|
+
const broadcastName = message.metadata?.broadcast_name
|
|
188
|
+
const broadcastStatus =
|
|
189
|
+
message.status !== 'sending' &&
|
|
190
|
+
message.status !== 'failed' &&
|
|
191
|
+
broadcastId ? (
|
|
192
|
+
<BroadcastStatus
|
|
193
|
+
broadcastId={broadcastId}
|
|
194
|
+
broadcastName={broadcastName}
|
|
195
|
+
onBroadcastClick={onBroadcastClick}
|
|
196
|
+
/>
|
|
197
|
+
) : null
|
|
198
|
+
|
|
199
|
+
const messageStatus = (
|
|
137
200
|
<MessageStatus
|
|
138
201
|
MessageSentStatus={DeliveredStatus}
|
|
139
202
|
MessageDeliveredStatus={DeliveredStatus}
|
|
@@ -141,4 +204,23 @@ export const SentMessageDeliveryStatus = () => {
|
|
|
141
204
|
MessageSendingStatus={SendingStatus}
|
|
142
205
|
/>
|
|
143
206
|
)
|
|
207
|
+
|
|
208
|
+
if (!broadcastStatus) {
|
|
209
|
+
return messageStatus
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<span className="sent-message-status-row">
|
|
214
|
+
{messageStatus}
|
|
215
|
+
{isLatestRealMessage && (
|
|
216
|
+
<span
|
|
217
|
+
className="sent-message-broadcast-separator"
|
|
218
|
+
aria-hidden="true"
|
|
219
|
+
>
|
|
220
|
+
•
|
|
221
|
+
</span>
|
|
222
|
+
)}
|
|
223
|
+
{broadcastStatus}
|
|
224
|
+
</span>
|
|
225
|
+
)
|
|
144
226
|
}
|