@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.12.7",
3
+ "version": "1.12.8-rc-1765974383",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- <Window>
491
- {/* Custom Channel Header */}
492
- <div className="p-4">
493
- <CustomChannelHeader
494
- onBack={onBack}
495
- showBackButton={showBackButton}
496
- onShowInfo={handleShowInfo}
497
- canShowInfo={Boolean(participant)}
498
- />
499
- </div>
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
- {/* Message List */}
502
- <div className="flex-1 overflow-hidden relative">
503
- <MessageList
504
- hideDeletedMessages
505
- hideNewMessageSeparator={false}
506
- messageActions={[]}
507
- />
508
- </div>
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
- {/* Message Input */}
511
- <CustomMessageInput
512
- renderActions={() => renderMessageInputActions?.(channel)}
513
- />
514
- </Window>
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
+ }