@linktr.ee/messaging-react 1.12.7 → 1.12.8-rc-1765974383
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 +20 -0
- package/dist/index.js +919 -713
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelView.tsx +34 -23
- package/src/components/CustomMessage/CustomMessage.stories.tsx +220 -0
- package/src/components/CustomMessage/MessageTag.stories.tsx +80 -0
- package/src/components/CustomMessage/MessageTag.tsx +68 -0
- package/src/components/CustomMessage/index.tsx +260 -0
- package/src/components/CustomSystemMessage/index.tsx +10 -1
- package/src/stream-custom-data.ts +20 -0
- package/src/styles.css +34 -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,7 @@ 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 { CustomMessage } from './CustomMessage'
|
|
25
28
|
import { CustomMessageInput } from './CustomMessageInput'
|
|
26
29
|
import { CustomSystemMessage } from './CustomSystemMessage'
|
|
27
30
|
import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
|
|
@@ -487,31 +490,39 @@ const ChannelViewInner: React.FC<{
|
|
|
487
490
|
|
|
488
491
|
return (
|
|
489
492
|
<>
|
|
490
|
-
<
|
|
491
|
-
{
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
493
|
+
<WithComponents
|
|
494
|
+
overrides={{
|
|
495
|
+
Message: (props: MessageUIComponentProps) => (
|
|
496
|
+
<CustomMessage {...props} />
|
|
497
|
+
),
|
|
498
|
+
}}
|
|
499
|
+
>
|
|
500
|
+
<Window>
|
|
501
|
+
{/* Custom Channel Header */}
|
|
502
|
+
<div className="p-4">
|
|
503
|
+
<CustomChannelHeader
|
|
504
|
+
onBack={onBack}
|
|
505
|
+
showBackButton={showBackButton}
|
|
506
|
+
onShowInfo={handleShowInfo}
|
|
507
|
+
canShowInfo={Boolean(participant)}
|
|
508
|
+
/>
|
|
509
|
+
</div>
|
|
500
510
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
511
|
+
{/* Message List */}
|
|
512
|
+
<div className="flex-1 overflow-hidden relative">
|
|
513
|
+
<MessageList
|
|
514
|
+
hideDeletedMessages
|
|
515
|
+
hideNewMessageSeparator={false}
|
|
516
|
+
messageActions={undefined}
|
|
517
|
+
/>
|
|
518
|
+
</div>
|
|
509
519
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
520
|
+
{/* Message Input */}
|
|
521
|
+
<CustomMessageInput
|
|
522
|
+
renderActions={() => renderMessageInputActions?.(channel)}
|
|
523
|
+
/>
|
|
524
|
+
</Window>
|
|
525
|
+
</WithComponents>
|
|
515
526
|
|
|
516
527
|
{/* Channel Info Dialog */}
|
|
517
528
|
<ChannelInfoDialog
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
import { CustomSystemMessage } from '../CustomSystemMessage'
|
|
12
|
+
|
|
13
|
+
import { CustomMessage } from './index'
|
|
14
|
+
|
|
15
|
+
const meta: Meta = {
|
|
16
|
+
title: 'Components/CustomMessage',
|
|
17
|
+
component: CustomMessage,
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: 'fullscreen',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
export default meta
|
|
23
|
+
|
|
24
|
+
const mockUser = {
|
|
25
|
+
id: 'storybook-user',
|
|
26
|
+
name: 'Storybook User',
|
|
27
|
+
image: 'https://i.pravatar.cc/150?img=1',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const createMockChannel = async (
|
|
31
|
+
client: StreamChat,
|
|
32
|
+
messages: TemplateProps['messages']
|
|
33
|
+
) => {
|
|
34
|
+
const participant = mockParticipants[0]
|
|
35
|
+
|
|
36
|
+
const mockMessages = messages.map((msg, index) => ({
|
|
37
|
+
...msg,
|
|
38
|
+
type: msg.type ?? ('regular' as const),
|
|
39
|
+
created_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
|
|
40
|
+
updated_at: new Date(Date.now() - 1000 * 60 * (messages.length - index)),
|
|
41
|
+
html: `<p>${msg.text}</p>`,
|
|
42
|
+
attachments: [],
|
|
43
|
+
latest_reactions: [],
|
|
44
|
+
own_reactions: [],
|
|
45
|
+
reaction_counts: {},
|
|
46
|
+
reaction_scores: {},
|
|
47
|
+
reply_count: 0,
|
|
48
|
+
status: 'received',
|
|
49
|
+
cid: 'messaging:storybook-channel-1',
|
|
50
|
+
mentioned_users: [],
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
const channelData = {
|
|
54
|
+
members: [mockUser.id, participant.id],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const channel = client.channel(
|
|
58
|
+
'messaging',
|
|
59
|
+
'storybook-channel-1',
|
|
60
|
+
channelData
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
channel.watch = async () => {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
channel.state.messages = mockMessages as unknown as any[]
|
|
66
|
+
channel.state.members = {
|
|
67
|
+
[mockUser.id]: { user: mockUser, user_id: mockUser.id },
|
|
68
|
+
[participant.id]: { user: participant, user_id: participant.id },
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
channel: channelData,
|
|
72
|
+
members: [],
|
|
73
|
+
messages: mockMessages,
|
|
74
|
+
watchers: [],
|
|
75
|
+
pinned_messages: [],
|
|
76
|
+
duration: '0ms',
|
|
77
|
+
} as unknown as QueryChannelAPIResponse
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await channel.watch()
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore errors in mock mode
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return channel
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface TemplateProps {
|
|
90
|
+
messages: Array<{
|
|
91
|
+
id: string
|
|
92
|
+
text: string
|
|
93
|
+
user: typeof mockUser | { id: string; name: string }
|
|
94
|
+
type?: 'regular' | 'system'
|
|
95
|
+
tag_type?: 'paid-priority' | 'tip'
|
|
96
|
+
amount?: number
|
|
97
|
+
currency?: string
|
|
98
|
+
hide_date?: boolean
|
|
99
|
+
system_type?: 'priority'
|
|
100
|
+
}>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const Template: StoryFn<TemplateProps> = ({ messages }) => {
|
|
104
|
+
const [client] = React.useState(() => {
|
|
105
|
+
const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
|
|
106
|
+
c.userID = mockUser.id
|
|
107
|
+
c.user = mockUser
|
|
108
|
+
return c
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const [channel, setChannel] = React.useState<ChannelType | null>(null)
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
createMockChannel(client, messages).then(setChannel)
|
|
115
|
+
}, [client, messages])
|
|
116
|
+
|
|
117
|
+
if (!channel) {
|
|
118
|
+
return <div className="p-4">Loading...</div>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Chat client={client}>
|
|
123
|
+
<div className="h-screen w-full bg-white">
|
|
124
|
+
<Channel
|
|
125
|
+
channel={channel}
|
|
126
|
+
Message={CustomMessage}
|
|
127
|
+
MessageSystem={CustomSystemMessage}
|
|
128
|
+
>
|
|
129
|
+
<Window>
|
|
130
|
+
<MessageList />
|
|
131
|
+
</Window>
|
|
132
|
+
</Channel>
|
|
133
|
+
</div>
|
|
134
|
+
</Chat>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const participant = mockParticipants[0]
|
|
139
|
+
|
|
140
|
+
export const Default: StoryFn<TemplateProps> = Template.bind({})
|
|
141
|
+
Default.args = {
|
|
142
|
+
messages: [
|
|
143
|
+
{ id: 'msg-1', text: 'Hey, how are you?', user: participant },
|
|
144
|
+
{ id: 'msg-2', text: "I'm doing great, thanks!", user: mockUser },
|
|
145
|
+
{ id: 'msg-3', text: 'Awesome! Have a good day.', user: participant },
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})
|
|
150
|
+
WithTipTag.args = {
|
|
151
|
+
messages: [
|
|
152
|
+
{ id: 'msg-1', text: 'Love your content!', user: participant },
|
|
153
|
+
{
|
|
154
|
+
id: 'msg-2',
|
|
155
|
+
text: "Here's a tip for you! Keep up the great work.",
|
|
156
|
+
user: participant,
|
|
157
|
+
tag_type: 'tip',
|
|
158
|
+
amount: 5.5,
|
|
159
|
+
currency: 'USD',
|
|
160
|
+
},
|
|
161
|
+
{ id: 'msg-3', text: 'Thank you so much! 🙏', user: mockUser },
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const WithPriorityTag: StoryFn<TemplateProps> = Template.bind({})
|
|
166
|
+
WithPriorityTag.args = {
|
|
167
|
+
messages: [
|
|
168
|
+
{
|
|
169
|
+
id: 'msg-1',
|
|
170
|
+
text: 'This is a priority message that should stand out!',
|
|
171
|
+
user: participant,
|
|
172
|
+
tag_type: 'paid-priority',
|
|
173
|
+
},
|
|
174
|
+
{ id: 'msg-2', text: 'Thanks for the priority message!', user: mockUser },
|
|
175
|
+
],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const MixedTags: StoryFn<TemplateProps> = Template.bind({})
|
|
179
|
+
MixedTags.args = {
|
|
180
|
+
messages: [
|
|
181
|
+
{ id: 'msg-1', text: 'Regular message from a fan', user: participant },
|
|
182
|
+
{
|
|
183
|
+
id: 'msg-2',
|
|
184
|
+
text: 'I wanted to tip you for your amazing work!',
|
|
185
|
+
user: participant,
|
|
186
|
+
custom_data: { tag_type: 'tip', amount: 5.5, currency: 'USD' },
|
|
187
|
+
},
|
|
188
|
+
{ id: 'msg-3', text: 'Thank you!', user: mockUser },
|
|
189
|
+
{
|
|
190
|
+
id: 'msg-4',
|
|
191
|
+
text: 'Please check this priority question ASAP!',
|
|
192
|
+
user: participant,
|
|
193
|
+
custom_data: { tag_type: 'paid-priority' },
|
|
194
|
+
},
|
|
195
|
+
{ id: 'msg-5', text: 'Sure, let me look into that.', user: mockUser },
|
|
196
|
+
{ id: 'msg-6', text: 'Another regular follow-up.', user: participant },
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const WithSystemMessage: StoryFn<TemplateProps> = Template.bind({})
|
|
201
|
+
WithSystemMessage.args = {
|
|
202
|
+
messages: [
|
|
203
|
+
{ id: 'msg-1', text: 'Hey there!', user: participant },
|
|
204
|
+
{ id: 'msg-2', text: 'Hi! How can I help?', user: mockUser },
|
|
205
|
+
{
|
|
206
|
+
id: 'msg-3',
|
|
207
|
+
text: 'I have an urgent question about my order!',
|
|
208
|
+
user: participant,
|
|
209
|
+
custom_data: { tag_type: 'paid-priority' },
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'msg-4',
|
|
213
|
+
text: 'Storybook User will respond to your priority message within 24 hours.',
|
|
214
|
+
user: { id: 'system', name: 'System' },
|
|
215
|
+
type: 'system',
|
|
216
|
+
system_type: 'priority',
|
|
217
|
+
hide_date: true,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
const createMockMessage = (
|
|
19
|
+
tagType?: 'tip' | 'paid-priority',
|
|
20
|
+
options?: { amount?: number; currency?: string }
|
|
21
|
+
): LocalMessage =>
|
|
22
|
+
({
|
|
23
|
+
id: 'msg-1',
|
|
24
|
+
text: 'Hello world',
|
|
25
|
+
type: 'regular',
|
|
26
|
+
created_at: new Date(),
|
|
27
|
+
updated_at: new Date(),
|
|
28
|
+
tag_type: tagType,
|
|
29
|
+
...options,
|
|
30
|
+
}) as LocalMessage
|
|
31
|
+
|
|
32
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
33
|
+
return (
|
|
34
|
+
<div className="p-12">
|
|
35
|
+
<MessageTag {...args} />
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Tip: StoryFn<ComponentProps> = Template.bind({})
|
|
41
|
+
Tip.args = {
|
|
42
|
+
message: createMockMessage('tip', { amount: 5.5, currency: 'USD' }),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const TipWholeUnit: StoryFn<ComponentProps> = Template.bind({})
|
|
46
|
+
TipWholeUnit.args = {
|
|
47
|
+
message: createMockMessage('tip', { amount: 5, currency: 'USD' }),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const PaidPriority: StoryFn<ComponentProps> = Template.bind({})
|
|
51
|
+
PaidPriority.args = {
|
|
52
|
+
message: createMockMessage('paid-priority'),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const NoTag: StoryFn<ComponentProps> = Template.bind({})
|
|
56
|
+
NoTag.args = {
|
|
57
|
+
message: createMockMessage(),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const AllVariants: StoryFn = () => {
|
|
61
|
+
return (
|
|
62
|
+
<div className="p-12 flex flex-col gap-4">
|
|
63
|
+
<div className="flex items-center gap-4">
|
|
64
|
+
<span className="text-sm w-24">Tip:</span>
|
|
65
|
+
<MessageTag
|
|
66
|
+
message={createMockMessage('tip', { amount: 10.5, currency: 'USD' })}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex items-center gap-4">
|
|
70
|
+
<span className="text-sm w-24">Priority:</span>
|
|
71
|
+
<MessageTag message={createMockMessage('paid-priority')} />
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex items-center gap-4">
|
|
74
|
+
<span className="text-sm w-24">No tag:</span>
|
|
75
|
+
<MessageTag message={createMockMessage()} />
|
|
76
|
+
<span className="text-xs text-stone">(renders nothing)</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CoinVerticalIcon } from '@phosphor-icons/react'
|
|
2
|
+
import { LocalMessage } from 'stream-chat'
|
|
3
|
+
|
|
4
|
+
interface MessageTagProps {
|
|
5
|
+
message: LocalMessage
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SparkleIcon = () => (
|
|
9
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
|
10
|
+
<path
|
|
11
|
+
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"
|
|
12
|
+
fill="currentColor"
|
|
13
|
+
/>
|
|
14
|
+
</svg>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const formatCurrency = (amount: number, currency: string): string => {
|
|
18
|
+
try {
|
|
19
|
+
const hasDecimals = amount % 1 !== 0
|
|
20
|
+
return new Intl.NumberFormat(undefined, {
|
|
21
|
+
style: 'currency',
|
|
22
|
+
currency: currency || 'USD',
|
|
23
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
24
|
+
maximumFractionDigits: 2,
|
|
25
|
+
currencyDisplay: 'narrowSymbol',
|
|
26
|
+
}).format(amount)
|
|
27
|
+
} catch {
|
|
28
|
+
return `${amount} ${currency}`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TAG_CONFIG: Record<
|
|
33
|
+
string,
|
|
34
|
+
{ label: (amount: number, currency: string) => string; icon: React.ReactNode }
|
|
35
|
+
> = {
|
|
36
|
+
tip: {
|
|
37
|
+
label: (amount: number, currency: string) =>
|
|
38
|
+
`Sent with ${formatCurrency(amount, currency)} tip`,
|
|
39
|
+
icon: <CoinVerticalIcon size={12} />,
|
|
40
|
+
},
|
|
41
|
+
'paid-priority': {
|
|
42
|
+
label: () => 'Priority',
|
|
43
|
+
icon: <SparkleIcon />,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const MessageTag = ({ message }: MessageTagProps) => {
|
|
48
|
+
const tagType = message.tag_type
|
|
49
|
+
|
|
50
|
+
if (!tagType) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (tagType === 'tip' && !message.amount) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const config = TAG_CONFIG[tagType]
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={`message-tag message-tag--${tagType}`}>
|
|
62
|
+
<span className="message-tag__icon">{config.icon}</span>
|
|
63
|
+
<span className="message-tag__label">
|
|
64
|
+
{config.label(message.amount ?? 0, message.currency ?? '')}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|