@linktr.ee/messaging-react 2.0.0 → 2.0.1-rc-1778656305

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 (50) hide show
  1. package/dist/Card-CAC3fPjy.js +107 -0
  2. package/dist/Card-CAC3fPjy.js.map +1 -0
  3. package/dist/Card-DLUBUg_w.js +132 -0
  4. package/dist/Card-DLUBUg_w.js.map +1 -0
  5. package/dist/Card-_StSlnYh.js +163 -0
  6. package/dist/Card-_StSlnYh.js.map +1 -0
  7. package/dist/LockedThumbnail-p5RsFOug.js +220 -0
  8. package/dist/LockedThumbnail-p5RsFOug.js.map +1 -0
  9. package/dist/assets/index.css +1 -1
  10. package/dist/{index-Brz9orsI.js → index-B1h46F9x.js} +811 -772
  11. package/dist/index-B1h46F9x.js.map +1 -0
  12. package/dist/index.d.ts +87 -28
  13. package/dist/index.js +3 -3
  14. package/package.json +1 -1
  15. package/src/components/ChannelView.test.tsx +11 -0
  16. package/src/components/ChannelView.tsx +35 -32
  17. package/src/components/CustomMessage/index.tsx +2 -3
  18. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +57 -17
  19. package/src/components/CustomTypingIndicator/CustomTypingIndicator.test.tsx +187 -0
  20. package/src/components/CustomTypingIndicator/DmAgentContext.ts +3 -0
  21. package/src/components/CustomTypingIndicator/index.tsx +101 -37
  22. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +230 -89
  23. package/src/components/LockedAttachment/components/Composer/Card.tsx +221 -0
  24. package/src/components/LockedAttachment/components/Composer/index.ts +2 -0
  25. package/src/components/LockedAttachment/components/Received/Card.tsx +191 -0
  26. package/src/components/LockedAttachment/components/Received/CardActions.tsx +91 -0
  27. package/src/components/LockedAttachment/components/Received/index.ts +2 -0
  28. package/src/components/LockedAttachment/components/Sent/Card.tsx +177 -0
  29. package/src/components/LockedAttachment/components/Sent/index.ts +2 -0
  30. package/src/components/LockedAttachment/components/_shared/CardBody.tsx +94 -0
  31. package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +178 -0
  32. package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
  33. package/src/components/LockedAttachment/components/_shared/LockedCardShell.tsx +36 -0
  34. package/src/components/LockedAttachment/components/_shared/LockedThumbnail.tsx +128 -0
  35. package/src/components/LockedAttachment/index.tsx +43 -12
  36. package/src/components/LockedAttachment/types.ts +17 -0
  37. package/src/components/MediaMessage/index.tsx +2 -2
  38. package/src/index.ts +6 -1
  39. package/src/styles.css +7 -0
  40. package/dist/Card-BHknCeHw.js +0 -138
  41. package/dist/Card-BHknCeHw.js.map +0 -1
  42. package/dist/Card-DT7_ms2p.js +0 -127
  43. package/dist/Card-DT7_ms2p.js.map +0 -1
  44. package/dist/index-Brz9orsI.js.map +0 -1
  45. package/src/components/LockedAttachment/components/Creator/Card.tsx +0 -210
  46. package/src/components/LockedAttachment/components/Creator/index.tsx +0 -2
  47. package/src/components/LockedAttachment/components/Visitor/Card.tsx +0 -155
  48. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +0 -62
  49. package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +0 -12
  50. package/src/components/LockedAttachment/components/Visitor/index.ts +0 -2
@@ -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
@@ -1,6 +1,8 @@
1
1
  import type { Meta, StoryFn } from '@storybook/react'
2
2
  import React, { useState } from 'react'
3
3
 
4
+ import type { LockedAttachmentGalleryItem } from './types'
5
+
4
6
  import LockedAttachment from '.'
5
7
 
6
8
  const VIDEO_THUMBNAIL_BLURRED = '/video-thumbnail-blurred.jpg'
@@ -73,6 +75,37 @@ const VARIANTS = [
73
75
  },
74
76
  ]
75
77
 
