@linktr.ee/messaging-react 1.35.0 → 1.36.0-rc-1777522205

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 CHANGED
@@ -27,7 +27,7 @@ export declare type AttachmentSourceType = 'image' | 'audio' | 'video' | 'docume
27
27
  /**
28
28
  * Avatar component that displays a user image or colored initial fallback
29
29
  */
30
- export declare const Avatar: ({ id, image, size, className, starred, shape, aiActive, }: AvatarProps) => JSX_2.Element;
30
+ export declare const Avatar: ({ id, image, size, className, starred, shape, dmAgentEnabled, }: AvatarProps) => JSX_2.Element;
31
31
 
32
32
  export declare interface AvatarProps {
33
33
  id: string;
@@ -37,7 +37,7 @@ export declare interface AvatarProps {
37
37
  className?: string;
38
38
  starred?: boolean;
39
39
  shape?: 'squircle' | 'circle';
40
- aiActive?: boolean;
40
+ dmAgentEnabled?: boolean;
41
41
  }
42
42
 
43
43
  /**
@@ -151,6 +151,8 @@ export declare interface ChannelViewProps {
151
151
  * messages will be sent with skip_push and silent flags to suppress
152
152
  * notifications to the creator until the agent responds.
153
153
  * The library reads chatbot_paused from channel.data internally.
154
+ *
155
+ * In header UI, this treatment is shown only for visitor view.
154
156
  */
155
157
  dmAgentEnabled?: boolean;
156
158
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as e, a as t, C as i, b as n, c as o, d as g, F as r, e as m, L as u, f as M, h as c, i as l, j as h, P as C, k as P, u as d, l as L, m as p, n as v } from "./index-BsSqs61X.js";
1
+ import { A as e, a as t, C as i, b as n, c as o, d as g, F as r, e as m, L as u, f as M, h as c, i as l, j as h, P as C, k as P, u as d, l as L, m as p, n as v } from "./index-DOsC03ZN.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": "1.35.0",
3
+ "version": "1.36.0-rc-1777522205",
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 @@ AIActive.args = {
34
34
  id: 'ai-agent',
35
35
  name: 'AI Assistant',
36
36
  image: 'https://i.pravatar.cc/150?img=12',
37
- aiActive: true,
37
+ dmAgentEnabled: true,
38
38
  }
39
39
 
40
40
  export const WithoutImage: StoryFn<ComponentProps> = Template.bind({})
