@linktr.ee/messaging-react 4.2.0-rc-1787083791 → 4.2.0-rc-1787181113
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-B4lMv-0o.js → Card-BOudQBd6.js} +2 -2
- package/dist/{Card-B4lMv-0o.js.map → Card-BOudQBd6.js.map} +1 -1
- package/dist/{Card-DWL5I62S.cjs → Card-k2959zNX.cjs} +2 -2
- package/dist/{Card-DWL5I62S.cjs.map → Card-k2959zNX.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-C93LTGLc.js → index-6RdyZh6F.js} +1809 -1912
- package/dist/index-6RdyZh6F.js.map +1 -0
- package/dist/index-BcU18EjV.cjs +3 -0
- package/dist/index-BcU18EjV.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +78 -59
- package/dist/index.js +30 -26
- 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 +2 -2
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +346 -0
- package/src/components/AttachmentCard/Thumbnail.test.tsx +215 -0
- package/src/components/AttachmentCard/index.test.tsx +160 -0
- package/src/components/Avatar/Avatar.test.tsx +126 -0
- package/src/components/Avatar/getAvatarEmoji.test.ts +39 -0
- package/src/components/ChannelList/ChannelListContext.test.tsx +87 -0
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +410 -3
- package/src/components/ChannelList/index.test.tsx +132 -1
- package/src/components/CustomMessage/CustomMessage.stories.tsx +6 -314
- package/src/components/CustomMessage/CustomMessage.test.tsx +56 -2
- package/src/components/CustomMessage/Locked.stories.tsx +108 -0
- package/src/components/CustomMessage/MessageTag.stories.tsx +17 -53
- package/src/components/CustomMessage/MessageTag.test.tsx +7 -40
- package/src/components/CustomMessage/MessageTag.tsx +7 -39
- package/src/components/CustomMessage/PaidMessage.stories.tsx +42 -0
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +8 -7
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +2 -1
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +0 -33
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +0 -6
- package/src/components/CustomMessage/Tip.stories.tsx +48 -0
- package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +51 -0
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +65 -0
- package/src/components/CustomMessage/TipMessage/index.tsx +27 -0
- package/src/components/CustomMessage/index.tsx +22 -19
- package/src/components/FaqList/FaqList.test.tsx +204 -0
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +327 -0
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +76 -0
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +248 -0
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +123 -0
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +193 -0
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +242 -0
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +37 -0
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +72 -0
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +162 -0
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +58 -165
- package/src/components/MessageAttachment/Audio/index.tsx +77 -71
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +74 -176
- package/src/components/MessageAttachment/index.tsx +9 -25
- package/src/components/MessageAttachment/types.ts +9 -19
- package/src/components/MessagingShell/MessagingShell.test.tsx +20 -0
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +4 -0
- package/src/logging/index.test.tsx +136 -0
- package/src/logging/index.tsx +48 -0
- package/src/providers/MessagingProvider.test.tsx +113 -1
- package/src/providers/MessagingProvider.tsx +25 -15
- package/src/stories/customMessageStoryTemplate.tsx +199 -0
- package/src/styles.css +10 -121
- package/src/testing/createMockMessagingClient.ts +2 -2
- package/src/types.ts +9 -0
- package/dist/index-B-9LkWep.cjs +0 -3
- package/dist/index-B-9LkWep.cjs.map +0 -1
- package/dist/index-C93LTGLc.js.map +0 -1
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +0 -335
|
@@ -7,7 +7,7 @@ import { MessageTag } from './MessageTag'
|
|
|
7
7
|
|
|
8
8
|
interface MockMessageOptions {
|
|
9
9
|
metadata?: {
|
|
10
|
-
custom_type?: '
|
|
10
|
+
custom_type?: 'MESSAGE_CHATBOT' | 'MESSAGE_PAID'
|
|
11
11
|
amount_text?: string
|
|
12
12
|
}
|
|
13
13
|
text?: string
|
|
@@ -78,35 +78,7 @@ describe('MessageTag', () => {
|
|
|
78
78
|
expect(indicator.children[1]).toHaveTextContent('Sent with AI')
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
-
it('
|
|
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', () => {
|
|
81
|
+
it('renders the "Delivered with $X message" paid tag', () => {
|
|
110
82
|
renderWithProviders(
|
|
111
83
|
<MessageTag
|
|
112
84
|
message={createMockMessage({
|
|
@@ -116,24 +88,19 @@ describe('MessageTag', () => {
|
|
|
116
88
|
)
|
|
117
89
|
|
|
118
90
|
const paidTag = screen.getByText('Delivered with $10.00 message')
|
|
119
|
-
expect(paidTag.closest('.message-tag--
|
|
120
|
-
expect(screen.queryByText(/tip/i)).toBeNull()
|
|
91
|
+
expect(paidTag.closest('.message-tag--paid')).not.toBeNull()
|
|
121
92
|
})
|
|
122
93
|
|
|
123
|
-
it('
|
|
124
|
-
renderWithProviders(
|
|
94
|
+
it('returns null for a paid message with no amount_text', () => {
|
|
95
|
+
const { container } = renderWithProviders(
|
|
125
96
|
<MessageTag
|
|
126
97
|
message={createMockMessage({
|
|
127
|
-
|
|
128
|
-
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
|
|
98
|
+
metadata: { custom_type: 'MESSAGE_PAID' },
|
|
129
99
|
})}
|
|
130
|
-
standalone
|
|
131
100
|
/>
|
|
132
101
|
)
|
|
133
102
|
|
|
134
|
-
|
|
135
|
-
expect(paidTag.closest('.message-tip-standalone')).not.toBeNull()
|
|
136
|
-
expect(screen.queryByText(/tip/i)).toBeNull()
|
|
103
|
+
expect(container.firstChild).toBeNull()
|
|
137
104
|
})
|
|
138
105
|
|
|
139
106
|
it('returns null for non-custom messages', () => {
|
|
@@ -4,8 +4,6 @@ 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
|
|
9
7
|
/** When true, renders sender-side chatbot variants */
|
|
10
8
|
isMyMessage?: boolean
|
|
11
9
|
/** Whether the message includes an attachment */
|
|
@@ -28,11 +26,6 @@ const SparkleIcon = ({ size = 15 }: { size?: number }) => (
|
|
|
28
26
|
</svg>
|
|
29
27
|
)
|
|
30
28
|
|
|
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
|
-
|
|
36
29
|
/** Check if a message is a paid message based on metadata */
|
|
37
30
|
const isPaidMessage = (message: LocalMessage): boolean => {
|
|
38
31
|
return message.metadata?.custom_type === MESSAGE_CUSTOM_TYPE.PAID
|
|
@@ -62,55 +55,31 @@ export const isMediaAttachmentMessage = (message: LocalMessage): boolean => {
|
|
|
62
55
|
)
|
|
63
56
|
}
|
|
64
57
|
|
|
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
|
-
|
|
70
58
|
/** Check if the footer tag on this message already reads "Delivered with …" */
|
|
71
59
|
export const hasPaidDeliveryTag = (message: LocalMessage): boolean => {
|
|
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()
|
|
60
|
+
return isPaidMessage(message) && !!message.metadata?.amount_text
|
|
82
61
|
}
|
|
83
62
|
|
|
84
63
|
export const MessageTag = ({
|
|
85
64
|
message,
|
|
86
|
-
standalone = false,
|
|
87
65
|
isMyMessage = false,
|
|
88
66
|
hasAttachment = false,
|
|
89
67
|
}: MessageTagProps) => {
|
|
90
|
-
const
|
|
68
|
+
const isPaid = isPaidMessage(message)
|
|
91
69
|
const isChatbot = isChatbotMessage(message)
|
|
92
70
|
|
|
93
|
-
if (!
|
|
71
|
+
if (!isPaid && !isChatbot) {
|
|
94
72
|
return null
|
|
95
73
|
}
|
|
96
74
|
|
|
97
|
-
if (
|
|
75
|
+
if (isPaid) {
|
|
98
76
|
const amountText = message.metadata?.amount_text
|
|
99
77
|
if (!amountText) return null
|
|
100
78
|
|
|
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
|
-
|
|
110
79
|
return (
|
|
111
|
-
<div className=
|
|
112
|
-
<GiftIcon size={
|
|
113
|
-
<span>{
|
|
80
|
+
<div className="message-tag--paid inline-flex items-center gap-1 text-[12px] font-medium leading-[2] text-[#016630]">
|
|
81
|
+
<GiftIcon size={12} />
|
|
82
|
+
<span>Delivered with {amountText} message</span>
|
|
114
83
|
</div>
|
|
115
84
|
)
|
|
116
85
|
}
|
|
@@ -135,7 +104,6 @@ export const MessageTag = ({
|
|
|
135
104
|
</span>
|
|
136
105
|
)
|
|
137
106
|
|
|
138
|
-
// Chatbot indicator variant
|
|
139
107
|
return (
|
|
140
108
|
<div
|
|
141
109
|
className={`${chatbotBase} ${chatbotVariant}`}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Meta } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Template,
|
|
5
|
+
type TemplateProps,
|
|
6
|
+
} from '../../stories/customMessageStoryTemplate'
|
|
7
|
+
import {
|
|
8
|
+
currentUserArgType,
|
|
9
|
+
storyUsers,
|
|
10
|
+
} from '../../stories/decorators/storyUser'
|
|
11
|
+
|
|
12
|
+
const meta: Meta = {
|
|
13
|
+
title: 'CustomMessage/Paid Message',
|
|
14
|
+
argTypes: currentUserArgType,
|
|
15
|
+
args: { currentUser: storyUsers.creator },
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: 'fullscreen',
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
export default meta
|
|
21
|
+
|
|
22
|
+
// A paid message keeps the original small "Delivered with $X message" tag
|
|
23
|
+
// (gift icon + green text) under the bubble — unlike a tip, it never gets
|
|
24
|
+
// the pill treatment.
|
|
25
|
+
export const Default = Template.bind({})
|
|
26
|
+
Default.args = {
|
|
27
|
+
messages: [
|
|
28
|
+
{
|
|
29
|
+
id: 'msg-1',
|
|
30
|
+
text: "Here's the private coaching link we discussed!",
|
|
31
|
+
user: storyUsers.creator,
|
|
32
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10' },
|
|
33
|
+
},
|
|
34
|
+
// Trailing reply so the paid message is no longer the latest own
|
|
35
|
+
// message — keeps the "✓✓ Delivered" stamp off this example.
|
|
36
|
+
{
|
|
37
|
+
id: 'msg-2',
|
|
38
|
+
text: 'Thank you so much!',
|
|
39
|
+
user: storyUsers.visitor,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
} satisfies TemplateProps
|
|
@@ -392,14 +392,14 @@ describe('SentMessageDeliveryStatus', () => {
|
|
|
392
392
|
})
|
|
393
393
|
|
|
394
394
|
it.each([
|
|
395
|
-
['paid', '
|
|
396
|
-
['
|
|
397
|
-
])('does not render the delivery stamp on
|
|
395
|
+
['a paid message with text', 'thanks!'],
|
|
396
|
+
['a textless paid message', undefined],
|
|
397
|
+
])('does not render the delivery stamp on %s', (_label, text) => {
|
|
398
398
|
mockedUseMessageContext.mockReturnValue({
|
|
399
399
|
message: {
|
|
400
400
|
id: 'own-2',
|
|
401
|
-
text
|
|
402
|
-
metadata: { custom_type:
|
|
401
|
+
text,
|
|
402
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
|
|
403
403
|
},
|
|
404
404
|
isMyMessage: () => true,
|
|
405
405
|
threadList: false,
|
|
@@ -413,11 +413,12 @@ 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 tip, which renders a pill instead of a footer tag', () => {
|
|
417
417
|
mockedUseMessageContext.mockReturnValue({
|
|
418
418
|
message: {
|
|
419
419
|
id: 'own-2',
|
|
420
|
-
|
|
420
|
+
text: 'thanks!',
|
|
421
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' },
|
|
421
422
|
},
|
|
422
423
|
isMyMessage: () => true,
|
|
423
424
|
threadList: false,
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import type { OutboundClickHandler, ResolvedOutbound } from '../../types'
|
|
15
15
|
|
|
16
|
-
import { hasPaidDeliveryTag
|
|
16
|
+
import { hasPaidDeliveryTag } from './MessageTag'
|
|
17
|
+
import { isTipMessage } from './TipMessage'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Message status stamps for sending and terminal `MessageStatus` states
|
|
@@ -257,37 +257,4 @@ 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
|
-
})
|
|
293
260
|
})
|
|
@@ -221,12 +221,6 @@ function mediaItemForKind(
|
|
|
221
221
|
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
222
222
|
mimeType: trimToUndefined(attachment.mime_type) ?? 'audio/mpeg',
|
|
223
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,
|
|
230
224
|
}
|
|
231
225
|
case 'pdf':
|
|
232
226
|
return {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Template,
|
|
5
|
+
type TemplateProps,
|
|
6
|
+
} from '../../stories/customMessageStoryTemplate'
|
|
7
|
+
import {
|
|
8
|
+
currentUserArgType,
|
|
9
|
+
storyUsers,
|
|
10
|
+
} from '../../stories/decorators/storyUser'
|
|
11
|
+
|
|
12
|
+
const meta: Meta = {
|
|
13
|
+
title: 'CustomMessage/Tip Message',
|
|
14
|
+
argTypes: currentUserArgType,
|
|
15
|
+
args: { currentUser: storyUsers.creator },
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: 'fullscreen',
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
export default meta
|
|
21
|
+
|
|
22
|
+
// Figma "Tip with message" (node 2353:14548): a sent tip pill stacked
|
|
23
|
+
// directly above its accompanying text bubble, tail + delivery status below.
|
|
24
|
+
export const TipWithMessage = Template.bind({})
|
|
25
|
+
TipWithMessage.args = {
|
|
26
|
+
messages: [
|
|
27
|
+
{
|
|
28
|
+
id: 'msg-1',
|
|
29
|
+
text: 'Thanks you so much!',
|
|
30
|
+
user: storyUsers.creator,
|
|
31
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$1' },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
} satisfies TemplateProps
|
|
35
|
+
|
|
36
|
+
// Same "Tip with message" layout, but textless — only the tip pill renders,
|
|
37
|
+
// no second bubble.
|
|
38
|
+
export const TipWithoutMessage = Template.bind({})
|
|
39
|
+
TipWithoutMessage.args = {
|
|
40
|
+
messages: [
|
|
41
|
+
{
|
|
42
|
+
id: 'msg-1',
|
|
43
|
+
text: '',
|
|
44
|
+
user: storyUsers.creator,
|
|
45
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$1' },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
} satisfies TemplateProps
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { LocalMessage } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
import { now } from '../../../stories/decorators/storyTime'
|
|
6
|
+
|
|
7
|
+
import { TipMessage } from '.'
|
|
8
|
+
|
|
9
|
+
type ComponentProps = React.ComponentProps<typeof TipMessage>
|
|
10
|
+
|
|
11
|
+
const meta: Meta<ComponentProps> = {
|
|
12
|
+
title: 'TipMessage',
|
|
13
|
+
component: TipMessage,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'centered',
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
export default meta
|
|
19
|
+
|
|
20
|
+
interface MockMessageOptions {
|
|
21
|
+
metadata?: {
|
|
22
|
+
custom_type?: 'MESSAGE_TIP'
|
|
23
|
+
amount_text?: string
|
|
24
|
+
}
|
|
25
|
+
text?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createMockMessage = (options?: MockMessageOptions): LocalMessage =>
|
|
29
|
+
({
|
|
30
|
+
id: 'msg-1',
|
|
31
|
+
text: options?.text ?? '',
|
|
32
|
+
type: 'regular',
|
|
33
|
+
created_at: now(),
|
|
34
|
+
updated_at: now(),
|
|
35
|
+
metadata: options?.metadata,
|
|
36
|
+
}) as LocalMessage
|
|
37
|
+
|
|
38
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
39
|
+
return (
|
|
40
|
+
<div className="p-12">
|
|
41
|
+
<TipMessage {...args} />
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Standalone: StoryFn<ComponentProps> = Template.bind({})
|
|
47
|
+
Standalone.args = {
|
|
48
|
+
message: createMockMessage({
|
|
49
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
50
|
+
}),
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { LocalMessage } from 'stream-chat'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders, screen } from '../../../test/utils'
|
|
5
|
+
|
|
6
|
+
import { TipMessage } from '.'
|
|
7
|
+
|
|
8
|
+
interface MockMessageOptions {
|
|
9
|
+
metadata?: {
|
|
10
|
+
custom_type?: 'MESSAGE_TIP'
|
|
11
|
+
amount_text?: string
|
|
12
|
+
}
|
|
13
|
+
text?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const createMockMessage = (options?: MockMessageOptions): LocalMessage =>
|
|
17
|
+
({
|
|
18
|
+
id: 'msg-1',
|
|
19
|
+
text: options?.text ?? 'Hello world',
|
|
20
|
+
type: 'regular',
|
|
21
|
+
created_at: new Date(),
|
|
22
|
+
updated_at: new Date(),
|
|
23
|
+
metadata: options?.metadata,
|
|
24
|
+
}) as LocalMessage
|
|
25
|
+
|
|
26
|
+
describe('TipMessage', () => {
|
|
27
|
+
it('renders tip pill regardless of accompanying text', () => {
|
|
28
|
+
renderWithProviders(
|
|
29
|
+
<TipMessage
|
|
30
|
+
message={createMockMessage({
|
|
31
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
|
|
32
|
+
})}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const tipTag = screen.getByText('$5.50 tip!')
|
|
37
|
+
expect(tipTag.closest('.message-tip-pill')).not.toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders tip pill label for textless tip messages', () => {
|
|
41
|
+
renderWithProviders(
|
|
42
|
+
<TipMessage
|
|
43
|
+
message={createMockMessage({
|
|
44
|
+
text: '',
|
|
45
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
46
|
+
})}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const tipTag = screen.getByText('$50.00 tip!')
|
|
51
|
+
expect(tipTag.closest('.message-tip-pill')).not.toBeNull()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns null when amount_text is missing', () => {
|
|
55
|
+
const { container } = renderWithProviders(
|
|
56
|
+
<TipMessage
|
|
57
|
+
message={createMockMessage({
|
|
58
|
+
metadata: { custom_type: 'MESSAGE_TIP' },
|
|
59
|
+
})}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
expect(container.firstChild).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MESSAGE_CUSTOM_TYPE } from '@linktr.ee/messaging-taxonomy'
|
|
2
|
+
import { LocalMessage } from 'stream-chat'
|
|
3
|
+
|
|
4
|
+
/** Check if a message is a tip based on metadata */
|
|
5
|
+
export const isTipMessage = (message: LocalMessage): boolean => {
|
|
6
|
+
return message.metadata?.custom_type === MESSAGE_CUSTOM_TYPE.TIP
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Tip pill — the gift-emoji bubble a tip message attaches, with or without accompanying text */
|
|
10
|
+
export const TipMessage = ({ message }: { message: LocalMessage }) => {
|
|
11
|
+
const amountText = message.metadata?.amount_text
|
|
12
|
+
if (!amountText) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="message-tip-pill inline-flex items-end gap-0.5 max-w-[520px] rounded-[1.5rem] bg-[#43e660] px-4 py-3 text-white">
|
|
16
|
+
<span
|
|
17
|
+
className="flex h-5 items-center text-[20px] leading-none"
|
|
18
|
+
aria-hidden="true"
|
|
19
|
+
>
|
|
20
|
+
🎁
|
|
21
|
+
</span>
|
|
22
|
+
<span className="text-[14px] font-bold leading-[1.43] tracking-[0.28px] text-[rgba(4,4,3,0.9)]">
|
|
23
|
+
{amountText} tip!
|
|
24
|
+
</span>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
isChatbotMessage,
|
|
44
44
|
isMediaAttachmentMessage,
|
|
45
45
|
isTextAttachmentMessage,
|
|
46
|
-
isTipOnlyMessage,
|
|
47
46
|
} from './MessageTag'
|
|
48
47
|
import { MessageTail } from './MessageTail'
|
|
49
48
|
import { OutboundContext } from './OutboundContext'
|
|
@@ -51,6 +50,7 @@ import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
|
|
|
51
50
|
import StreamAttachmentMessage, {
|
|
52
51
|
buildOrderedAttachmentSegments,
|
|
53
52
|
} 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 isTip = isTipMessage(message)
|
|
220
220
|
const isChatbot = isChatbotMessage(message)
|
|
221
221
|
const isAttachment = isAttachmentMessage(message)
|
|
222
222
|
const hasRenderableAttachments = !!(
|
|
@@ -258,6 +258,18 @@ 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
|
+
)
|
|
261
273
|
return (
|
|
262
274
|
<>
|
|
263
275
|
{editing && (
|
|
@@ -307,8 +319,11 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
307
319
|
marginInlineStart: 0,
|
|
308
320
|
}}
|
|
309
321
|
>
|
|
310
|
-
{
|
|
311
|
-
<
|
|
322
|
+
{isTip ? (
|
|
323
|
+
<div className="str-chat__message-bubble-wrapper message-tip-wrapper">
|
|
324
|
+
<TipMessage message={message} />
|
|
325
|
+
{message.text?.trim() && accompanyingTextBubble}
|
|
326
|
+
</div>
|
|
312
327
|
) : isTextAttachmentMessage(message) ? (
|
|
313
328
|
<div className="str-chat__message-bubble-wrapper">
|
|
314
329
|
<div className="flex items-center gap-2">
|
|
@@ -327,7 +342,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
327
342
|
</div>
|
|
328
343
|
</div>
|
|
329
344
|
) : isMediaAttachmentMessage(message) ? (
|
|
330
|
-
<div className="str-chat__message-bubble-wrapper">
|
|
345
|
+
<div className="str-chat__message-bubble-wrapper message-locked-attachment-wrapper">
|
|
331
346
|
<div className="flex items-center gap-2">
|
|
332
347
|
{isMine && MessageActions && <MessageActions />}
|
|
333
348
|
{isMine ? (
|
|
@@ -359,19 +374,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
359
374
|
)}
|
|
360
375
|
{!isMine && MessageActions && <MessageActions />}
|
|
361
376
|
</div>
|
|
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
|
-
)}
|
|
377
|
+
{message.text && accompanyingTextBubble}
|
|
375
378
|
</div>
|
|
376
379
|
) : canRenderAttachmentsInToolkit ? (
|
|
377
380
|
<StreamAttachmentMessage
|
|
@@ -425,7 +428,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
425
428
|
</div>
|
|
426
429
|
)}
|
|
427
430
|
</div>
|
|
428
|
-
{!isAttachment && !
|
|
431
|
+
{!isAttachment && !isTip && (
|
|
429
432
|
<div className="str-chat__message-footer">
|
|
430
433
|
{(!isChatbot || useAttachmentFooterChatbotTag) && (
|
|
431
434
|
<MessageTag
|