78
+ /**
79
+ * A mixed-media collection (a video + 3 photos) used by the Gallery stories
80
+ * below. Each `LockedAttachment.*` card accepts a `gallery` prop and renders
81
+ * a carousel of these items when 2+ items are provided.
82
+ */
83
+ const GALLERY_ITEMS: LockedAttachmentGalleryItem[] = [
84
+ {
85
+ mimeType: 'video/mp4',
86
+ thumbnailUrl: VIDEO_THUMBNAIL_BLURRED,
87
+ sourceUrl: VIDEO_SOURCE,
88
+ },
89
+ {
90
+ mimeType: 'image/jpeg',
91
+ thumbnailUrl: IMAGE_THUMBNAIL_BLURRED,
92
+ sourceUrl: IMAGE_SOURCE,
93
+ },
94
+ {
95
+ mimeType: 'image/jpeg',
96
+ thumbnailUrl: DOCUMENT_THUMBNAIL_BLURRED,
97
+ sourceUrl: DOCUMENT_SOURCE,
98
+ },
99
+ {
100
+ mimeType: 'image/jpeg',
101
+ thumbnailUrl: AUDIO_THUMBNAIL_BLURRED,
102
+ sourceUrl: AUDIO_SOURCE,
103
+ },
104
+ ]
105
+
106
+ const GALLERY_DETAIL = '1 video, 3 photos'
107
+ const GALLERY_TITLE = 'Workout Guide'
108
+
76
109
  const Table = ({ children }: { children: React.ReactNode }) => (
77
110
  <div className="min-h-screen w-full p-12 bg-[#F9F7F4]">
78
111
  <table className="border-separate border-spacing-4">{children}</table>
@@ -99,91 +132,7 @@ const TableHead = ({
99
132
  </thead>
100
133
  )
101
134
 
102
- export const Received: StoryFn = () => {
103
- const [isPaid, setPaid] = useState<string | undefined>()
104
- const [isUnlocking, setUnlocking] = useState<string | undefined>()
105
-
106
- return (
107
- <Table>
108
- <TableHead variants={VARIANTS} />
109
- <tbody>
110
- <tr>
111
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
112
- Locked
113
- </td>
114
- {VARIANTS.map(
115
- ({
116
- title,
117
- mimeType,
118
- detail,
119
- thumbnailUrl,
120
- thumbnailUnlockedUrl,
121
- sourceUrl,
122
- }) => (
123
- <td key={mimeType} className="align-top">
124
- <LockedAttachment.Visitor
125
- title={title}
126
- thumbnailUrl={thumbnailUrl}
127
- mimeType={mimeType}
128
- detail={detail}
129
- paymentStatus={isPaid === mimeType ? 'paid' : undefined}
130
- amountText="AU$9.99"
131
- isUnlocking={isUnlocking === mimeType}
132
- onUnlockClick={() => {
133
- setUnlocking(mimeType)
134
- setTimeout(() => {
135
- setUnlocking(undefined)
136
- setPaid(mimeType)
137
- }, 1500)
138
- }}
139
- onDownloadClick={() => alert('Download clicked')}
140
- onFetchSource={async () => ({
141
- sourceUrl: sourceUrl,
142
- thumbnailUrl: thumbnailUnlockedUrl,
143
- })}
144
- />
145
- </td>
146
- )
147
- )}
148
- </tr>
149
- <tr>
150
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
151
- Unlocked
152
- </td>
153
- {VARIANTS.map(
154
- ({
155
- title,
156
- mimeType,
157
- detail,
158
- thumbnailUrl,
159
- thumbnailUnlockedUrl,
160
- sourceUrl,
161
- }) => (
162
- <td key={mimeType} className="align-top">
163
- <LockedAttachment.Visitor
164
- title={title}
165
- thumbnailUrl={thumbnailUrl}
166
- mimeType={mimeType}
167
- detail={detail}
168
- amountText="AU$9.99"
169
- paymentStatus="paid"
170
- onUnlockClick={() => alert('Unlock clicked')}
171
- onDownloadClick={() => alert('Download clicked')}
172
- onFetchSource={async () => ({
173
- sourceUrl: sourceUrl,
174
- thumbnailUrl: thumbnailUnlockedUrl,
175
- })}
176
- />
177
- </td>
178
- )
179
- )}
180
- </tr>
181
- </tbody>
182
- </Table>
183
- )
184
- }
185
-
186
- export const Sent: StoryFn = () => (
135
+ export const Composer: StoryFn = () => (
187
136
  <Table>
188
137
  <TableHead variants={VARIANTS} />
189
138
  <tbody>
@@ -200,7 +149,7 @@ export const Sent: StoryFn = () => (
200
149
  sourceUrl,
201
150
  }) => (
202
151
  <td key={mimeType} className="align-top">
203
- <LockedAttachment.Creator
152
+ <LockedAttachment.Composer
204
153
  placeholderTitle="Attachment title"
205
154
  placeholderAmountText="AU$0.00"
206
155
  thumbnailUrl={thumbnailUrl}
@@ -224,7 +173,7 @@ export const Sent: StoryFn = () => (
224
173
  </td>
225
174
  {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
226
175
  <td key={mimeType} className="align-top">
227
- <LockedAttachment.Creator
176
+ <LockedAttachment.Composer
228
177
  title={title}
229
178
  thumbnailUrl={thumbnailUrl}
230
179
  mimeType={mimeType}
@@ -235,6 +184,14 @@ export const Sent: StoryFn = () => (
235
184
  </td>
236
185
  ))}
237
186
  </tr>
187
+ </tbody>
188
+ </Table>
189
+ )
190
+
191
+ export const Sent: StoryFn = () => (
192
+ <Table>
193
+ <TableHead variants={VARIANTS} />
194
+ <tbody>
238
195
  <tr>
239
196
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
240
197
  Sent
@@ -249,7 +206,7 @@ export const Sent: StoryFn = () => (
249
206
  sourceUrl,
250
207
  }) => (
251
208
  <td key={mimeType} className="align-top">
252
- <LockedAttachment.Creator
209
+ <LockedAttachment.Sent
253
210
  title={title}
254
211
  thumbnailUrl={thumbnailUrl}
255
212
  mimeType={mimeType}
@@ -281,7 +238,7 @@ export const Sent: StoryFn = () => (
281
238
  sourceUrl,
282
239
  }) => (
283
240
  <td key={mimeType} className="align-top">
284
- <LockedAttachment.Creator
241
+ <LockedAttachment.Sent
285
242
  title={title}
286
243
  thumbnailUrl={thumbnailUrl}
287
244
  mimeType={mimeType}
@@ -303,3 +260,187 @@ export const Sent: StoryFn = () => (
303
260
  </tbody>
304
261
  </Table>
305
262
  )
263
+
264
+ export const Gallery: StoryFn = () => {
265
+ const [isPaid, setPaid] = useState(false)
266
+ const [isUnlocking, setUnlocking] = useState(false)
267
+
268
+ return (
269
+ <Table>
270
+ <thead>
271
+ <tr>
272
+ <th className="text-left text-xs font-medium text-black/40 pb-2" />
273
+ <th className="text-left text-xs font-medium text-black/40 pb-2">
274
+ Composer
275
+ </th>
276
+ <th className="text-left text-xs font-medium text-black/40 pb-2">
277
+ Sent
278
+ </th>
279
+ <th className="text-left text-xs font-medium text-black/40 pb-2">
280
+ Received
281
+ </th>
282
+ </tr>
283
+ </thead>
284
+ <tbody>
285
+ <tr>
286
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
287
+ Locked
288
+ </td>
289
+ <td className="align-top">
290
+ <LockedAttachment.Composer
291
+ title={GALLERY_TITLE}
292
+ detail={GALLERY_DETAIL}
293
+ amountText="AU$50.00"
294
+ gallery={GALLERY_ITEMS}
295
+ onDismiss={() => alert('Dismissed gallery')}
296
+ onEditClick={() => alert('Edit gallery')}
297
+ />
298
+ </td>
299
+ <td className="align-top">
300
+ <LockedAttachment.Sent
301
+ title={GALLERY_TITLE}
302
+ detail={GALLERY_DETAIL}
303
+ amountText="AU$50.00"
304
+ gallery={GALLERY_ITEMS}
305
+ />
306
+ </td>
307
+ <td className="align-top">
308
+ <LockedAttachment.Received
309
+ title={GALLERY_TITLE}
310
+ detail={GALLERY_DETAIL}
311
+ amountText="AU$50.00"
312
+ gallery={GALLERY_ITEMS}
313
+ paymentStatus={isPaid ? 'paid' : undefined}
314
+ isUnlocking={isUnlocking}
315
+ onUnlockClick={() => {
316
+ setUnlocking(true)
317
+ setTimeout(() => {
318
+ setUnlocking(false)
319
+ setPaid(true)
320
+ }, 1500)
321
+ }}
322
+ onDownloadClick={() => alert('Download gallery')}
323
+ />
324
+ </td>
325
+ </tr>
326
+ <tr>
327
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
328
+ Sold / Purchased
329
+ </td>
330
+ <td className="align-top">
331
+ <LockedAttachment.Composer
332
+ title={GALLERY_TITLE}
333
+ detail={GALLERY_DETAIL}
334
+ amountText="AU$50.00"
335
+ gallery={GALLERY_ITEMS}
336
+ onEditClick={() => alert('Edit gallery')}
337
+ />
338
+ </td>
339
+ <td className="align-top">
340
+ <LockedAttachment.Sent
341
+ title={GALLERY_TITLE}
342
+ detail={GALLERY_DETAIL}
343
+ amountText="AU$50.00"
344
+ paymentStatus="paid"
345
+ gallery={GALLERY_ITEMS}
346
+ />
347
+ </td>
348
+ <td className="align-top">
349
+ <LockedAttachment.Received
350
+ title={GALLERY_TITLE}
351
+ detail={GALLERY_DETAIL}
352
+ amountText="AU$50.00"
353
+ paymentStatus="paid"
354
+ gallery={GALLERY_ITEMS}
355
+ onDownloadClick={() => alert('Download gallery')}
356
+ />
357
+ </td>
358
+ </tr>
359
+ </tbody>
360
+ </Table>
361
+ )
362
+ }
363
+
364
+ export const Received: StoryFn = () => {
365
+ const [isPaid, setPaid] = useState<string | undefined>()
366
+ const [isUnlocking, setUnlocking] = useState<string | undefined>()
367
+
368
+ return (
369
+ <Table>
370
+ <TableHead variants={VARIANTS} />
371
+ <tbody>
372
+ <tr>
373
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
374
+ Locked
375
+ </td>
376
+ {VARIANTS.map(
377
+ ({
378
+ title,
379
+ mimeType,
380
+ detail,
381
+ thumbnailUrl,
382
+ thumbnailUnlockedUrl,
383
+ sourceUrl,
384
+ }) => (
385
+ <td key={mimeType} className="align-top">
386
+ <LockedAttachment.Received
387
+ title={title}
388
+ thumbnailUrl={thumbnailUrl}
389
+ mimeType={mimeType}
390
+ detail={detail}
391
+ paymentStatus={isPaid === mimeType ? 'paid' : undefined}
392
+ amountText="AU$9.99"
393
+ isUnlocking={isUnlocking === mimeType}
394
+ onUnlockClick={() => {
395
+ setUnlocking(mimeType)
396
+ setTimeout(() => {
397
+ setUnlocking(undefined)
398
+ setPaid(mimeType)
399
+ }, 1500)
400
+ }}
401
+ onDownloadClick={() => alert('Download clicked')}
402
+ onFetchSource={async () => ({
403
+ sourceUrl: sourceUrl,
404
+ thumbnailUrl: thumbnailUnlockedUrl,
405
+ })}
406
+ />
407
+ </td>
408
+ )
409
+ )}
410
+ </tr>
411
+ <tr>
412
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
413
+ Unlocked
414
+ </td>
415
+ {VARIANTS.map(
416
+ ({
417
+ title,
418
+ mimeType,
419
+ detail,
420
+ thumbnailUrl,
421
+ thumbnailUnlockedUrl,
422
+ sourceUrl,
423
+ }) => (
424
+ <td key={mimeType} className="align-top">
425
+ <LockedAttachment.Received
426
+ title={title}
427
+ thumbnailUrl={thumbnailUrl}
428
+ mimeType={mimeType}
429
+ detail={detail}
430
+ amountText="AU$9.99"
431
+ paymentStatus="paid"
432
+ onUnlockClick={() => alert('Unlock clicked')}
433
+ onDownloadClick={() => alert('Download clicked')}
434
+ onFetchSource={async () => ({
435
+ sourceUrl: sourceUrl,
436
+ thumbnailUrl: thumbnailUnlockedUrl,
437
+ })}
438
+ />
439
+ </td>
440
+ )
441
+ )}
442
+ </tr>
443
+ </tbody>
444
+ </Table>
445
+ )
446
+ }