@@ -170,7 +170,7 @@ export const AIActiveVariousSizes: StoryFn = () => {
170
170
  id="ai-user-consistent"
171
171
  name="AI Assistant"
172
172
  size={size}
173
- aiActive
173
+ dmAgentEnabled
174
174
  />
175
175
  <span className="text-xs text-stone">{size}px</span>
176
176
  </div>
@@ -12,7 +12,7 @@ export interface AvatarProps {
12
12
  className?: string
13
13
  starred?: boolean
14
14
  shape?: 'squircle' | 'circle'
15
- aiActive?: boolean
15
+ dmAgentEnabled?: boolean
16
16
  }
17
17
 
18
18
  /**
@@ -25,7 +25,7 @@ export const Avatar = ({
25
25
  className,
26
26
  starred = false,
27
27
  shape = 'squircle',
28
- aiActive = false,
28
+ dmAgentEnabled = false,
29
29
  }: AvatarProps) => {
30
30
  const emoji = getAvatarEmoji(id)
31
31
 
@@ -37,14 +37,15 @@ export const Avatar = ({
37
37
  }
38
38
 
39
39
  const fontSizeClass = getFontSizeClass()
40
- const aiRingWidth = Math.max(1, Math.ceil(size / 40))
40
+ // Match design treatment: 2px AI border at 40px avatar size.
41
+ const aiBorderWidth = size >= 40 ? 2 : 1
41
42
 
42
43
  const borderStyle =
43
44
  shape === 'circle'
44
45
  ? { borderRadius: '50%' }
45
46
  : {
46
47
  borderRadius: '33%',
47
- 'corner-shape': 'superellipse(1.3)',
48
+ cornerShape: 'superellipse(1.3)',
48
49
  }
49
50
 
50
51
  const avatarInner = (
@@ -89,15 +90,19 @@ export const Avatar = ({
89
90
  </div>
90
91
  )}
91
92
  <div
93
+ data-testid="avatar-ring"
92
94
  className={classNames(
93
95
  'h-full w-full',
94
- aiActive
95
- ? 'bg-[linear-gradient(135deg,#A855F7_0%,#8B5CF6_45%,#6D28D9_100%)]'
96
- : 'bg-transparent'
96
+ 'bg-transparent'
97
97
  )}
98
98
  style={{
99
99
  ...borderStyle,
100
- ...(aiActive && { padding: `${aiRingWidth}px` }),
100
+ ...(dmAgentEnabled && {
101
+ borderWidth: `${aiBorderWidth}px`,
102
+ borderStyle: 'solid',
103
+ borderColor: 'var(--AI-Gradient, #7F22FE)',
104
+ boxShadow: 'inset 0 1px 2px 0 rgba(255, 255, 255, 0.5)',
105
+ }),
101
106
  }}
102
107
  >
103
108
  {avatarInner}
@@ -125,12 +125,22 @@ const createMockChannel = async (
125
125
  channel.state.messages = mockMessages as unknown as any[]
126
126
  channel.state.members = {
127
127
  [mockUser.id]: {
128
- user: mockUser,
128
+ user: {
129
+ ...mockUser,
130
+ is_account: false,
131
+ },
129
132
  user_id: mockUser.id,
133
+ role: 'owner',
134
+ is_account: false,
130
135
  },
131
136
  [participant.id]: {
132
- user: participant,
137
+ user: {
138
+ ...participant,
139
+ is_account: true,
140
+ },
133
141
  user_id: participant.id,
142
+ role: 'member',
143
+ is_account: true,
134
144
  },
135
145
  }
136
146
  return {
@@ -240,6 +250,25 @@ Default.parameters = {
240
250
  },
241
251
  }
242
252
 
253
+ export const DmAgentHeader: StoryFn<TemplateProps> = Template.bind({})
254
+ DmAgentHeader.args = {
255
+ showBackButton: false,
256
+ dmAgentEnabled: true,
257
+ onBack: () => console.log('Back clicked'),
258
+ onLeaveConversation: (channel) =>
259
+ console.log('Leave conversation:', channel.id),
260
+ onBlockParticipant: (participantId) =>
261
+ console.log('Block participant:', participantId),
262
+ }
263
+ DmAgentHeader.parameters = {
264
+ docs: {
265
+ description: {
266
+ story:
267
+ 'Channel view with DM agent header treatment enabled (AI avatar border and "Replies instantly with AI assistant" helper text).',
268
+ },
269
+ },
270
+ }
271
+
243
272
  export const WithBackButton: StoryFn<TemplateProps> = Template.bind({})
244
273
  WithBackButton.args = {
245
274
  showBackButton: true,
@@ -71,16 +71,36 @@ vi.mock('../hooks/useChannelStar', () => ({
71
71
  useChannelStar: () => mockIsStarred,
72
72
  }))
73
73
 
74
- const createChannel = () =>
74
+ const createChannel = ({
75
+ currentUserId = 'visitor-1',
76
+ currentUserRole = 'owner',
77
+ }: {
78
+ currentUserId?: 'visitor-1' | 'linker-1'
79
+ currentUserRole?: 'owner' | 'member'
80
+ } = {}) =>
75
81
  ({
76
82
  id: 'channel-1',
77
83
  cid: 'messaging:channel-1',
78
84
  data: {},
79
- _client: { userID: 'visitor-1' },
85
+ _client: { userID: currentUserId },
80
86
  state: {
81
87
  members: {
82
- visitor: { user: { id: 'visitor-1', name: 'Visitor' }, role: 'owner' },
83
- linker: { user: { id: 'linker-1', name: 'Linker' }, role: 'member' },
88
+ current: {
89
+ user: {
90
+ id: currentUserId,
91
+ name: currentUserId === 'visitor-1' ? 'Visitor' : 'Linker',
92
+ is_account: currentUserId !== 'visitor-1',
93
+ },
94
+ role: currentUserRole,
95
+ },
96
+ other: {
97
+ user: {
98
+ id: currentUserId === 'visitor-1' ? 'linker-1' : 'visitor-1',
99
+ name: currentUserId === 'visitor-1' ? 'Linker' : 'Visitor',
100
+ is_account: currentUserId === 'visitor-1',
101
+ },
102
+ role: currentUserRole === 'owner' ? 'member' : 'owner',
103
+ },
84
104
  },
85
105
  membership: {},
86
106
  messages: [],
@@ -172,6 +192,57 @@ describe('ChannelView', () => {
172
192
  expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
173
193
  })
174
194
 
195
+ it('passes dmAgentEnabled to avatar when dmAgentEnabled is true for visitor view', () => {
196
+ const channel = createChannel({
197
+ currentUserId: 'visitor-1',
198
+ currentUserRole: 'owner',
199
+ })
200
+
201
+ renderWithProviders(
202
+ <ChannelView channel={channel} dmAgentEnabled={true} />
203
+ )
204
+
205
+ expect(avatarRenderCalls.length).toBeGreaterThan(0)
206
+ expect(
207
+ avatarRenderCalls.every((call) => call.dmAgentEnabled === true)
208
+ ).toBe(
209
+ true
210
+ )
211
+ })
212
+
213
+ it('renders dm agent helper text when dmAgentEnabled is true for visitor view', () => {
214
+ const channel = createChannel({
215
+ currentUserId: 'visitor-1',
216
+ currentUserRole: 'owner',
217
+ })
218
+
219
+ renderWithProviders(
220
+ <ChannelView channel={channel} dmAgentEnabled={true} />
221
+ )
222
+
223
+ expect(
224
+ screen.getAllByText('Replies instantly with AI assistant').length
225
+ ).toBeGreaterThan(0)
226
+ })
227
+
228
+ it('does not render dm agent helper text for linker view even when dmAgentEnabled is true', () => {
229
+ const channel = createChannel({
230
+ currentUserId: 'linker-1',
231
+ currentUserRole: 'member',
232
+ })
233
+
234
+ renderWithProviders(
235
+ <ChannelView channel={channel} dmAgentEnabled={true} />
236
+ )
237
+
238
+ expect(
239
+ screen.queryByText('Replies instantly with AI assistant')
240
+ ).not.toBeInTheDocument()
241
+ expect(
242
+ avatarRenderCalls.every((call) => call.dmAgentEnabled === false)
243
+ ).toBe(true)
244
+ })
245
+
175
246
  it('keeps channel banner and input action renderers working', () => {
176
247
  const channel = createChannel()
177
248
  const renderMessageInputActions = vi.fn((currentChannel: Channel) => (
@@ -2,6 +2,7 @@ import {
2
2
  ArrowLeftIcon,
3
3
  CaretRightIcon,
4
4
  DotsThreeIcon,
5
+ SparkleIcon,
5
6
  StarIcon,
6
7
  } from '@phosphor-icons/react'
7
8
  import classNames from 'classnames'
@@ -32,6 +33,7 @@ import { LoadingState } from './MessagingShell/LoadingState'
32
33
 
33
34
  const ICON_BTN_CLASS =
34
35
  'size-10 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
36
+ const DM_AGENT_HEADER_HELPER_TEXT = 'Replies instantly with AI assistant'
35
37
 
36
38
  /**
37
39
  * Custom channel header component
@@ -42,12 +44,14 @@ const CustomChannelHeader: React.FC<{
42
44
  onShowInfo: () => void
43
45
  canShowInfo: boolean
44
46
  showStarButton?: boolean
47
+ dmAgentEnabled?: boolean
45
48
  }> = ({
46
49
  onBack,
47
50
  showBackButton,
48
51
  onShowInfo,
49
52
  canShowInfo,
50
53
  showStarButton = false,
54
+ dmAgentEnabled = false,
51
55
  }) => {
52
56
  const { channel } = useChannelStateContext()
53
57
 
@@ -100,6 +104,7 @@ const CustomChannelHeader: React.FC<{
100
104
  name={participantName}
101
105
  image={participantImage}
102
106
  starred={showStarButton && isStarred}
107
+ dmAgentEnabled={dmAgentEnabled}
103
108
  size={40}
104
109
  />
105
110
  <button
@@ -111,6 +116,12 @@ const CustomChannelHeader: React.FC<{
111
116
  {participantName}
112
117
  <CaretRightIcon className="size-3 shrink-0" />
113
118
  </button>
119
+ {dmAgentEnabled && (
120
+ <div className="flex items-center gap-1 text-[10px] leading-3 text-black/55">
121
+ <SparkleIcon className="size-3 shrink-0 text-black/55" />
122
+ <span>{DM_AGENT_HEADER_HELPER_TEXT}</span>
123
+ </div>
124
+ )}
114
125
  </div>
115
126
  <div className="flex justify-end items-center gap-2">
116
127
  {showStarButton && (
@@ -159,6 +170,7 @@ const CustomChannelHeader: React.FC<{
159
170
  name={participantName}
160
171
  image={participantImage}
161
172
  starred={showStarButton && isStarred}
173
+ dmAgentEnabled={dmAgentEnabled}
162
174
  size={40}
163
175
  />
164
176
  <div className="min-w-0">
@@ -177,6 +189,12 @@ const CustomChannelHeader: React.FC<{
177
189
  {participantName}
178
190
  </h1>
179
191
  )}
192
+ {dmAgentEnabled && (
193
+ <div className="mt-0.5 flex items-center gap-1 text-[10px] leading-3 text-black/55">
194
+ <SparkleIcon className="size-3 shrink-0 text-black/55" />
195
+ <span className="truncate">{DM_AGENT_HEADER_HELPER_TEXT}</span>
196
+ </div>
197
+ )}
180
198
  </div>
181
199
  </div>
182
200
  <div className="flex items-center gap-2">
@@ -238,6 +256,7 @@ const ChannelViewInner: React.FC<{
238
256
  messageNode: React.ReactElement,
239
257
  message: NonNullable<MessageUIComponentProps['message']>
240
258
  ) => React.ReactNode
259
+ dmAgentEnabled?: boolean
241
260
  }> = ({
242
261
  onBack,
243
262
  showBackButton,
@@ -255,6 +274,7 @@ const ChannelViewInner: React.FC<{
255
274
  customProfileContent,
256
275
  customChannelActions,
257
276
  renderMessage,
277
+ dmAgentEnabled = false,
258
278
  }) => {
259
279
  const { channel } = useChannelStateContext()
260
280
  const infoDialogRef = useRef<HTMLDialogElement>(null)
@@ -267,6 +287,23 @@ const ChannelViewInner: React.FC<{
267
287
  )
268
288
  }, [channel._client.userID, channel.state.members])
269
289
 
290
+ const currentMember = React.useMemo(() => {
291
+ const members = Object.values(channel.state.members || {})
292
+ return members.find((member) => member.user?.id === channel._client.userID)
293
+ }, [channel._client.userID, channel.state.members])
294
+
295
+ const currentUserIsAccount =
296
+ (currentMember?.user as { is_account?: boolean } | undefined)?.is_account ??
297
+ (currentMember as { is_account?: boolean } | undefined)?.is_account
298
+ const participantIsAccount =
299
+ (participant?.user as { is_account?: boolean } | undefined)?.is_account ??
300
+ (participant as { is_account?: boolean } | undefined)?.is_account
301
+
302
+ const showDmAgentHeader =
303
+ dmAgentEnabled &&
304
+ currentUserIsAccount === false &&
305
+ participantIsAccount === true
306
+
270
307
  // Get follower status label from channel data
271
308
  const followerStatusLabel = React.useMemo(() => {
272
309
  const channelExtraData = (channel.data ?? {}) as {
@@ -326,6 +363,7 @@ const ChannelViewInner: React.FC<{
326
363
  onShowInfo={handleShowInfo}
327
364
  canShowInfo={Boolean(participant)}
328
365
  showStarButton={showStarButton}
366
+ dmAgentEnabled={showDmAgentHeader}
329
367
  />
330
368
  </div>
331
369
 
@@ -473,6 +511,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
473
511
  onBlockParticipantClick={onBlockParticipantClick}
474
512
  onReportParticipantClick={onReportParticipantClick}
475
513
  showStarButton={showStarButton}
514
+ dmAgentEnabled={dmAgentEnabled}
476
515
  chatbotVotingEnabled={chatbotVotingEnabled}
477
516
  renderChannelBanner={renderChannelBanner}
478
517
  customProfileContent={customProfileContent}
package/src/types.ts CHANGED
@@ -156,6 +156,8 @@ export interface ChannelViewProps {
156
156
  * messages will be sent with skip_push and silent flags to suppress
157
157
  * notifications to the creator until the agent responds.
158
158
  * The library reads chatbot_paused from channel.data internally.
159
+ *
160
+ * In header UI, this treatment is shown only for visitor view.
159
161
  */
160
162
  dmAgentEnabled?: boolean
161
163