@linktr.ee/messaging-react 1.13.0 → 1.14.0
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/assets/index.css +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +931 -721
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelView.tsx +36 -23
- package/src/components/CustomDateSeparator/index.tsx +5 -0
- package/src/components/CustomLinkPreviewList/index.tsx +1 -1
- package/src/components/CustomMessage/CustomMessage.stories.tsx +208 -0
- package/src/components/CustomMessage/MessageTag.stories.tsx +119 -0
- package/src/components/CustomMessage/MessageTag.tsx +84 -0
- package/src/components/CustomMessage/index.tsx +271 -0
- package/src/components/CustomSystemMessage/index.tsx +1 -0
- package/src/stream-custom-data.ts +11 -0
- package/src/styles.css +45 -0
package/package.json
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
Window,
|
|
15
15
|
MessageList,
|
|
16
16
|
useChannelStateContext,
|
|
17
|
+
WithComponents,
|
|
18
|
+
MessageUIComponentProps,
|
|
17
19
|
} from 'stream-chat-react'
|
|
18
20
|
|
|
19
21
|
import { useMessagingContext } from '../providers/MessagingProvider'
|
|
@@ -22,6 +24,8 @@ import type { ChannelViewProps } from '../types'
|
|
|
22
24
|
import ActionButton from './ActionButton'
|
|
23
25
|
import { Avatar } from './Avatar'
|
|
24
26
|
import { CloseButton } from './CloseButton'
|
|
27
|
+
import { CustomDateSeparator } from './CustomDateSeparator'
|
|
28
|
+
import { CustomMessage } from './CustomMessage'
|
|
25
29
|
import { CustomMessageInput } from './CustomMessageInput'
|
|
26
30
|
import { CustomSystemMessage } from './CustomSystemMessage'
|
|
27
31
|
import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
|
|
@@ -487,31 +491,39 @@ const ChannelViewInner: React.FC<{
|
|
|
487
491
|
|
|
488
492
|
return (
|
|
489
493
|
<>
|
|
490
|
-
<
|
|
491
|
-
{
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
494
|
+
<WithComponents
|
|
495
|
+
overrides={{
|
|
496
|
+
Message: (props: MessageUIComponentProps) => (
|
|
497
|
+
<CustomMessage {...props} />
|
|
498
|
+
),
|
|
499
|
+
}}
|
|
500
|
+
>
|
|
501
|
+
<Window>
|
|
502
|
+
{/* Custom Channel Header */}
|
|
503
|
+
<div className="p-4">
|
|
504
|
+
<CustomChannelHeader
|
|
505
|
+
onBack={onBack}
|
|
506
|
+
showBackButton={showBackButton}
|
|
507
|
+
onShowInfo={handleShowInfo}
|
|
508
|
+
canShowInfo={Boolean(participant)}
|
|
509
|
+
/>
|
|
510
|
+
</div>
|
|
500
511
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
{/* Message List */}
|
|
513
|
+
<div className="flex-1 overflow-hidden relative">
|
|
514
|
+
<MessageList
|
|
515
|
+
hideDeletedMessages
|
|
516
|
+
hideNewMessageSeparator={false}
|
|
517
|
+
messageActions={undefined}
|
|
518
|
+
/>
|
|
519
|
+
</div>
|
|
509
520
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
521
|
+
{/* Message Input */}
|
|
522
|
+
<CustomMessageInput
|
|
523
|
+
renderActions={() => renderMessageInputActions?.(channel)}
|
|
524
|
+
/>
|
|
525
|
+
</Window>
|
|
526
|
+
</WithComponents>
|
|
515
527
|
|
|
516
528
|
{/* Channel Info Dialog */}
|
|
517
529
|
<ChannelInfoDialog
|
|
@@ -561,6 +573,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
561
573
|
MessageSystem={CustomSystemMessage}
|
|
562
574
|
EmptyStateIndicator={CustomChannelEmptyState}
|
|
563
575
|
LoadingIndicator={LoadingState}
|
|
576
|
+
DateSeparator={CustomDateSeparator}
|
|
564
577
|
>
|
|
565
578
|
<ChannelViewInner
|
|
566
579
|
onBack={onBack}
|
|
@@ -84,7 +84,7 @@ export const CustomLinkPreviewList = () => {
|
|
|
84
84
|
if (!showLinkPreviews) return null
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
|
-
<div className="
|
|
87
|
+
<div className="flex flex-col items-center w-full gap-2 mb-4">
|
|
88
88
|
{stateLinkPreviews.map((linkPreview) => (
|
|
89
89
|
<CustomLinkPreviewCard
|
|
90
90
|
key={linkPreview.og_scrape_url}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
Channel as ChannelType,
|
|
5
|
+
QueryChannelAPIResponse,
|
|
6
|
+
StreamChat,
|
|
7
|
+
} from 'stream-chat'
|
|
8
|
+
import { Channel, Chat, MessageList, Window } from 'stream-chat-react'
|
|
9
|
+
|
|
10
|
+
import { mockParticipants } from '../../stories/mocks'
|
|
11
|
+
|
|
12
|
+
import { CustomMessage } from './index'
|
|
13
|
+
|
|
14
|
+
const meta: Meta = {
|
|
15
|
+
title: 'Components/CustomMessage',
|
|
16
|
+
component: CustomMessage,
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: 'fullscreen',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
export default meta
|
|
22
|
+
|
|
23
|
+
const mockUser = {
|
|
24
|
+
id: 'storybook-user',
|
|
25
|
+
name: 'Storybook User',
|
|
26
|
+
image: 'https://i.pravatar.cc/150?img=1',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const createMockChannel = async (
|
|
30
|
+
client: StreamChat,
|
|
31
|
+
messages: TemplateProps['messages']
|
|
32
|
+
) => {
|
|
33
|
+
const participant = mockParticipants[0]
|
|
34
|
+
|
|
35
|
+
const mockMessages = messages.map((msg, index) => ({
|
|
36
|
+
...msg,
|
|
37
|
+
type: msg.type ?? ('regular' as const),
|
|
38
|
+
created_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
|
|
39
|
+
updated_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
|
|
40
|
+
html: `<p>${msg.text}</p>`,
|
|
41
|
+
attachments: [],
|
|
42
|
+
latest_reactions: [],
|
|
43
|
+
own_reactions: [],
|
|
44
|
+
reaction_counts: {},
|
|
45
|
+
reaction_scores: {},
|
|
46
|
+
reply_count: 0,
|
|
47
|
+
status: 'received',
|
|
48
|
+
cid: 'messaging:storybook-channel-1',
|
|
49
|
+
mentioned_users: [],
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
const channelData = {
|
|
53
|
+
members: [mockUser.id, participant.id],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const channel = client.channel(
|
|
57
|
+
'messaging',
|
|
58
|
+
'storybook-channel-1',
|
|
59
|
+
channelData
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
channel.watch = async () => {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
channel.state.messages = mockMessages as unknown as any[]
|
|
65
|
+
channel.state.members = {
|
|
66
|
+
[mockUser.id]: { user: mockUser, user_id: mockUser.id },
|
|
67
|
+
[participant.id]: { user: participant, user_id: participant.id },
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
channel: channelData,
|
|
71
|
+
members: [],
|
|
72
|
+
messages: mockMessages,
|
|
73
|
+
watchers: [],
|
|
74
|
+
pinned_messages: [],
|
|
75
|
+
duration: '0ms',
|
|
76
|
+
} as unknown as QueryChannelAPIResponse
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await channel.watch()
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignore errors in mock mode
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return channel
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface TemplateProps {
|
|
89
|
+
messages: Array<{
|
|
90
|
+
id: string
|
|
91
|
+
text: string
|
|
92
|
+
user: typeof mockUser | { id: string; name: string }
|
|
93
|
+
type?: 'regular' | 'system'
|
|
94
|
+
metadata?: {
|
|
95
|
+
custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
|
|
96
|
+
amount_text?: string
|
|
97
|
+
}
|
|
98
|
+
}>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const Template: StoryFn<TemplateProps> = ({ messages }) => {
|
|
102
|
+
const [client] = React.useState(() => {
|
|
103
|
+
const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
|
|
104
|
+
c.userID = mockUser.id
|
|
105
|
+
c.user = mockUser
|
|
106
|
+
return c
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const [channel, setChannel] = React.useState<ChannelType | null>(null)
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
createMockChannel(client, messages).then(setChannel)
|
|
113
|
+
}, [client, messages])
|
|
114
|
+
|
|
115
|
+
if (!channel) {
|
|
116
|
+
return <div className="p-4">Loading...</div>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Chat client={client}>
|
|
121
|
+
<div className="h-screen w-full bg-white">
|
|
122
|
+
<Channel channel={channel} Message={CustomMessage}>
|
|
123
|
+
<Window>
|
|
124
|
+
<MessageList />
|
|
125
|
+
</Window>
|
|
126
|
+
</Channel>
|
|
127
|
+
</div>
|
|
128
|
+
</Chat>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const participant = mockParticipants[0]
|
|
133
|
+
|
|
134
|
+
export const Default: StoryFn<TemplateProps> = Template.bind({})
|
|
135
|
+
Default.args = {
|
|
136
|
+
messages: [
|
|
137
|
+
{ id: 'msg-1', text: 'Hey, how are you?', user: participant },
|
|
138
|
+
{ id: 'msg-2', text: "I'm doing great, thanks!", user: mockUser },
|
|
139
|
+
{ id: 'msg-3', text: 'Awesome! Have a good day.', user: participant },
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})
|
|
144
|
+
WithTipTag.args = {
|
|
145
|
+
messages: [
|
|
146
|
+
{ id: 'msg-1', text: 'Love your content!', user: participant },
|
|
147
|
+
{
|
|
148
|
+
id: 'msg-2',
|
|
149
|
+
text: "Here's a tip for you! Keep up the great work.",
|
|
150
|
+
user: participant,
|
|
151
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
|
|
152
|
+
},
|
|
153
|
+
{ id: 'msg-3', text: 'Thank you so much! 🙏', user: mockUser },
|
|
154
|
+
],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const TipOnly: StoryFn<TemplateProps> = Template.bind({})
|
|
158
|
+
TipOnly.args = {
|
|
159
|
+
messages: [
|
|
160
|
+
{ id: 'msg-1', text: 'Hello!', user: participant },
|
|
161
|
+
{
|
|
162
|
+
id: 'msg-2',
|
|
163
|
+
text: '',
|
|
164
|
+
user: participant,
|
|
165
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
166
|
+
},
|
|
167
|
+
{ id: 'msg-3', text: 'Wow, thank you for the tip!', user: mockUser },
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const MixedTags: StoryFn<TemplateProps> = Template.bind({})
|
|
172
|
+
MixedTags.args = {
|
|
173
|
+
messages: [
|
|
174
|
+
{ id: 'msg-1', text: 'Regular message from a fan', user: participant },
|
|
175
|
+
{
|
|
176
|
+
id: 'msg-2',
|
|
177
|
+
text: 'I wanted to tip you for your amazing work!',
|
|
178
|
+
user: participant,
|
|
179
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
|
|
180
|
+
},
|
|
181
|
+
{ id: 'msg-3', text: 'Thank you!', user: mockUser },
|
|
182
|
+
{
|
|
183
|
+
id: 'msg-4',
|
|
184
|
+
text: '',
|
|
185
|
+
user: participant,
|
|
186
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$25.00' },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'msg-5',
|
|
190
|
+
text: 'Wow, a tip with no message! Thanks!',
|
|
191
|
+
user: mockUser,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'msg-6',
|
|
195
|
+
text: 'This is a paid message!',
|
|
196
|
+
user: participant,
|
|
197
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
|
|
198
|
+
},
|
|
199
|
+
{ id: 'msg-7', text: 'Got it, thanks!', user: mockUser },
|
|
200
|
+
{
|
|
201
|
+
id: 'msg-8',
|
|
202
|
+
text: 'This is from a chatbot!',
|
|
203
|
+
user: participant,
|
|
204
|
+
metadata: { custom_type: 'MESSAGE_CHATBOT' },
|
|
205
|
+
},
|
|
206
|
+
{ id: 'msg-9', text: 'Thanks for letting me know.', user: mockUser },
|
|
207
|
+
],
|
|
208
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { LocalMessage } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
import { MessageTag } from './MessageTag'
|
|
6
|
+
|
|
7
|
+
type ComponentProps = React.ComponentProps<typeof MessageTag>
|
|
8
|
+
|
|
9
|
+
const meta: Meta<ComponentProps> = {
|
|
10
|
+
title: 'Components/MessageTag',
|
|
11
|
+
component: MessageTag,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
export default meta
|
|
17
|
+
|
|
18
|
+
interface MockMessageOptions {
|
|
19
|
+
metadata?: {
|
|
20
|
+
custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
|
|
21
|
+
amount_text?: string
|
|
22
|
+
}
|
|
23
|
+
text?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const createMockMessage = (options?: MockMessageOptions): LocalMessage =>
|
|
27
|
+
({
|
|
28
|
+
id: 'msg-1',
|
|
29
|
+
text: options?.text ?? 'Hello world',
|
|
30
|
+
type: 'regular',
|
|
31
|
+
created_at: new Date(),
|
|
32
|
+
updated_at: new Date(),
|
|
33
|
+
metadata: options?.metadata,
|
|
34
|
+
}) as LocalMessage
|
|
35
|
+
|
|
36
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
37
|
+
return (
|
|
38
|
+
<div className="p-12">
|
|
39
|
+
<MessageTag {...args} />
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Tip: StoryFn<ComponentProps> = Template.bind({})
|
|
45
|
+
Tip.args = {
|
|
46
|
+
message: createMockMessage({
|
|
47
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
|
|
48
|
+
}),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const TipStandalone: StoryFn<ComponentProps> = Template.bind({})
|
|
52
|
+
TipStandalone.args = {
|
|
53
|
+
message: createMockMessage({
|
|
54
|
+
text: '',
|
|
55
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
56
|
+
}),
|
|
57
|
+
standalone: true,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Paid: StoryFn<ComponentProps> = Template.bind({})
|
|
61
|
+
Paid.args = {
|
|
62
|
+
message: createMockMessage({
|
|
63
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
|
|
64
|
+
}),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Chatbot: StoryFn<ComponentProps> = Template.bind({})
|
|
68
|
+
Chatbot.args = {
|
|
69
|
+
message: createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } }),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const NoTag: StoryFn<ComponentProps> = Template.bind({})
|
|
73
|
+
NoTag.args = {
|
|
74
|
+
message: createMockMessage(),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const AllVariants: StoryFn = () => {
|
|
78
|
+
return (
|
|
79
|
+
<div className="p-12 flex flex-col gap-4">
|
|
80
|
+
<div className="flex items-center gap-4">
|
|
81
|
+
<span className="text-sm w-32">Tip:</span>
|
|
82
|
+
<MessageTag
|
|
83
|
+
message={createMockMessage({
|
|
84
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$10.50' },
|
|
85
|
+
})}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex items-center gap-4">
|
|
89
|
+
<span className="text-sm w-32">Paid:</span>
|
|
90
|
+
<MessageTag
|
|
91
|
+
message={createMockMessage({
|
|
92
|
+
metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$25.00' },
|
|
93
|
+
})}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex items-center gap-4">
|
|
97
|
+
<span className="text-sm w-32">Tip (standalone):</span>
|
|
98
|
+
<MessageTag
|
|
99
|
+
message={createMockMessage({
|
|
100
|
+
text: '',
|
|
101
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
102
|
+
})}
|
|
103
|
+
standalone
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="flex items-center gap-4">
|
|
107
|
+
<span className="text-sm w-32">Chatbot:</span>
|
|
108
|
+
<MessageTag
|
|
109
|
+
message={createMockMessage({ metadata: { custom_type: 'MESSAGE_CHATBOT' } })}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex items-center gap-4">
|
|
113
|
+
<span className="text-sm w-32">No tag:</span>
|
|
114
|
+
<MessageTag message={createMockMessage()} />
|
|
115
|
+
<span className="text-xs text-stone">(renders nothing)</span>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { GiftIcon } from '@phosphor-icons/react'
|
|
2
|
+
import { LocalMessage } from 'stream-chat'
|
|
3
|
+
|
|
4
|
+
interface MessageTagProps {
|
|
5
|
+
message: LocalMessage
|
|
6
|
+
/** When true, renders as a standalone bubble instead of a small tag */
|
|
7
|
+
standalone?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SparkleIcon = () => (
|
|
11
|
+
<svg width="12" height="12" viewBox="0 0 10 10" fill="none">
|
|
12
|
+
<path
|
|
13
|
+
d="M10.003 5a.705.705 0 0 1-.469.67L6.7 6.7 5.67 9.535a.715.715 0 0 1-1.34 0L3.3 6.7.466 5.67a.715.715 0 0 1 0-1.34L3.3 3.3 4.33.466a.715.715 0 0 1 1.34 0L6.7 3.3l2.834 1.03a.705.705 0 0 1 .469.67"
|
|
14
|
+
fill="currentColor"
|
|
15
|
+
/>
|
|
16
|
+
</svg>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
/** Check if a message is a tip based on metadata */
|
|
20
|
+
export const isTipMessage = (message: LocalMessage): boolean => {
|
|
21
|
+
return message.metadata?.custom_type === 'MESSAGE_TIP'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Check if a message is a paid message based on metadata */
|
|
25
|
+
export const isPaidMessage = (message: LocalMessage): boolean => {
|
|
26
|
+
return message.metadata?.custom_type === 'MESSAGE_PAID'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Check if a message is a chatbot message based on metadata */
|
|
30
|
+
export const isChatbotMessage = (message: LocalMessage): boolean => {
|
|
31
|
+
return message.metadata?.custom_type === 'MESSAGE_CHATBOT'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Check if a message has a tip/paid tag (both render the same) */
|
|
35
|
+
export const isTipOrPaidMessage = (message: LocalMessage): boolean => {
|
|
36
|
+
return isTipMessage(message) || isPaidMessage(message)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Check if a message is a tip/paid-only message (no text) */
|
|
40
|
+
export const isTipOnlyMessage = (message: LocalMessage): boolean => {
|
|
41
|
+
return isTipOrPaidMessage(message) && !message.text?.trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const MessageTag = ({
|
|
45
|
+
message,
|
|
46
|
+
standalone = false,
|
|
47
|
+
}: MessageTagProps) => {
|
|
48
|
+
const isTipOrPaid = isTipOrPaidMessage(message)
|
|
49
|
+
const isChatbot = isChatbotMessage(message)
|
|
50
|
+
|
|
51
|
+
if (!isTipOrPaid && !isChatbot) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isTipOrPaid) {
|
|
56
|
+
const amountText = message.metadata?.amount_text
|
|
57
|
+
if (!amountText) return null
|
|
58
|
+
|
|
59
|
+
const className = standalone
|
|
60
|
+
? 'message-tip-standalone'
|
|
61
|
+
: 'message-tag message-tag--tip'
|
|
62
|
+
|
|
63
|
+
const label = standalone
|
|
64
|
+
? `${amountText} tip`
|
|
65
|
+
: `Delivered with ${amountText} tip`
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={className}>
|
|
69
|
+
<GiftIcon size={standalone ? 14 : 12} />
|
|
70
|
+
<span>{label}</span>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Chatbot tag
|
|
76
|
+
return (
|
|
77
|
+
<div className="message-tag message-tag--chatbot">
|
|
78
|
+
<span className="message-tag__icon" style={{ marginTop: -1 }}>
|
|
79
|
+
<SparkleIcon />
|
|
80
|
+
</span>
|
|
81
|
+
<span className="message-tag__label">Chatbot</span>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|