@linktr.ee/messaging-react 1.40.2 → 2.0.1

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.
Files changed (31) hide show
  1. package/dist/{Card-A0lkei-S.js → Card-BKP9ml9O.js} +2 -2
  2. package/dist/{Card-A0lkei-S.js.map → Card-BKP9ml9O.js.map} +1 -1
  3. package/dist/{Card-DXoAKkv0.js → Card-Bk_4lVzP.js} +2 -2
  4. package/dist/{Card-DXoAKkv0.js.map → Card-Bk_4lVzP.js.map} +1 -1
  5. package/dist/assets/index.css +1 -1
  6. package/dist/index-Bex7eg3v.js +3092 -0
  7. package/dist/index-Bex7eg3v.js.map +1 -0
  8. package/dist/index.d.ts +22 -2
  9. package/dist/index.js +12 -10
  10. package/package.json +2 -2
  11. package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +2 -14
  12. package/src/components/ChannelInfoDialog/index.tsx +4 -8
  13. package/src/components/ChannelList/ChannelListContext.tsx +2 -0
  14. package/src/components/ChannelList/CustomChannelPreview.tsx +14 -3
  15. package/src/components/ChannelList/index.tsx +9 -1
  16. package/src/components/ChannelView.test.tsx +11 -0
  17. package/src/components/ChannelView.tsx +44 -33
  18. package/src/components/CustomMessage/index.tsx +22 -4
  19. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +57 -17
  20. package/src/components/CustomTypingIndicator/CustomTypingIndicator.test.tsx +187 -0
  21. package/src/components/CustomTypingIndicator/DmAgentContext.ts +3 -0
  22. package/src/components/CustomTypingIndicator/index.tsx +101 -37
  23. package/src/components/MessagingShell/index.tsx +4 -4
  24. package/src/index.ts +13 -2
  25. package/src/stories/mocks.tsx +2 -9
  26. package/src/styles.css +7 -0
  27. package/src/types.ts +11 -1
  28. package/src/utils/getMessageDisplayText.test.ts +44 -0
  29. package/src/utils/getMessageDisplayText.ts +27 -0
  30. package/dist/index-B_PLgcDi.js +0 -2994
  31. package/dist/index-B_PLgcDi.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -101,6 +101,11 @@ export declare interface ChannelListProps {
101
101
  sort?: ChannelSort;
102
102
  customEmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>;
103
103
  renderMessagePreview?: (message: LocalMessage | undefined, defaultPreview?: string) => React.ReactNode;
104
+ /**
105
+ * Language code used to pick translated message text from Stream Chat i18n.
106
+ * Falls back to message.text when no matching translation exists.
107
+ */
108
+ viewerLanguage?: string;
104
109
  }
105
110
 
106
111
  /**
@@ -112,7 +117,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
112
117
  * Props that MessagingShell passes through to ChannelView.
113
118
  * ChannelViewProps is the source of truth for these props.
114
119
  */
115
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onDeleteConversationClick' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'messageMetadata' | 'onMessageSent' | 'showStarButton' | 'chatbotVotingEnabled' | 'renderChannelBanner' | 'customProfileContent' | 'customChannelActions' | 'renderMessage' | 'sendButton'>;
120
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onDeleteConversationClick' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'messageMetadata' | 'onMessageSent' | 'showStarButton' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customProfileContent' | 'customChannelActions' | 'renderMessage' | 'sendButton'>;
116
121
 
117
122
  /**
118
123
  * ChannelView component props
@@ -185,6 +190,11 @@ export declare interface ChannelViewProps {
185
190
  * Defaults to false.
186
191
  */
187
192
  chatbotVotingEnabled?: boolean;
193
+ /**
194
+ * Language code used to pick translated message text from Stream Chat i18n.
195
+ * Falls back to message.text when no matching translation exists.
196
+ */
197
+ viewerLanguage?: string;
188
198
  /**
189
199
  * Custom render function for a banner/card component that renders
190
200
  * between the channel header and message list.
@@ -283,6 +293,11 @@ export declare interface FaqListProps {
283
293
  */
