@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,11 @@
1
1
  import type { Meta, StoryFn } from '@storybook/react'
2
2
  import React from 'react'
3
- import { Channel, CustomMessageData, LocalMessage, StreamChat } from 'stream-chat'
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
- ({ channel, selectedChannel, onChannelSelect, debug = false, unread }) => {
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
- {isLastMessageFromChatbot && '✨ '}
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="flex justify-between items-center @lg:hidden">
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
- <button
120
- className={classNames(
121
- 'size-10 rounded-full bg-[#F1F0EE] flex items-center justify-center',
122
- !showBackButton && 'invisible'
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
- <div className="size-10" />
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
  /**