@linktr.ee/messaging-react 1.18.0 → 1.19.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/index.d.ts +7 -0
- package/dist/index.js +872 -868
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +93 -1
- package/src/components/ChannelList/CustomChannelPreview.tsx +18 -4
- package/src/components/ChannelList/index.tsx +3 -1
- package/src/components/ChannelView.tsx +14 -14
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/types.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Channel,
|
|
5
|
+
CustomMessageData,
|
|
6
|
+
LocalMessage,
|
|
7
|
+
StreamChat,
|
|
8
|
+
} from 'stream-chat'
|
|
4
9
|
|
|
5
10
|
import CustomChannelPreview from './CustomChannelPreview'
|
|
6
11
|
|
|
@@ -220,6 +225,93 @@ ChatbotMessage.args = {
|
|
|
220
225
|
}),
|
|
221
226
|
}
|
|
222
227
|
|
|
228
|
+
const renderMessagePreviewWithMarkers = (
|
|
229
|
+
message: LocalMessage | undefined,
|
|
230
|
+
defaultPreview?: string
|
|
231
|
+
): React.ReactNode => {
|
|
232
|
+
if (!message) return defaultPreview
|
|
233
|
+
|
|
234
|
+
const metadata = (
|
|
235
|
+
message as LocalMessage & { metadata?: Record<string, unknown> }
|
|
236
|
+
).metadata
|
|
237
|
+
|
|
238
|
+
if (metadata?.custom_type === 'MESSAGE_URGENT') {
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
<span className="text-red-600 font-bold">🔥 URGENT:</span>{' '}
|
|
242
|
+
{defaultPreview}
|
|
243
|
+
</>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (metadata?.is_priority) {
|
|
248
|
+
return `⚡ ${defaultPreview}`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (metadata?.is_vip) {
|
|
252
|
+
return `👑 ${defaultPreview}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return defaultPreview
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const WithCustomMessagePreview: StoryFn = () => {
|
|
259
|
+
const channels = [
|
|
260
|
+
createMockChannel({
|
|
261
|
+
id: 'channel-urgent',
|
|
262
|
+
participantName: 'Alice Urgent',
|
|
263
|
+
participantId: 'participant-urgent',
|
|
264
|
+
participantImage: 'https://i.pravatar.cc/150?img=36',
|
|
265
|
+
lastMessageText: 'Critical issue needs immediate attention!',
|
|
266
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60), // 1 minute ago
|
|
267
|
+
lastMessageMetadata: { custom_type: 'MESSAGE_URGENT' },
|
|
268
|
+
unreadCount: 3,
|
|
269
|
+
}),
|
|
270
|
+
createMockChannel({
|
|
271
|
+
id: 'channel-priority',
|
|
272
|
+
participantName: 'Bob Priority',
|
|
273
|
+
participantId: 'participant-priority',
|
|
274
|
+
participantImage: 'https://i.pravatar.cc/150?img=37',
|
|
275
|
+
lastMessageText: 'High priority message',
|
|
276
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
|
|
277
|
+
lastMessageMetadata: { is_priority: true },
|
|
278
|
+
unreadCount: 1,
|
|
279
|
+
}),
|
|
280
|
+
createMockChannel({
|
|
281
|
+
id: 'channel-vip',
|
|
282
|
+
participantName: 'Carol VIP',
|
|
283
|
+
participantId: 'participant-vip',
|
|
284
|
+
participantImage: 'https://i.pravatar.cc/150?img=38',
|
|
285
|
+
lastMessageText: 'Thanks for the support!',
|
|
286
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 10), // 10 minutes ago
|
|
287
|
+
lastMessageMetadata: { is_vip: true },
|
|
288
|
+
}),
|
|
289
|
+
createMockChannel({
|
|
290
|
+
id: 'channel-normal',
|
|
291
|
+
participantName: 'David Normal',
|
|
292
|
+
participantId: 'participant-normal',
|
|
293
|
+
participantImage: 'https://i.pravatar.cc/150?img=39',
|
|
294
|
+
lastMessageText: 'Hey, how are you?',
|
|
295
|
+
lastMessageTime: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
|
|
296
|
+
}),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div className="w-[360px] bg-chalk border border-sand overflow-hidden">
|
|
301
|
+
{channels.map((channel) => (
|
|
302
|
+
<CustomChannelPreview
|
|
303
|
+
key={channel.id}
|
|
304
|
+
channel={channel}
|
|
305
|
+
onChannelSelect={(channel) => {
|
|
306
|
+
console.log('Channel selected:', channel.id)
|
|
307
|
+
}}
|
|
308
|
+
renderMessagePreview={renderMessagePreviewWithMarkers}
|
|
309
|
+
/>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
223
315
|
export const SelectedWithUnread: StoryFn<ComponentProps> = Template.bind({})
|
|
224
316
|
SelectedWithUnread.args = {
|
|
225
317
|
channel: createMockChannel({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import { Channel } from 'stream-chat'
|
|
3
|
+
import { Channel, LocalMessage } from 'stream-chat'
|
|
4
4
|
import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
5
5
|
|
|
6
6
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
@@ -11,13 +11,24 @@ type CustomChannelPreviewProps = ChannelPreviewUIComponentProps & {
|
|
|
11
11
|
selectedChannel?: Channel | null
|
|
12
12
|
onChannelSelect: (channel: Channel) => void
|
|
13
13
|
debug?: boolean
|
|
14
|
+
renderMessagePreview?: (
|
|
15
|
+
message: LocalMessage | undefined,
|
|
16
|
+
defaultPreview?: string
|
|
17
|
+
) => React.ReactNode
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Custom channel preview that handles selection
|
|
18
22
|
*/
|
|
19
23
|
const CustomChannelPreview = React.memo<CustomChannelPreviewProps>(
|
|
20
|
-
({
|
|
24
|
+
({
|
|
25
|
+
channel,
|
|
26
|
+
selectedChannel,
|
|
27
|
+
onChannelSelect,
|
|
28
|
+
debug = false,
|
|
29
|
+
unread,
|
|
30
|
+
renderMessagePreview,
|
|
31
|
+
}) => {
|
|
21
32
|
const isSelected = selectedChannel?.id === channel?.id
|
|
22
33
|
|
|
23
34
|
const handleClick = () => {
|
|
@@ -67,6 +78,10 @@ const CustomChannelPreview = React.memo<CustomChannelPreviewProps>(
|
|
|
67
78
|
? isChatbotMessage(lastMessage)
|
|
68
79
|
: false
|
|
69
80
|
|
|
81
|
+
const messagePreview = renderMessagePreview
|
|
82
|
+
? renderMessagePreview(lastMessage, lastMessageText)
|
|
83
|
+
: `${isLastMessageFromChatbot ? '✨ ' : ''}${lastMessageText}`
|
|
84
|
+
|
|
70
85
|
// Use the unread prop passed by Stream Chat (reactive and updates automatically)
|
|
71
86
|
const unreadCount = unread ?? 0
|
|
72
87
|
|
|
@@ -124,8 +139,7 @@ const CustomChannelPreview = React.memo<CustomChannelPreviewProps>(
|
|
|
124
139
|
{/* Message and unread badge row */}
|
|
125
140
|
<div className="flex items-center justify-between gap-2 min-w-0">
|
|
126
141
|
<p className="text-xs text-stone mr-2 flex-1 line-clamp-2">
|
|
127
|
-
{
|
|
128
|
-
{lastMessageText}
|
|
142
|
+
{messagePreview}
|
|
129
143
|
</p>
|
|
130
144
|
{unreadCount > 0 && (
|
|
131
145
|
<span className="bg-[#7f22fe] text-white text-xs px-2 py-0.5 rounded-full min-w-[20px] text-center flex-shrink-0">
|
|
@@ -18,6 +18,7 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
18
18
|
filters,
|
|
19
19
|
className,
|
|
20
20
|
customEmptyStateIndicator,
|
|
21
|
+
renderMessagePreview,
|
|
21
22
|
}) => {
|
|
22
23
|
// Track renders
|
|
23
24
|
const renderCountRef = React.useRef(0)
|
|
@@ -42,10 +43,11 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
42
43
|
selectedChannel={selectedChannel}
|
|
43
44
|
onChannelSelect={onChannelSelect}
|
|
44
45
|
debug={debug}
|
|
46
|
+
renderMessagePreview={renderMessagePreview}
|
|
45
47
|
/>
|
|
46
48
|
)
|
|
47
49
|
return Preview
|
|
48
|
-
}, [selectedChannel, onChannelSelect, debug])
|
|
50
|
+
}, [selectedChannel, onChannelSelect, debug, renderMessagePreview])
|
|
49
51
|
|
|
50
52
|
return (
|
|
51
53
|
<div
|
|
@@ -114,20 +114,20 @@ const CustomChannelHeader: React.FC<{
|
|
|
114
114
|
|
|
115
115
|
return (
|
|
116
116
|
<div className="@container">
|
|
117
|
-
<div className="
|
|
117
|
+
<div className="grid grid-cols-[1fr_auto_1fr] w-full items-center @lg:hidden">
|
|
118
118
|
<div className="flex items-center gap-2">
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
{showBackButton && (
|
|
120
|
+
<button
|
|
121
|
+
className={classNames(
|
|
122
|
+
'size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center'
|
|
123
|
+
)}
|
|
124
|
+
onClick={onBack || (() => {})}
|
|
125
|
+
type="button"
|
|
126
|
+
aria-label="Back to conversations"
|
|
127
|
+
>
|
|
128
|
+
<ArrowLeftIcon className="size-5 text-black/90" />
|
|
129
|
+
</button>
|
|
130
|
+
)}
|
|
131
131
|
</div>
|
|
132
132
|
<div className="flex flex-col gap-1 items-center">
|
|
133
133
|
<Avatar
|
|
@@ -140,7 +140,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
140
140
|
{participantName}
|
|
141
141
|
</h1>
|
|
142
142
|
</div>
|
|
143
|
-
<div className="flex items-center gap-2">
|
|
143
|
+
<div className="flex justify-end items-center gap-2">
|
|
144
144
|
{showStarButton && (
|
|
145
145
|
<button
|
|
146
146
|
className="size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center"
|
|
@@ -34,6 +34,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
34
34
|
messageMetadata,
|
|
35
35
|
onMessageSent,
|
|
36
36
|
showStarButton = false,
|
|
37
|
+
renderMessagePreview,
|
|
37
38
|
}) => {
|
|
38
39
|
const {
|
|
39
40
|
service,
|
|
@@ -449,6 +450,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
449
450
|
selectedChannel={selectedChannel || undefined}
|
|
450
451
|
filters={channelFilters}
|
|
451
452
|
customEmptyStateIndicator={channelListCustomEmptyStateIndicator}
|
|
453
|
+
renderMessagePreview={renderMessagePreview}
|
|
452
454
|
/>
|
|
453
455
|
</div>
|
|
454
456
|
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import type {
|
|
6
6
|
Channel,
|
|
7
7
|
ChannelFilters,
|
|
8
|
+
LocalMessage,
|
|
8
9
|
SendMessageAPIResponse,
|
|
9
10
|
} from 'stream-chat'
|
|
10
11
|
import { EmptyStateIndicatorProps } from 'stream-chat-react'
|
|
@@ -67,6 +68,10 @@ export interface ChannelListProps {
|
|
|
67
68
|
className?: string
|
|
68
69
|
filters: ChannelFilters
|
|
69
70
|
customEmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>
|
|
71
|
+
renderMessagePreview?: (
|
|
72
|
+
message: LocalMessage | undefined,
|
|
73
|
+
defaultPreview?: string
|
|
74
|
+
) => React.ReactNode
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
/**
|
|
@@ -198,6 +203,15 @@ export interface MessagingShellProps extends ChannelViewPassthroughProps {
|
|
|
198
203
|
* Useful for showing a custom empty state indicator when the channel list is empty.
|
|
199
204
|
*/
|
|
200
205
|
channelListCustomEmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Custom render function for message preview text in channel list.
|
|
209
|
+
* Receives the message and a default preview string.
|
|
210
|
+
*/
|
|
211
|
+
renderMessagePreview?: (
|
|
212
|
+
message: LocalMessage | undefined,
|
|
213
|
+
defaultPreview?: string
|
|
214
|
+
) => React.ReactNode
|
|
201
215
|
}
|
|
202
216
|
|
|
203
217
|
/**
|