@linktr.ee/messaging-react 4.2.0-rc-1787325521 → 4.2.0-rc-1787599724
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-BYJyA83X.js → Card-D7UPq5-g.js} +2 -2
- package/dist/{Card-BYJyA83X.js.map → Card-D7UPq5-g.js.map} +1 -1
- package/dist/{Card-BplJtZao.cjs → Card-DcBZqMBt.cjs} +2 -2
- package/dist/{Card-BplJtZao.cjs.map → Card-DcBZqMBt.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BUC9vqhJ.js → index-CSkr_CO5.js} +2089 -1952
- package/dist/index-CSkr_CO5.js.map +1 -0
- package/dist/index-CwkY7QwT.cjs +3 -0
- package/dist/index-CwkY7QwT.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +59 -78
- package/dist/index.js +26 -30
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +13 -11
- package/src/components/AttachmentCard/utils/icons.ts +7 -6
- package/src/components/Avatar/Avatar.test.tsx +0 -126
- package/src/components/Avatar/getAvatarEmoji.test.ts +0 -39
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +3 -410
- package/src/components/ChannelList/index.test.tsx +1 -132
- package/src/components/ChannelView.stories.tsx +0 -4
- package/src/components/ChannelView.test.tsx +3 -9
- package/src/components/CustomMessage/CustomMessage.stories.tsx +314 -6
- package/src/components/CustomMessage/CustomMessage.test.tsx +2 -56
- package/src/components/CustomMessage/MessageTag.stories.tsx +53 -17
- package/src/components/CustomMessage/MessageTag.test.tsx +40 -7
- package/src/components/CustomMessage/MessageTag.tsx +39 -7
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +7 -8
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -2
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +12 -5
- package/src/components/CustomMessage/index.tsx +19 -22
- package/src/components/CustomMessageInput/index.tsx +1 -1
- package/src/components/CustomSystemMessage/index.tsx +2 -3
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +0 -327
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +153 -0
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +450 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.test.ts +24 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.ts +16 -0
- package/src/components/MessageAttachment/Audio/index.tsx +83 -77
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
- package/src/components/MessageAttachment/_shared/fileMeta.test.ts +0 -44
- package/src/components/MessageAttachment/_shared/useIsClient.ts +23 -0
- package/src/components/MessageAttachment/index.tsx +25 -9
- package/src/components/MessageAttachment/types.ts +19 -9
- package/src/components/MessagingShell/MessagingShell.test.tsx +0 -20
- package/src/components/MessagingShell/index.tsx +0 -2
- package/src/components/RichCard/RichCard.test.tsx +1 -3
- package/src/index.ts +0 -4
- package/src/providers/MessagingProvider.test.tsx +2 -130
- package/src/providers/MessagingProvider.tsx +15 -26
- package/src/styles.css +210 -57
- package/src/testing/createMockMessagingClient.ts +2 -2
- package/src/types.ts +0 -9
- package/dist/index-BUC9vqhJ.js.map +0 -1
- package/dist/index-BkqyKPJP.cjs +0 -3
- package/dist/index-BkqyKPJP.cjs.map +0 -1
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +0 -346
- package/src/components/AttachmentCard/Thumbnail.test.tsx +0 -215
- package/src/components/AttachmentCard/index.test.tsx +0 -160
- package/src/components/ChannelList/ChannelListContext.test.tsx +0 -87
- package/src/components/CustomMessage/Locked.stories.tsx +0 -108
- package/src/components/CustomMessage/PaidMessage.stories.tsx +0 -42
- package/src/components/CustomMessage/Tip.stories.tsx +0 -48
- package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +0 -51
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +0 -65
- package/src/components/CustomMessage/TipMessage/index.tsx +0 -27
- package/src/components/FaqList/FaqList.test.tsx +0 -204
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +0 -76
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +0 -248
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +0 -123
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +0 -193
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +0 -242
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +0 -37
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +0 -72
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +0 -162
- package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +0 -182
- package/src/components/MessageAttachment/_shared/Bubble.test.tsx +0 -111
- package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +0 -36
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +0 -114
- package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +0 -42
- package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +0 -168
- package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +0 -52
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +0 -69
- package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +0 -45
- package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +0 -55
- package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +0 -246
- package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +0 -113
- package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +0 -68
- package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +0 -16
- package/src/logging/index.test.tsx +0 -136
- package/src/logging/index.tsx +0 -48
- package/src/stories/customMessageStoryTemplate.tsx +0 -202
|
@@ -19,7 +19,7 @@ export default meta
|
|
|
19
19
|
|
|
20
20
|
interface MockMessageOptions {
|
|
21
21
|
metadata?: {
|
|
22
|
-
custom_type?: '
|
|
22
|
+
custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
|
|
23
23
|
amount_text?: string
|
|
24
24
|
}
|
|
25
25
|
text?: string
|
|
@@ -48,6 +48,32 @@ const Template: StoryFn<ComponentProps> = (args) => {
|
|
|
48
48
|
// the variant matrix as the single snapshot source of truth.
|
|
49
49
|
const skipInChromatic = { chromatic: { disableSnapshot: true } }
|
|
50
50
|
|
|
51
|
+
export const Tip: StoryFn<ComponentProps> = Template.bind({})
|
|
52
|
+
Tip.args = {
|
|
53
|
+
message: createMockMessage({
|
|
54
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
|
|
55
|
+
}),
|
|
56
|
+
}
|
|
57
|
+
Tip.parameters = skipInChromatic
|
|
58
|
+
|
|
59
|
+
export const TipStandalone: StoryFn<ComponentProps> = Template.bind({})
|
|
60
|
+
TipStandalone.args = {
|
|
61
|
+
message: createMockMessage({
|
|
62
|
+
text: '',
|
|
63
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
64
|
+
}),
|
|
65
|
+
standalone: true,
|
|
66
|
+
}
|
|
67
|
+
TipStandalone.parameters = skipInChromatic
|
|
68
|
+
|
|
69
|
+
export const Paid: StoryFn<ComponentProps> = Template.bind({})
|
|
70
|
+
Paid.args = {
|
|
71
|
+
message: createMockMessage({
|
|
72
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
|
|
73
|
+
}),
|
|
74
|
+
}
|
|
75
|
+
Paid.parameters = skipInChromatic
|
|
76
|
+
|
|
51
77
|
export const ChatbotReceiverText: StoryFn<ComponentProps> = Template.bind({})
|
|
52
78
|
ChatbotReceiverText.args = {
|
|
53
79
|
message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
|
|
@@ -79,14 +105,6 @@ ChatbotSenderAttachment.args = {
|
|
|
79
105
|
}
|
|
80
106
|
ChatbotSenderAttachment.parameters = skipInChromatic
|
|
81
107
|
|
|
82
|
-
export const Paid: StoryFn<ComponentProps> = Template.bind({})
|
|
83
|
-
Paid.args = {
|
|
84
|
-
message: createMockMessage({
|
|
85
|
-
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
|
|
86
|
-
}),
|
|
87
|
-
}
|
|
88
|
-
Paid.parameters = skipInChromatic
|
|
89
|
-
|
|
90
108
|
export const NoTag: StoryFn<ComponentProps> = Template.bind({})
|
|
91
109
|
NoTag.args = {
|
|
92
110
|
message: createMockMessage(),
|
|
@@ -96,6 +114,32 @@ NoTag.parameters = skipInChromatic
|
|
|
96
114
|
export const AllVariants: StoryFn = () => {
|
|
97
115
|
return (
|
|
98
116
|
<div className="p-12 flex flex-col gap-4">
|
|
117
|
+
<div className="flex items-center gap-4">
|
|
118
|
+
<span className="text-sm w-32">Tip:</span>
|
|
119
|
+
<MessageTag
|
|
120
|
+
message={createMockMessage({
|
|
121
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$10.50' },
|
|
122
|
+
})}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="flex items-center gap-4">
|
|
126
|
+
<span className="text-sm w-32">Paid:</span>
|
|
127
|
+
<MessageTag
|
|
128
|
+
message={createMockMessage({
|
|
129
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
|
|
130
|
+
})}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="flex items-center gap-4">
|
|
134
|
+
<span className="text-sm w-32">Tip (standalone):</span>
|
|
135
|
+
<MessageTag
|
|
136
|
+
message={createMockMessage({
|
|
137
|
+
text: '',
|
|
138
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
139
|
+
})}
|
|
140
|
+
standalone
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
99
143
|
<div className="flex items-center gap-4">
|
|
100
144
|
<span className="text-sm w-32">Chatbot (receiver):</span>
|
|
101
145
|
<MessageTag
|
|
@@ -127,14 +171,6 @@ export const AllVariants: StoryFn = () => {
|
|
|
127
171
|
/>
|
|
128
172
|
</div>
|
|
129
173
|
</div>
|
|
130
|
-
<div className="flex items-center gap-4">
|
|
131
|
-
<span className="text-sm w-32">Paid:</span>
|
|
132
|
-
<MessageTag
|
|
133
|
-
message={createMockMessage({
|
|
134
|
-
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
|
|
135
|
-
})}
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
138
174
|
<div className="flex items-center gap-4">
|
|
139
175
|
<span className="text-sm w-32">No tag:</span>
|
|
140
176
|
<MessageTag message={createMockMessage()} />
|
|
@@ -7,7 +7,7 @@ import { MessageTag } from './MessageTag'
|
|
|
7
7
|
|
|
8
8
|
interface MockMessageOptions {
|
|
9
9
|
metadata?: {
|
|
10
|
-
custom_type?: '
|
|
10
|
+
custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
|
|
11
11
|
amount_text?: string
|
|
12
12
|
}
|
|
13
13
|
text?: string
|
|
@@ -78,7 +78,35 @@ describe('MessageTag', () => {
|
|
|
78
78
|
expect(indicator.children[1]).toHaveTextContent('Sent with AI')
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
-
it('
|
|
81
|
+
it('keeps tip tag behavior unchanged', () => {
|
|
82
|
+
renderWithProviders(
|
|
83
|
+
<MessageTag
|
|
84
|
+
message={createMockMessage({
|
|
85
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
|
|
86
|
+
})}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const tipTag = screen.getByText('Delivered with $5.50 tip')
|
|
91
|
+
expect(tipTag.closest('.message-tag--tip')).not.toBeNull()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('renders tip standalone label for textless tip messages', () => {
|
|
95
|
+
renderWithProviders(
|
|
96
|
+
<MessageTag
|
|
97
|
+
message={createMockMessage({
|
|
98
|
+
text: '',
|
|
99
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
100
|
+
})}
|
|
101
|
+
standalone
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const tipTag = screen.getByText('$50.00 tip')
|
|
106
|
+
expect(tipTag.closest('.message-tip-standalone')).not.toBeNull()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('renders paid footer tag with message copy, not tip copy', () => {
|
|
82
110
|
renderWithProviders(
|
|
83
111
|
<MessageTag
|
|
84
112
|
message={createMockMessage({
|
|
@@ -88,19 +116,24 @@ describe('MessageTag', () => {
|
|
|
88
116
|
)
|
|
89
117
|
|
|
90
118
|
const paidTag = screen.getByText('Delivered with $10.00 message')
|
|
91
|
-
expect(paidTag.closest('.message-tag--
|
|
119
|
+
expect(paidTag.closest('.message-tag--tip')).not.toBeNull()
|
|
120
|
+
expect(screen.queryByText(/tip/i)).toBeNull()
|
|
92
121
|
})
|
|
93
122
|
|
|
94
|
-
it('
|
|
95
|
-
|
|
123
|
+
it('renders paid standalone label for textless paid messages', () => {
|
|
124
|
+
renderWithProviders(
|
|
96
125
|
<MessageTag
|
|
97
126
|
message={createMockMessage({
|
|
98
|
-
|
|
127
|
+
text: '',
|
|
128
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
|
|
99
129
|
})}
|
|
130
|
+
standalone
|
|
100
131
|
/>
|
|
101
132
|
)
|
|
102
133
|
|
|
103
|
-
|
|
134
|
+
const paidTag = screen.getByText('$25.00 message')
|
|
135
|
+
expect(paidTag.closest('.message-tip-standalone')).not.toBeNull()
|
|
136
|
+
expect(screen.queryByText(/tip/i)).toBeNull()
|
|
104
137
|
})
|
|
105
138
|
|
|
106
139
|
it('returns null for non-custom messages', () => {
|
|
@@ -4,6 +4,8 @@ import { LocalMessage } from 'stream-chat'
|
|
|
4
4
|
|
|
5
5
|
interface MessageTagProps {
|
|
6
6
|
message: LocalMessage
|
|
7
|
+
/** When true, renders as a standalone bubble instead of a small tag */
|
|
8
|
+
standalone?: boolean
|
|
7
9
|
/** When true, renders sender-side chatbot variants */
|
|
8
10
|
isMyMessage?: boolean
|
|
9
11
|
/** Whether the message includes an attachment */
|
|
@@ -26,6 +28,11 @@ const SparkleIcon = ({ size = 15 }: { size?: number }) => (
|
|
|
26
28
|
</svg>
|
|
27
29
|
)
|
|
28
30
|
|
|
31
|
+
/** Check if a message is a tip based on metadata */
|
|
32
|
+
export const isTipMessage = (message: LocalMessage): boolean => {
|
|
33
|
+
return message.metadata?.custom_type === MESSAGE_CUSTOM_TYPE.TIP
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
/** Check if a message is a paid message based on metadata */
|
|
30
37
|
const isPaidMessage = (message: LocalMessage): boolean => {
|
|
31
38
|
return message.metadata?.custom_type === MESSAGE_CUSTOM_TYPE.PAID
|
|
@@ -55,31 +62,55 @@ export const isMediaAttachmentMessage = (message: LocalMessage): boolean => {
|
|
|
55
62
|
)
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
/** Check if a message has a tip or paid tag (shared layout, distinct copy) */
|
|
66
|
+
const isTipOrPaidMessage = (message: LocalMessage): boolean => {
|
|
67
|
+
return isTipMessage(message) || isPaidMessage(message)
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
/** Check if the footer tag on this message already reads "Delivered with …" */
|
|
59
71
|
export const hasPaidDeliveryTag = (message: LocalMessage): boolean => {
|
|
60
|
-
return
|
|
72
|
+
return (
|
|
73
|
+
isTipOrPaidMessage(message) &&
|
|
74
|
+
!isTipOnlyMessage(message) &&
|
|
75
|
+
!!message.metadata?.amount_text
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Check if a message is a tip/paid-only message (no text) */
|
|
80
|
+
export const isTipOnlyMessage = (message: LocalMessage): boolean => {
|
|
81
|
+
return isTipOrPaidMessage(message) && !message.text?.trim()
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
export const MessageTag = ({
|
|
64
85
|
message,
|
|
86
|
+
standalone = false,
|
|
65
87
|
isMyMessage = false,
|
|
66
88
|
hasAttachment = false,
|
|
67
89
|
}: MessageTagProps) => {
|
|
68
|
-
const
|
|
90
|
+
const isTipOrPaid = isTipOrPaidMessage(message)
|
|
69
91
|
const isChatbot = isChatbotMessage(message)
|
|
70
92
|
|
|
71
|
-
if (!
|
|
93
|
+
if (!isTipOrPaid && !isChatbot) {
|
|
72
94
|
return null
|
|
73
95
|
}
|
|
74
96
|
|
|
75
|
-
if (
|
|
97
|
+
if (isTipOrPaid) {
|
|
76
98
|
const amountText = message.metadata?.amount_text
|
|
77
99
|
if (!amountText) return null
|
|
78
100
|
|
|
101
|
+
const className = standalone
|
|
102
|
+
? '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]'
|
|
103
|
+
: 'message-tag--tip inline-flex items-center gap-1 text-[12px] leading-[2] font-medium text-[#016630]'
|
|
104
|
+
|
|
105
|
+
const amountLabel = isPaidMessage(message) ? 'message' : 'tip'
|
|
106
|
+
const label = standalone
|
|
107
|
+
? `${amountText} ${amountLabel}`
|
|
108
|
+
: `Delivered with ${amountText} ${amountLabel}`
|
|
109
|
+
|
|
79
110
|
return (
|
|
80
|
-
<div className=
|
|
81
|
-
<GiftIcon size={12} />
|
|
82
|
-
<span>
|
|
111
|
+
<div className={className}>
|
|
112
|
+
<GiftIcon size={standalone ? 14 : 12} />
|
|
113
|
+
<span>{label}</span>
|
|
83
114
|
</div>
|
|
84
115
|
)
|
|
85
116
|
}
|
|
@@ -104,6 +135,7 @@ export const MessageTag = ({
|
|
|
104
135
|
</span>
|
|
105
136
|
)
|
|
106
137
|
|
|
138
|
+
// Chatbot indicator variant
|
|
107
139
|
return (
|
|
108
140
|
<div
|
|
109
141
|
className={`${chatbotBase} ${chatbotVariant}`}
|
|
@@ -392,14 +392,14 @@ describe('SentMessageDeliveryStatus', () => {
|
|
|
392
392
|
})
|
|
393
393
|
|
|
394
394
|
it.each([
|
|
395
|
-
['
|
|
396
|
-
['
|
|
397
|
-
])('does not render the delivery stamp on %s', (_label,
|
|
395
|
+
['paid', 'MESSAGE_PAID'],
|
|
396
|
+
['tip', 'MESSAGE_TIP'],
|
|
397
|
+
])('does not render the delivery stamp on a %s message', (_label, type) => {
|
|
398
398
|
mockedUseMessageContext.mockReturnValue({
|
|
399
399
|
message: {
|
|
400
400
|
id: 'own-2',
|
|
401
|
-
text,
|
|
402
|
-
metadata: { custom_type:
|
|
401
|
+
text: 'thanks!',
|
|
402
|
+
metadata: { custom_type: type, amount_text: '$10.00' },
|
|
403
403
|
},
|
|
404
404
|
isMyMessage: () => true,
|
|
405
405
|
threadList: false,
|
|
@@ -413,12 +413,11 @@ describe('SentMessageDeliveryStatus', () => {
|
|
|
413
413
|
expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument()
|
|
414
414
|
})
|
|
415
415
|
|
|
416
|
-
it('keeps the delivery stamp on a
|
|
416
|
+
it('keeps the delivery stamp on a textless paid message, which has no footer tag', () => {
|
|
417
417
|
mockedUseMessageContext.mockReturnValue({
|
|
418
418
|
message: {
|
|
419
419
|
id: 'own-2',
|
|
420
|
-
|
|
421
|
-
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
|
|
420
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
|
|
422
421
|
},
|
|
423
422
|
isMyMessage: () => true,
|
|
424
423
|
threadList: false,
|
|
@@ -13,8 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import type { OutboundClickHandler, ResolvedOutbound } from '../../types'
|
|
15
15
|
|
|
16
|
-
import { hasPaidDeliveryTag } from './MessageTag'
|
|
17
|
-
import { isTipMessage } from './TipMessage'
|
|
16
|
+
import { hasPaidDeliveryTag, isTipMessage } from './MessageTag'
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Message status stamps for sending and terminal `MessageStatus` states
|
|
@@ -257,4 +257,37 @@ describe('StreamAttachmentMessage', () => {
|
|
|
257
257
|
)
|
|
258
258
|
).toHaveTextContent('from the trip')
|
|
259
259
|
})
|
|
260
|
+
|
|
261
|
+
it('forwards a persisted duration and waveform to the audio card', () => {
|
|
262
|
+
// Audio is sent as `type: "file"` with an `audio/*` mime type (for
|
|
263
|
+
// mobile compatibility), and the composer persists `duration` +
|
|
264
|
+
// `waveform_data` on the outgoing attachment. Without this
|
|
265
|
+
// passthrough every sent and received card would fall back to
|
|
266
|
+
// `--:--` and a flat placeholder even though the data is there.
|
|
267
|
+
renderWithProviders(
|
|
268
|
+
<StreamAttachmentMessage
|
|
269
|
+
groupPosition="single"
|
|
270
|
+
isMyMessage
|
|
271
|
+
message={message([
|
|
272
|
+
{
|
|
273
|
+
type: 'file',
|
|
274
|
+
asset_url: 'https://cdn.example.com/take-1.mp3',
|
|
275
|
+
mime_type: 'audio/mpeg',
|
|
276
|
+
title: 'take-1.mp3',
|
|
277
|
+
duration: 12,
|
|
278
|
+
waveform_data: [0.2, 0.9, 0.5, 0.7],
|
|
279
|
+
},
|
|
280
|
+
])}
|
|
281
|
+
/>
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
expect(screen.getByText('00:12')).toBeInTheDocument()
|
|
285
|
+
// The flat placeholder renders every bar at 0%; a real waveform is
|
|
286
|
+
// observable as bars drawn above that floor.
|
|
287
|
+
const barHeights = screen
|
|
288
|
+
.getAllByTestId('amplitude-bar')
|
|
289
|
+
.map((bar) => bar.style.getPropertyValue('--mes-audio-wave-bar'))
|
|
290
|
+
expect(barHeights).toHaveLength(35)
|
|
291
|
+
expect(barHeights.some((height) => height !== '0%')).toBe(true)
|
|
292
|
+
})
|
|
260
293
|
})
|
|
@@ -155,7 +155,9 @@ export function buildOrderedAttachmentSegments(
|
|
|
155
155
|
return segments
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function countMediaSegmentBubbles(
|
|
158
|
+
function countMediaSegmentBubbles(
|
|
159
|
+
segments: AttachmentSegment[]
|
|
160
|
+
): number {
|
|
159
161
|
return segments.filter((segment) => segment.type === 'media').length
|
|
160
162
|
}
|
|
161
163
|
|
|
@@ -219,6 +221,12 @@ function mediaItemForKind(
|
|
|
219
221
|
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
220
222
|
mimeType: trimToUndefined(attachment.mime_type) ?? 'audio/mpeg',
|
|
221
223
|
filename: trimToUndefined((attachment as { title?: string }).title),
|
|
224
|
+
// Both are first-class `stream-chat` Attachment fields. The
|
|
225
|
+
// composer writes them at send time so the waveform card paints
|
|
226
|
+
// the real amplitudes and the correct duration with no remote
|
|
227
|
+
// decode and no CDN CORS exposure.
|
|
228
|
+
durationSeconds: attachment.duration,
|
|
229
|
+
waveformData: attachment.waveform_data,
|
|
222
230
|
}
|
|
223
231
|
case 'pdf':
|
|
224
232
|
return {
|
|
@@ -243,9 +251,7 @@ function buildMediaClusterRenderProps(
|
|
|
243
251
|
): Record<string, unknown> {
|
|
244
252
|
if (attachments.length > 1) {
|
|
245
253
|
return {
|
|
246
|
-
items: attachments.map((attachment) =>
|
|
247
|
-
mediaItemForKind(kind, attachment)
|
|
248
|
-
),
|
|
254
|
+
items: attachments.map((attachment) => mediaItemForKind(kind, attachment)),
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
257
|
|
|
@@ -318,7 +324,8 @@ const StreamAttachmentMessage: React.FC<StreamAttachmentMessageProps> = ({
|
|
|
318
324
|
: groupPosition
|
|
319
325
|
// The trailing caption bubble carries the tail on single / last-in-run
|
|
320
326
|
// messages, matching the in-channel text bubbles.
|
|
321
|
-
const captionShowsTail =
|
|
327
|
+
const captionShowsTail =
|
|
328
|
+
groupPosition === 'single' || groupPosition === 'end'
|
|
322
329
|
|
|
323
330
|
const pushLinkRow = (
|
|
324
331
|
attachment: Attachment,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
isChatbotMessage,
|
|
44
44
|
isMediaAttachmentMessage,
|
|
45
45
|
isTextAttachmentMessage,
|
|
46
|
+
isTipOnlyMessage,
|
|
46
47
|
} from './MessageTag'
|
|
47
48
|
import { MessageTail } from './MessageTail'
|
|
48
49
|
import { OutboundContext } from './OutboundContext'
|
|
@@ -50,7 +51,6 @@ import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
|
|
|
50
51
|
import StreamAttachmentMessage, {
|
|
51
52
|
buildOrderedAttachmentSegments,
|
|
52
53
|
} from './StreamAttachmentMessage'
|
|
53
|
-
import { TipMessage, isTipMessage } from './TipMessage'
|
|
54
54
|
|
|
55
55
|
type CustomMessageUIComponentProps = MessageUIComponentProps & {
|
|
56
56
|
viewerLanguage?: string
|
|
@@ -216,7 +216,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
216
216
|
)
|
|
217
217
|
|
|
218
218
|
const poll = message.poll_id && client.polls.fromState(message.poll_id)
|
|
219
|
-
const
|
|
219
|
+
const isTipOnly = isTipOnlyMessage(message)
|
|
220
220
|
const isChatbot = isChatbotMessage(message)
|
|
221
221
|
const isAttachment = isAttachmentMessage(message)
|
|
222
222
|
const hasRenderableAttachments = !!(
|
|
@@ -258,18 +258,6 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
258
258
|
? () => onFetchSource(message, channel)
|
|
259
259
|
: undefined,
|
|
260
260
|
}
|
|
261
|
-
// Shared by the tip and locked-media-attachment branches below: an
|
|
262
|
-
// accompanying text bubble stacked under the pill / card.
|
|
263
|
-
const accompanyingTextBubble = (
|
|
264
|
-
<div
|
|
265
|
-
className={classNames('str-chat__message-bubble', {
|
|
266
|
-
'relative isolate': showTail,
|
|
267
|
-
})}
|
|
268
|
-
>
|
|
269
|
-
<MessageText message={displayMessage} renderText={renderText} />
|
|
270
|
-
{showTail && <MessageTail side={isMine ? 'me' : 'other'} />}
|
|
271
|
-
</div>
|
|
272
|
-
)
|
|
273
261
|
return (
|
|
274
262
|
<>
|
|
275
263
|
{editing && (
|
|
@@ -319,11 +307,8 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
319
307
|
marginInlineStart: 0,
|
|
320
308
|
}}
|
|
321
309
|
>
|
|
322
|
-
{
|
|
323
|
-
<
|
|
324
|
-
<TipMessage message={message} />
|
|
325
|
-
{message.text?.trim() && accompanyingTextBubble}
|
|
326
|
-
</div>
|
|
310
|
+
{isTipOnly ? (
|
|
311
|
+
<MessageTag message={message} standalone />
|
|
327
312
|
) : isTextAttachmentMessage(message) ? (
|
|
328
313
|
<div className="str-chat__message-bubble-wrapper">
|
|
329
314
|
<div className="flex items-center gap-2">
|
|
@@ -342,7 +327,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
342
327
|
</div>
|
|
343
328
|
</div>
|
|
344
329
|
) : isMediaAttachmentMessage(message) ? (
|
|
345
|
-
<div className="str-chat__message-bubble-wrapper
|
|
330
|
+
<div className="str-chat__message-bubble-wrapper">
|
|
346
331
|
<div className="flex items-center gap-2">
|
|
347
332
|
{isMine && MessageActions && <MessageActions />}
|
|
348
333
|
{isMine ? (
|
|
@@ -374,7 +359,19 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
374
359
|
)}
|
|
375
360
|
{!isMine && MessageActions && <MessageActions />}
|
|
376
361
|
</div>
|
|
377
|
-
{message.text &&
|
|
362
|
+
{message.text && (
|
|
363
|
+
<div
|
|
364
|
+
className={classNames('str-chat__message-bubble', {
|
|
365
|
+
'relative isolate': showTail,
|
|
366
|
+
})}
|
|
367
|
+
>
|
|
368
|
+
<MessageText
|
|
369
|
+
message={displayMessage}
|
|
370
|
+
renderText={renderText}
|
|
371
|
+
/>
|
|
372
|
+
{showTail && <MessageTail side={isMine ? 'me' : 'other'} />}
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
378
375
|
</div>
|
|
379
376
|
) : canRenderAttachmentsInToolkit ? (
|
|
380
377
|
<StreamAttachmentMessage
|
|
@@ -428,7 +425,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
428
425
|
</div>
|
|
429
426
|
)}
|
|
430
427
|
</div>
|
|
431
|
-
{!isAttachment && !
|
|
428
|
+
{!isAttachment && !isTipOnly && (
|
|
432
429
|
<div className="str-chat__message-footer">
|
|
433
430
|
{(!isChatbot || useAttachmentFooterChatbotTag) && (
|
|
434
431
|
<MessageTag
|
|
@@ -496,7 +496,7 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
|
|
|
496
496
|
|
|
497
497
|
<div className="relative z-10 flex min-h-0 flex-col">
|
|
498
498
|
{renderHeader?.()}
|
|
499
|
-
<div className="flex min-h-0 flex-col gap-4 p-
|
|
499
|
+
<div className="flex min-h-0 flex-col gap-4 p-4">
|
|
500
500
|
<div
|
|
501
501
|
// @ts-expect-error Only React 19 onwards has `inert` in its types.
|
|
502
502
|
inert={isFrozen ? '' : undefined}
|
|
@@ -19,9 +19,8 @@ const DM_AGENT_SYSTEM_MESSAGE_FALLBACK_TEXT: Record<DmAgentSystemType, string> =
|
|
|
19
19
|
* explicitly added. Keep in sync with the mobile client (cerberus
|
|
20
20
|
* `CustomMessageSystem`); there is no shared package between the two surfaces.
|
|
21
21
|
*/
|
|
22
|
-
const SHOW_SYSTEM_MESSAGE_TIMESTAMP_TYPES: readonly AgeSafetySystemType[] =
|
|
23
|
-
'SYSTEM_AGE_SAFETY_BLOCKED'
|
|
24
|
-
]
|
|
22
|
+
const SHOW_SYSTEM_MESSAGE_TIMESTAMP_TYPES: readonly AgeSafetySystemType[] =
|
|
23
|
+
['SYSTEM_AGE_SAFETY_BLOCKED']
|
|
25
24
|
|
|
26
25
|
const AGE_SAFETY_SYSTEM_MESSAGE_FALLBACK_TEXT: Record<
|
|
27
26
|
AgeSafetySystemType,
|