284
294
  export declare const formatRelativeTime: (date: Date) => string;
285
295
 
296
+ export declare function getMessageDisplayText({ message, viewerLanguage, }: {
297
+ message?: MessageWithI18n | null;
298
+ viewerLanguage?: string;
299
+ }): string | undefined;
300
+
286
301
  export declare function isLinkAttachment(a: Attachment): boolean;
287
302
 
288
303
  export declare const LockedAttachment: {
@@ -358,6 +373,10 @@ declare interface MessageVoteButtonsProps {
358
373
  onVoteDown: () => void;
359
374
  }
360
375
 
376
+ declare type MessageWithI18n = Pick<LocalMessage, 'text'> & {
377
+ i18n?: Record<string, string> | null;
378
+ };
379
+
361
380
  /**
362
381
  * Messaging capabilities configuration
363
382
  */
@@ -462,13 +481,14 @@ export declare interface MessagingShellProps extends ChannelViewPassthroughProps
462
481
  renderMessagePreview?: (message: LocalMessage | undefined, defaultPreview?: string) => React.ReactNode;
463
482
  }
464
483
 
484
+ export declare function normalizeLanguageCode(language?: string): string | undefined;
485
+
465
486
  /**
466
487
  * Generic participant interface for different host environments
467
488
  */
468
489
  export declare interface Participant {
469
490
  id: string;
470
491
  name: string;
471
- email?: string;
472
492
  image?: string;
473
493
  username?: string;
474
494
  phone?: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b as e, c as t, C as i, d as n, e as o, f as g, F as m, g as r, L as M, M as l, h, i as u, j as L, k as c, l as d, r as v, m as C, u as A, n as k, o as F } from "./index-B_PLgcDi.js";
1
+ import { b as e, c as t, C as i, d as n, e as o, f as g, F as m, g as r, L as M, M as l, h as u, i as h, j as L, k as d, l as C, m as c, n as v, r as A, o as k, u as p, p as F, q as f } from "./index-Bex7eg3v.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
@@ -10,15 +10,17 @@ export {
10
10
  r as FaqListItem,
11
11
  M as LockedAttachment,
12
12
  l as MediaMessage,
13
- h as MessageVoteButtons,
14
- u as MessagingProvider,
13
+ u as MessageVoteButtons,
14
+ h as MessagingProvider,
15
15
  L as MessagingShell,
16
- c as formatRelativeTime,
17
- d as isLinkAttachment,
18
- v as resolveLinkAttachment,
19
- C as resolveMediaFromMessage,
20
- A as useCustomMessage,
21
- k as useMessageVote,
22
- F as useMessaging
16
+ d as formatRelativeTime,
17
+ C as getMessageDisplayText,
18
+ c as isLinkAttachment,
19
+ v as normalizeLanguageCode,
20
+ A as resolveLinkAttachment,
21
+ k as resolveMediaFromMessage,
22
+ p as useCustomMessage,
23
+ F as useMessageVote,
24
+ f as useMessaging
23
25
  };
24
26
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.40.2",
3
+ "version": "2.0.1",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@linktr.ee/component-library": "11.8.6",
37
- "@linktr.ee/messaging-core": "^1.8.0",
37
+ "@linktr.ee/messaging-core": "^2.0.0",
38
38
  "@phosphor-icons/react": "^2.1.10"
39
39
  },
40
40
  "devDependencies": {
@@ -65,14 +65,13 @@ const createChannel = () =>
65
65
  }) as unknown as Channel
66
66
 
67
67
  const createParticipant = (
68
- overrides: Partial<{ name: string; email: string; username: string }> = {}
68
+ overrides: Partial<{ name: string; username: string }> = {}
69
69
  ) =>
