@linktr.ee/messaging-react 2.0.0 → 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.
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 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-Brz9orsI.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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "2.0.0",
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",
@@ -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
 
@@ -495,38 +496,40 @@ export const ChannelView = React.memo<ChannelViewProps>(
495
496
  className
496
497
  )}
497
498
  >
498
- <Channel
499
- channel={channel}
500
- MessageSystem={CustomSystemMessage}
501
- EmptyStateIndicator={CustomChannelEmptyState}
502
- LoadingIndicator={LoadingState}
503
- DateSeparator={CustomDateSeparator}
504
- TypingIndicator={CustomTypingIndicator}
505
- doSendMessageRequest={doSendMessageRequest}
506
- {...(sendButton ? { SendButton: sendButton } : {})}
507
- >
508
- <ChannelViewInner
509
- onBack={onBack}
510
- showBackButton={showBackButton}
511
- renderMessageInputActions={renderMessageInputActions}
512
- renderConversationFooter={renderConversationFooter}
513
- onLeaveConversation={onLeaveConversation}
514
- onBlockParticipant={onBlockParticipant}
515
- CustomChannelEmptyState={CustomChannelEmptyState}
516
- showDeleteConversation={showDeleteConversation}
517
- onDeleteConversationClick={onDeleteConversationClick}
518
- onBlockParticipantClick={onBlockParticipantClick}
519
- onReportParticipantClick={onReportParticipantClick}
520
- showStarButton={showStarButton}
521
- dmAgentEnabled={dmAgentEnabled}
522
- chatbotVotingEnabled={chatbotVotingEnabled}
523
- renderChannelBanner={renderChannelBanner}
524
- customProfileContent={customProfileContent}
525
- customChannelActions={customChannelActions}
526
- renderMessage={renderMessage}
527
- viewerLanguage={viewerLanguage}
528
- />
529
- </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>
530
533
  </div>
531
534
  )
532
535
  }