70
70
  ({
71
71
  user: {
72
72
  id: 'linker-1',
73
73
  name: overrides.name ?? 'Linker',
74
74
  image: undefined,
75
- email: overrides.email,
76
75
  username: overrides.username,
77
76
  },
78
77
  role: 'member',
@@ -99,18 +98,7 @@ describe('ChannelInfoDialog', () => {
99
98
  expect(nameEl).toBeInTheDocument()
100
99
  })
101
100
 
102
- it('renders participant email as secondary info', () => {
103
- renderWithProviders(
104
- <ChannelInfoDialog
105
- {...defaultProps()}
106
- participant={createParticipant({ email: 'linker@example.com' })}
107
- />
108
- )
109
-
110
- expect(screen.getByText('linker@example.com')).toBeInTheDocument()
111
- })
112
-
113
- it('renders participant username as secondary info when no email', () => {
101
+ it('renders participant username as secondary info', () => {
114
102
  renderWithProviders(
115
103
  <ChannelInfoDialog
116
104
  {...defaultProps()}
@@ -12,9 +12,8 @@ import ActionButton from '../ActionButton'
12
12
  import { Avatar } from '../Avatar'
13
13
  import { CloseButton } from '../CloseButton'
14
14
 
15
- // Custom user type with email and username
15
+ // Custom user type with username
16
16
  type CustomUser = {
17
- email?: string
18
17
  username?: string
19
18
  }
20
19
 
@@ -180,13 +179,10 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
180
179
  const participantName =
181
180
  participant.user?.name || participant.user?.id || 'Unknown member'
182
181
  const participantImage = participant.user?.image
183
- const participantEmail = (participant.user as CustomUser)?.email
184
182
  const participantUsername = (participant.user as CustomUser)?.username
185
- const participantSecondary = participantEmail
186
- ? participantEmail
187
- : participantUsername
188
- ? `linktr.ee/${participantUsername}`
189
- : undefined
183
+ const participantSecondary = participantUsername
184
+ ? `linktr.ee/${participantUsername}`
185
+ : undefined
190
186
  const participantId = participant.user?.id || 'unknown'
191
187
 
192
188
  return (
@@ -9,6 +9,7 @@ type ChannelListContextValue = {
9
9
  message: LocalMessage | undefined,
10
10
  defaultPreview?: string
11
11
  ) => React.ReactNode
12
+ viewerLanguage?: string
12
13
  }
13
14
 
14
15
  const ChannelListContext = React.createContext<ChannelListContextValue>({
@@ -16,6 +17,7 @@ const ChannelListContext = React.createContext<ChannelListContextValue>({
16
17
  onChannelSelect: () => {},
17
18
  debug: false,
18
19
  renderMessagePreview: undefined,
20
+ viewerLanguage: undefined,
19
21
  })
20
22
 
21
23
  export const ChannelListProvider = ChannelListContext.Provider
@@ -4,6 +4,7 @@ import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
4
4
 
5
5
  import { useChannelStar } from '../../hooks/useChannelStar'
6
6
  import { formatRelativeTime } from '../../utils/formatRelativeTime'
7
+ import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
7
8
  import { Avatar } from '../Avatar'
8
9
  import { isChatbotMessage } from '../CustomMessage/MessageTag'
9
10
 
@@ -14,8 +15,13 @@ import { useChannelListContext } from './ChannelListContext'
14
15
  */
15
16
  const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
16
17
  ({ channel, unread }) => {
17
- const { selectedChannel, onChannelSelect, debug, renderMessagePreview } =
18
- useChannelListContext()
18
+ const {
19
+ selectedChannel,
20
+ onChannelSelect,
21
+ debug,
22
+ renderMessagePreview,
23
+ viewerLanguage,
24
+ } = useChannelListContext()
19
25
 
20
26
  const isSelected = selectedChannel?.id === channel?.id
21
27
 
@@ -53,7 +59,12 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
53
59
  })()
54
60
 
55
61
  const getLastMessageText = () => {
56
- if (lastMessage?.text) return lastMessage.text
62
+ const displayText = getMessageDisplayText({
63
+ message: lastMessage,
64
+ viewerLanguage,
65
+ })
66
+ if (displayText) return displayText
67
+
57
68
  const isTip = lastMessage?.metadata?.custom_type === 'MESSAGE_TIP'
58
69
  if (isTip) return '💵 Sent a tip'
59
70
 
@@ -28,6 +28,7 @@ export const ChannelList = React.memo<ChannelListProps>(
28
28
  className,
29
29
  customEmptyStateIndicator,
30
30
  renderMessagePreview,
31
+ viewerLanguage,
31
32
  }) => {
32
33
  // Track renders
33
34
  const renderCountRef = React.useRef(0)
@@ -64,8 +65,15 @@ export const ChannelList = React.memo<ChannelListProps>(
64
65
  onChannelSelect,
65
66
  debug,
66
67
  renderMessagePreview,
68
+ viewerLanguage,
67
69
  }),
68
- [selectedChannel, onChannelSelect, debug, renderMessagePreview]
70
+ [
71
+ selectedChannel,
72
+ onChannelSelect,
73
+ debug,
74
+ renderMessagePreview,
75
+ viewerLanguage,
76
+ ]
69
77
  )
70
78
 
71
79
  return (
@@ -28,6 +28,17 @@ vi.mock('stream-chat-react', () => ({
28
28
  ),
29
29
  useMessageContext: () => ({ message: { id: 'message-1', text: 'hello' } }),
30
30
  useChannelStateContext: () => ({ channel: activeChannel }),
31
+ useChatContext: () => ({ client: { user: { id: 'visitor-1' } } }),
32
+ useTypingContext: () => ({ typing: {} }),
33
+ useAIState: () => ({ aiState: 'AI_STATE_IDLE' }),
34
+ AIStates: {
35
+ Error: 'AI_STATE_ERROR',
36
+ ExternalSources: 'AI_STATE_EXTERNAL_SOURCES',
37
+ Generating: 'AI_STATE_GENERATING',
38
+ Idle: 'AI_STATE_IDLE',
39
+ Stop: 'AI_STATE_STOP',
40
+ Thinking: 'AI_STATE_THINKING',
41
+ },
31
42
  }))
32
43
 
33
44
  vi.mock('../providers/MessagingProvider', () => ({
@@ -28,6 +28,7 @@ import { CustomMessage } from './CustomMessage'
28
28
  import { CustomMessageInput } from './CustomMessageInput'
29
29
  import { CustomSystemMessage } from './CustomSystemMessage'
30
30
  import CustomTypingIndicator from './CustomTypingIndicator'
31
+ import { DmAgentEnabledContext } from './CustomTypingIndicator/DmAgentContext'
31
32
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
32
33
  import { LoadingState } from './MessagingShell/LoadingState'
33
34
 
@@ -257,6 +258,7 @@ const ChannelViewInner: React.FC<{
257
258
  message: NonNullable<MessageUIComponentProps['message']>
258
259
  ) => React.ReactNode
259
260
  dmAgentEnabled?: boolean
261
+ viewerLanguage?: string
260
262
  }> = ({
261
263
  onBack,
262
264
  showBackButton,
@@ -275,6 +277,7 @@ const ChannelViewInner: React.FC<{
275
277
  customChannelActions,
276
278
  renderMessage,
277
279
  dmAgentEnabled = false,
280
+ viewerLanguage,
278
281
  }) => {
279
282
  const { channel } = useChannelStateContext()
280
283
  const infoDialogRef = useRef<HTMLDialogElement>(null)
@@ -339,7 +342,11 @@ const ChannelViewInner: React.FC<{
339
342
  // eslint-disable-next-line react-hooks/rules-of-hooks
340
343
  const { message } = useMessageContext('ChannelView')
341
344
  const messageNode = (
342
- <CustomMessage {...props} chatbotVotingEnabled={chatbotVotingEnabled} />
345
+ <CustomMessage
346
+ {...props}
347
+ chatbotVotingEnabled={chatbotVotingEnabled}
348
+ viewerLanguage={viewerLanguage}
349
+ />
343
350
  )
344
351
 
345
352
  if (!renderMessage || !message) {
@@ -348,7 +355,7 @@ const ChannelViewInner: React.FC<{
348
355
 
349
356
  return renderMessage(messageNode, message)
350
357
  },
351
- [chatbotVotingEnabled, renderMessage]
358
+ [chatbotVotingEnabled, renderMessage, viewerLanguage]
352
359
  )
353
360
 
354
361
  return (
@@ -436,6 +443,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
436
443
  customChannelActions,
437
444
  renderMessage,
438
445
  sendButton,
446
+ viewerLanguage,
439
447
  }) => {
440
448
  // Custom send message handler that:
441
449
  // 1. Applies messageMetadata if provided
@@ -488,37 +496,40 @@ export const ChannelView = React.memo<ChannelViewProps>(
488
496
  className
489
497
  )}
490
498
  >
491
- <Channel
492
- channel={channel}
493
- MessageSystem={CustomSystemMessage}
494
- EmptyStateIndicator={CustomChannelEmptyState}
495
- LoadingIndicator={LoadingState}
496
- DateSeparator={CustomDateSeparator}
497
- TypingIndicator={CustomTypingIndicator}
498
- doSendMessageRequest={doSendMessageRequest}
499
- {...(sendButton ? { SendButton: sendButton } : {})}
500
- >
501
- <ChannelViewInner
502
- onBack={onBack}
503
- showBackButton={showBackButton}
504
- renderMessageInputActions={renderMessageInputActions}
505
- renderConversationFooter={renderConversationFooter}
506
- onLeaveConversation={onLeaveConversation}
507
- onBlockParticipant={onBlockParticipant}
508
- CustomChannelEmptyState={CustomChannelEmptyState}
509
- showDeleteConversation={showDeleteConversation}
510
- onDeleteConversationClick={onDeleteConversationClick}
511
- onBlockParticipantClick={onBlockParticipantClick}
512
- onReportParticipantClick={onReportParticipantClick}
513
- showStarButton={showStarButton}
514
- dmAgentEnabled={dmAgentEnabled}
515
- chatbotVotingEnabled={chatbotVotingEnabled}
516
- renderChannelBanner={renderChannelBanner}
517
- customProfileContent={customProfileContent}
518
- customChannelActions={customChannelActions}
519
- renderMessage={renderMessage}
520
- />
521
- </Channel>
499
+ <DmAgentEnabledContext.Provider value={dmAgentEnabled ?? false}>
500
+ <Channel
501
+ channel={channel}
502
+ MessageSystem={CustomSystemMessage}
503
+ EmptyStateIndicator={CustomChannelEmptyState}
504
+ LoadingIndicator={LoadingState}
505
+ DateSeparator={CustomDateSeparator}
506
+ TypingIndicator={CustomTypingIndicator}
507
+ doSendMessageRequest={doSendMessageRequest}
508
+ {...(sendButton ? { SendButton: sendButton } : {})}
509
+ >
510
+ <ChannelViewInner
511
+ onBack={onBack}
512
+ showBackButton={showBackButton}
513
+ renderMessageInputActions={renderMessageInputActions}
514
+ renderConversationFooter={renderConversationFooter}
515
+ onLeaveConversation={onLeaveConversation}
516
+ onBlockParticipant={onBlockParticipant}
517
+ CustomChannelEmptyState={CustomChannelEmptyState}
518
+ showDeleteConversation={showDeleteConversation}
519
+ onDeleteConversationClick={onDeleteConversationClick}
520
+ onBlockParticipantClick={onBlockParticipantClick}
521
+ onReportParticipantClick={onReportParticipantClick}
522
+ showStarButton={showStarButton}
523
+ dmAgentEnabled={dmAgentEnabled}
524
+ chatbotVotingEnabled={chatbotVotingEnabled}
525
+ renderChannelBanner={renderChannelBanner}
526
+ customProfileContent={customProfileContent}
527
+ customChannelActions={customChannelActions}
528
+ renderMessage={renderMessage}
529
+ viewerLanguage={viewerLanguage}
530
+ />
531
+ </Channel>
532
+ </DmAgentEnabledContext.Provider>
522
533
  </div>
523
534
  )
524
535
  }
@@ -30,6 +30,7 @@ import {
30
30
  } from 'stream-chat-react'
31
31
 
32
32
  import { useMessageVote } from '../../hooks/useMessageVote'
33
+ import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
33
34
  import { Avatar } from '../Avatar'
34
35
  import LockedAttachment from '../LockedAttachment'
35
36
  import { isLinkAttachment } from '../MediaMessage'
@@ -45,10 +46,12 @@ import { MessageVoteButtons } from './MessageVoteButtons'
45
46
 
46
47
  type CustomMessageUIComponentProps = MessageUIComponentProps & {
47
48
  chatbotVotingEnabled?: boolean
49
+ viewerLanguage?: string
48
50
  }
49
51
 
50
52
  type CustomMessageWithContextProps = MessageContextValue & {
51
53
  chatbotVotingEnabled?: boolean
54
+ viewerLanguage?: string
52
55
  }
53
56
 
54
57
  const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
@@ -68,11 +71,13 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
68
71
  message,
69
72
  renderText,
70
73
  threadList,
74
+ viewerLanguage,
71
75
  } = props
72
76
 
73
77
  const { client } = useChatContext('CustomMessage')
74
78
  const { channel } = useChannelStateContext('CustomMessage')
75
- const { isUnlocking, onUnlockClick, onFetchSource, onDownloadClick } = useCustomMessage('LockedAttachment')
79
+ const { isUnlocking, onUnlockClick, onFetchSource, onDownloadClick } =
80
+ useCustomMessage('LockedAttachment')
76
81
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
77
82
  const reminder = useMessageReminder(message.id)
78
83
  const { selected: voteState, voteUp, voteDown } = useMessageVote(message)
@@ -107,6 +112,12 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
107
112
  const filtered = raw.filter((a) => !('type' in a) || !isLinkAttachment(a))
108
113
  return filtered.length === raw.length ? raw : filtered
109
114
  }, [message])
115
+ const displayMessage = useMemo(() => {
116
+ const displayText = getMessageDisplayText({ message, viewerLanguage })
117
+ return displayText === message.text
118
+ ? message
119
+ : { ...message, text: displayText }
120
+ }, [message, viewerLanguage])
110
121
 
111
122
  if (isDateSeparatorMessage(message)) {
112
123
  return null
@@ -247,7 +258,10 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
247
258
  )}
248
259
  {message.text && (
249
260
  <div className="str-chat__message-bubble">
250
- <MessageText message={message} renderText={renderText} />
261
+ <MessageText
262
+ message={displayMessage}
263
+ renderText={renderText}
264
+ />
251
265
  </div>
252
266
  )}
253
267
  </div>
@@ -273,11 +287,14 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
273
287
  ) : null}
274
288
  {isAIGenerated ? (
275
289
  <StreamedMessageText
276
- message={message}
290
+ message={displayMessage}
277
291
  renderText={renderText}
278
292
  />
279
293
  ) : (
280
- <MessageText message={message} renderText={renderText} />
294
+ <MessageText
295
+ message={displayMessage}
296
+ renderText={renderText}
297
+ />
281
298
  )}
282
299
  <MessageErrorIcon />
283
300
  </div>
@@ -317,6 +334,7 @@ const MemoizedCustomMessage = React.memo(
317
334
  CustomMessageWithContext,
318
335
  (prev, next) => {
319
336
  if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
337
+ if (prev.viewerLanguage !== next.viewerLanguage) return false
320
338
  return areMessageUIPropsEqual(prev, next)
321
339
  }
322
340
  ) as typeof CustomMessageWithContext
@@ -2,16 +2,21 @@ import type { Meta, StoryFn } from '@storybook/react'
2
2
  import React from 'react'
3
3
  import type { Event } from 'stream-chat'
4
4
  import {
5
+ AIStates,
5
6
  ChannelStateProvider,
6
7
  ChatProvider,
7
8
  TypingProvider,
8
9
  } from 'stream-chat-react'
9
10
 
11
+ import { DmAgentEnabledContext } from './DmAgentContext'
12
+
10
13
  import CustomTypingIndicator from '.'
11
14
 
12
15
  type StoryProps = {
13
16
  typingEventsEnabled?: boolean
14
17
  typing?: Record<string, Event>
18
+ aiState?: string
19
+ dmAgentEnabled?: boolean
15
20
  }
16
21
 
17
22
  const currentUser = {
@@ -33,10 +38,38 @@ const defaultTyping: Record<string, Event> = {
33
38
  } as Event,
34
39
  }
35
40
 
41
+ type ListenerMap = Record<string, (event: { ai_state: string; cid: string }) => void>
42
+
43
+ const createMockChannel = ({ aiState }: { aiState?: string }) => {
44
+ const listeners: ListenerMap = {}
45
+ const channel = {
46
+ cid: 'messaging:test',
47
+ state: {
48
+ members: {
49
+ [currentUser.id]: { user: currentUser },
50
+ [typingUser.id]: { user: typingUser },
51
+ },
52
+ },
53
+ on(eventType: string, handler: ListenerMap[string]) {
54
+ listeners[eventType] = handler
55
+ // Fire the requested AI state synchronously so the hook picks it up.
56
+ if (eventType === 'ai_indicator.update' && aiState) {
57
+ handler({ ai_state: aiState, cid: 'messaging:test' })
58
+ }
59
+ return { unsubscribe: () => {} }
60
+ },
61
+ }
62
+ return channel
63
+ }
64
+
36
65
  const StoryWrapper: React.FC<StoryProps> = ({
37
66
  typingEventsEnabled = true,
38
- typing = defaultTyping,
67
+ typing = {},
68
+ aiState,
69
+ dmAgentEnabled = true,
39
70
  }) => {
71
+ const channel = createMockChannel({ aiState })
72
+
40
73
  const chatContextValue = {
41
74
  client: {
42
75
  user: currentUser,
@@ -57,18 +90,7 @@ const StoryWrapper: React.FC<StoryProps> = ({
57
90
  }
58
91
 
59
92
  const channelStateValue = {
60
- channel: {
61
- state: {
62
- members: {
63
- [currentUser.id]: {
64
- user: currentUser,
65
- },
66
- [typingUser.id]: {
67
- user: typingUser,
68
- },
69
- },
70
- },
71
- },
93
+ channel,
72
94
  channelCapabilities: {},
73
95
  channelConfig: {
74
96
  typing_events: typingEventsEnabled,
@@ -85,9 +107,11 @@ const StoryWrapper: React.FC<StoryProps> = ({
85
107
  <ChatProvider value={chatContextValue as never}>
86
108
  <ChannelStateProvider value={channelStateValue as never}>
87
109
  <TypingProvider value={{ typing }}>
88
- <div className="relative h-20 w-[200px] bg-[#f4f4f4] p-3">
89
- <CustomTypingIndicator />
90
- </div>
110
+ <DmAgentEnabledContext.Provider value={dmAgentEnabled}>
111
+ <div className="relative h-20 w-[200px] bg-[#f4f4f4] p-3">
112
+ <CustomTypingIndicator />
113
+ </div>
114
+ </DmAgentEnabledContext.Provider>
91
115
  </TypingProvider>
92
116
  </ChannelStateProvider>
93
117
  </ChatProvider>
@@ -104,7 +128,9 @@ const meta: Meta<StoryProps> = {
104
128
  export default meta
105
129
 
106
130
  export const Default: StoryFn<StoryProps> = (args) => <StoryWrapper {...args} />
107
- Default.args = {}
131
+ Default.args = {
132
+ typing: defaultTyping,
133
+ }
108
134
 
109
135
  export const HiddenWhenNoTyping: StoryFn<StoryProps> = (args) => (
110
136
  <StoryWrapper {...args} />
@@ -112,3 +138,17 @@ export const HiddenWhenNoTyping: StoryFn<StoryProps> = (args) => (
112
138
  HiddenWhenNoTyping.args = {
113
139
  typing: {},
114
140
  }
141
+
142
+ export const AiAgentThinking: StoryFn<StoryProps> = (args) => (
143
+ <StoryWrapper {...args} />
144
+ )
145
+ AiAgentThinking.args = {
146
+ aiState: AIStates.Thinking,
147
+ }
148
+
149
+ export const AiAgentGenerating: StoryFn<StoryProps> = (args) => (
150
+ <StoryWrapper {...args} />
151
+ )
152
+ AiAgentGenerating.args = {
153
+ aiState: AIStates.Generating,
154
+ }