@@ -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
+ }
@@ -0,0 +1,187 @@
1
+ import React from 'react'
2
+ import type { Event } from 'stream-chat'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { renderWithProviders, screen } from '../../test/utils'
6
+
7
+ import { DmAgentEnabledContext } from './DmAgentContext'
8
+
9
+ const visitor = { id: 'visitor-1', name: 'Visitor' }
10
+ const agent = { id: 'creator-1', name: 'Creator', image: 'agent.png' }
11
+
12
+ let typingContext: { typing: Record<string, Event> } = { typing: {} }
13
+ let aiStateContext: { aiState: string } = { aiState: 'AI_STATE_IDLE' }
14
+ let channelStateContext: {
15
+ channel: {
16
+ state: { members: Record<string, { user: typeof visitor | typeof agent }> }
17
+ }
18
+ channelConfig: { typing_events: boolean }
19
+ thread: undefined
20
+ } = {
21
+ channel: {
22
+ state: {
23
+ members: {
24
+ [visitor.id]: { user: visitor },
25
+ [agent.id]: { user: agent },
26
+ },
27
+ },
28
+ },
29
+ channelConfig: { typing_events: true },
30
+ thread: undefined,
31
+ }
32
+
33
+ vi.mock('stream-chat-react', () => ({
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
+ },
42
+ useAIState: () => aiStateContext,
43
+ useChannelStateContext: () => channelStateContext,
44
+ useChatContext: () => ({ client: { user: visitor } }),
45
+ useTypingContext: () => typingContext,
46
+ }))
47
+
48
+ vi.mock('../Avatar', () => ({
49
+ Avatar: ({ name, id }: { name: string; id: string }) => (
50
+ <div data-testid="avatar" data-id={id}>
51
+ {name}
52
+ </div>
53
+ ),
54
+ }))
55
+
56
+ const importIndicator = async () => (await import('.')).default
57
+
58
+ const renderIndicator = async (dmAgentEnabled = true) => {
59
+ const CustomTypingIndicator = await importIndicator()
60
+ return renderWithProviders(
61
+ <DmAgentEnabledContext.Provider value={dmAgentEnabled}>
62
+ <CustomTypingIndicator />
63
+ </DmAgentEnabledContext.Provider>
64
+ )
65
+ }
66
+
67
+ describe('CustomTypingIndicator', () => {
68
+ beforeEach(() => {
69
+ typingContext = { typing: {} }
70
+ aiStateContext = { aiState: 'AI_STATE_IDLE' }
71
+ channelStateContext = {
72
+ channel: {
73
+ state: {
74
+ members: {
75
+ [visitor.id]: { user: visitor },
76
+ [agent.id]: { user: agent },
77
+ },
78
+ },
79
+ },
80
+ channelConfig: { typing_events: true },
81
+ thread: undefined,
82
+ }
83
+ })
84
+
85
+ it('renders nothing when idle and no typers', async () => {
86
+ await renderIndicator()
87
+ expect(screen.queryByTestId('typing-indicator')).toBeNull()
88
+ expect(screen.queryByTestId('typing-indicator-ai')).toBeNull()
89
+ })
90
+
91
+ it('renders the human typing bubble when someone else is typing', async () => {
92
+ typingContext = {
93
+ typing: {
94
+ [agent.id]: {
95
+ type: 'typing.start',
96
+ user: agent,
97
+ parent_id: undefined,
98
+ } as Event,
99
+ },
100
+ }
101
+ await renderIndicator()
102
+ expect(screen.getByTestId('typing-indicator')).toBeInTheDocument()
103
+ expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', agent.id)
104
+ })
105
+
106
+ it('hides the human typing bubble when typing_events is disabled', async () => {
107
+ channelStateContext.channelConfig.typing_events = false
108
+ typingContext = {
109
+ typing: {
110
+ [agent.id]: {
111
+ type: 'typing.start',
112
+ user: agent,
113
+ parent_id: undefined,
114
+ } as Event,
115
+ },
116
+ }
117
+ await renderIndicator()
118
+ expect(screen.queryByTestId('typing-indicator')).toBeNull()
119
+ })
120
+
121
+ it('renders the AI bubble when the agent is thinking', async () => {
122
+ aiStateContext = { aiState: 'AI_STATE_THINKING' }
123
+ await renderIndicator()
124
+ expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
125
+ expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', agent.id)
126
+ })
127
+
128
+ it('renders the AI bubble when the agent is generating', async () => {
129
+ aiStateContext = { aiState: 'AI_STATE_GENERATING' }
130
+ await renderIndicator()
131
+ expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
132
+ })
133
+
134
+ it('renders the AI bubble when the agent is checking external sources', async () => {
135
+ aiStateContext = { aiState: 'AI_STATE_EXTERNAL_SOURCES' }
136
+ await renderIndicator()
137
+ expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
138
+ })
139
+
140
+ it('does not render the AI bubble inside a thread list', async () => {
141
+ aiStateContext = { aiState: 'AI_STATE_GENERATING' }
142
+ const CustomTypingIndicator = await importIndicator()
143
+ renderWithProviders(
144
+ <DmAgentEnabledContext.Provider value={true}>
145
+ <CustomTypingIndicator threadList />
146
+ </DmAgentEnabledContext.Provider>
147
+ )
148
+ expect(screen.queryByTestId('typing-indicator-ai')).toBeNull()
149
+ })
150
+
151
+ it('renders the AI bubble even when typing_events is disabled', async () => {
152
+ channelStateContext.channelConfig.typing_events = false
153
+ aiStateContext = { aiState: 'AI_STATE_THINKING' }
154
+ await renderIndicator()
155
+ expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
156
+ })
157
+
158
+ it('falls back to a default avatar id when no other channel member exists', async () => {
159
+ channelStateContext.channel.state.members = {
160
+ [visitor.id]: { user: visitor },
161
+ }
162
+ aiStateContext = { aiState: 'AI_STATE_GENERATING' }
163
+ await renderIndicator()
164
+ expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument()
165
+ expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', 'ai-agent')
166
+ })
167
+
168
+ it('does not render the AI bubble when dmAgentEnabled is false', async () => {
169
+ aiStateContext = { aiState: 'AI_STATE_GENERATING' }
170
+ await renderIndicator(false)
171
+ expect(screen.queryByTestId('typing-indicator-ai')).toBeNull()
172
+ })
173
+
174
+ it('still renders the human bubble when dmAgentEnabled is false', async () => {
175
+ typingContext = {
176
+ typing: {
177
+ [agent.id]: {
178
+ type: 'typing.start',
179
+ user: agent,
180
+ parent_id: undefined,
181
+ } as Event,
182
+ },
183
+ }
184
+ await renderIndicator(false)
185
+ expect(screen.getByTestId('typing-indicator')).toBeInTheDocument()
186
+ })
187
+ })
@@ -0,0 +1,3 @@
1
+ import { createContext } from 'react'
2
+
3
+ export const DmAgentEnabledContext = createContext<boolean>(false)
@@ -1,6 +1,8 @@
1
- import React from 'react'
2
- import type { Event } from 'stream-chat'
1
+ import React, { useContext } from 'react'
2
+ import type { Event, UserResponse } from 'stream-chat'
3
3
  import {
4
+ AIStates,
5
+ useAIState,
4
6
  useChannelStateContext,
5
7
  useChatContext,
6
8
  useTypingContext,
@@ -8,6 +10,8 @@ import {
8
10
 
9
11
  import { Avatar } from '../Avatar'
10
12
 
13
+ import { DmAgentEnabledContext } from './DmAgentContext'
14
+
11
15
  interface CustomTypingIndicatorProps {
12
16
  threadList?: boolean
13
17
  }
@@ -25,10 +29,38 @@ const Circle = ({ cx, index }: { cx: string; index: number }) => (
25
29
  </circle>
26
30
  )
27
31
 
32
+ const AI_ACTIVE_STATES = new Set<string>([
33
+ AIStates.Thinking,
34
+ AIStates.Generating,
35
+ AIStates.ExternalSources,
36
+ ])
37
+
28
38
  const CustomTypingIndicator = ({ threadList }: CustomTypingIndicatorProps) => {
29
39
  const { channel, channelConfig, thread } = useChannelStateContext()
30
40
  const { client } = useChatContext()
31
41
  const { typing = {} } = useTypingContext()
42
+ const { aiState } = useAIState(channel)
43
+ const dmAgentEnabled = useContext(DmAgentEnabledContext)
44
+
45
+ // Show the AI indicator whenever the consumer agent is producing a reply.
46
+ // This event stream is independent of `typing.start`/`typing.stop`, so it is
47
+ // intentionally NOT gated by `channelConfig.typing_events`. Gate strictly on
48
+ // `dmAgentEnabled` so stale or off-surface ai_indicator events never surface
49
+ // the bubble on channels where the agent is not active.
50
+ const isAiActive =
51
+ !threadList && dmAgentEnabled && AI_ACTIVE_STATES.has(aiState)
52
+
53
+ if (isAiActive) {
54
+ const agentUser = findOtherChannelUser(channel, client.user?.id)
55
+ return (
56
+ <TypingBubble
57
+ avatarId={agentUser?.id ?? 'ai-agent'}
58
+ avatarName={agentUser?.name ?? agentUser?.id ?? 'Agent'}
59
+ avatarImage={agentUser?.image}
60
+ testId="typing-indicator-ai"
61
+ />
62
+ )
63
+ }
32
64
 
33
65
  if (channelConfig?.typing_events === false) {
34
66
  return null
@@ -59,43 +91,75 @@ const CustomTypingIndicator = ({ threadList }: CustomTypingIndicatorProps) => {
59
91
  ? channel.state.members[typingUser.id].user
60
92
  : undefined
61
93
 
62
- const avatarId = typingUser?.id ?? memberUser?.id ?? 'typing-user'
63
- const avatarName =
64
- typingUser?.name ?? memberUser?.name ?? typingUser?.id ?? 'Typing user'
65
- const avatarImage = typingUser?.image ?? memberUser?.image
66
-
67
94
  return (
68
- <div
69
- className="str-chat__typing-indicator !items-end !bg-transparent"
70
- data-testid="typing-indicator"
71
- style={{ insetInlineStart: 0, insetInlineEnd: 'auto' }}
72
- >
73
- <div className="shrink-0" aria-hidden="true">
74
- <Avatar
75
- id={avatarId}
76
- name={avatarName}
77
- image={avatarImage}
78
- size={24}
79
- shape="circle"
80
- />
81
- </div>
82
-
83
- <div className="px-4 py-3 rounded-lg bg-[#E9EAED] h-12 flex flex-col justify-end">
84
- <svg
85
- aria-hidden="true"
86
- className="block overflow-visible mb-[0.2rem]"
87
- viewBox="0 0 32 8"
88
- width="32"
89
- height="8"
90
- overflow="visible"
91
- >
92
- <Circle cx="4" index={0} />
93
- <Circle cx="16" index={1} />
94
- <Circle cx="28" index={2} />
95
- </svg>
96
- </div>
97
- </div>
95
+ <TypingBubble
96
+ avatarId={typingUser?.id ?? memberUser?.id ?? 'typing-user'}
97
+ avatarName={
98
+ typingUser?.name ?? memberUser?.name ?? typingUser?.id ?? 'Typing user'
99
+ }
100
+ avatarImage={typingUser?.image ?? memberUser?.image}
101
+ testId="typing-indicator"
102
+ />
98
103
  )
99
104
  }
100
105
 
106
+ const TypingBubble = ({
107
+ avatarId,
108
+ avatarName,
109
+ avatarImage,
110
+ testId,
111
+ }: {
112
+ avatarId: string
113
+ avatarName: string
114
+ avatarImage?: string | null
115
+ testId: string
116
+ }) => (
117
+ <div
118
+ className="str-chat__typing-indicator !items-end !bg-transparent"
119
+ data-testid={testId}
120
+ style={{ insetInlineStart: 0, insetInlineEnd: 'auto' }}
121
+ >
122
+ <div className="shrink-0" aria-hidden="true">
123
+ <Avatar
124
+ id={avatarId}
125
+ name={avatarName}
126
+ image={avatarImage ?? undefined}
127
+ size={24}
128
+ shape="circle"
129
+ />
130
+ </div>
131
+
132
+ <div className="px-4 py-3 rounded-lg bg-[#E9EAED] h-12 flex flex-col justify-end">
133
+ <svg
134
+ aria-hidden="true"
135
+ className="block overflow-visible mb-[0.2rem]"
136
+ viewBox="0 0 32 8"
137
+ width="32"
138
+ height="8"
139
+ overflow="visible"
140
+ >
141
+ <Circle cx="4" index={0} />
142
+ <Circle cx="16" index={1} />
143
+ <Circle cx="28" index={2} />
144
+ </svg>
145
+ </div>
146
+ </div>
147
+ )
148
+
149
+ type ChannelLike = ReturnType<typeof useChannelStateContext>['channel']
150
+
151
+ function findOtherChannelUser(
152
+ channel: ChannelLike,
153
+ selfId: string | undefined
154
+ ): UserResponse | undefined {
155
+ const members = channel?.state?.members ?? {}
156
+ for (const member of Object.values(members)) {
157
+ const memberUser = member?.user
158
+ if (memberUser && memberUser.id !== selfId) {
159
+ return memberUser
160
+ }
161
+ }
162
+ return undefined
163
+ }
164
+
101
165
  export default CustomTypingIndicator
package/src/styles.css CHANGED
@@ -1,6 +1,13 @@
1
1
  /* Stream Chat base styles */
2
2
  @import 'stream-chat-react/dist/css/v2/index.css';
3
3
 
4
+ /* Inherit the host's font instead of stream-chat-react's hardcoded system stack.
5
+ In admin (federation host) this resolves to Link Sans; on linktr.ee profile
6
+ pages it resolves to the creator's profile font. */
7
+ .str-chat {
8
+ --str-chat__font-family: inherit;
9
+ }
10
+
4
11
  /* Dialog component styles - used by messaging components */
5
12
  /* Note: Dialogs get moved to the top layer when opened with .showModal() */
6
13
  .mes-dialog {