@linktr.ee/messaging-react 1.25.1 → 1.26.1-rc-1776055454

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 (47) hide show
  1. package/dist/Preview-DqAv16NS.js +87 -0
  2. package/dist/Preview-DqAv16NS.js.map +1 -0
  3. package/dist/dash.all.min-Duv4lvGS.js +18858 -0
  4. package/dist/dash.all.min-Duv4lvGS.js.map +1 -0
  5. package/dist/hls-Bogc7CBn.js +21710 -0
  6. package/dist/hls-Bogc7CBn.js.map +1 -0
  7. package/dist/index-Da-xN4Yq.js +16142 -0
  8. package/dist/index-Da-xN4Yq.js.map +1 -0
  9. package/dist/index-Dj9rqWcU.js +69 -0
  10. package/dist/index-Dj9rqWcU.js.map +1 -0
  11. package/dist/index.d.ts +85 -9
  12. package/dist/index.js +1745 -1156
  13. package/dist/index.js.map +1 -1
  14. package/dist/mixin-B6jYfIcp.js +808 -0
  15. package/dist/mixin-B6jYfIcp.js.map +1 -0
  16. package/dist/react-BxlQMOfz.js +419 -0
  17. package/dist/react-BxlQMOfz.js.map +1 -0
  18. package/dist/react-COAP-MIW.js +377 -0
  19. package/dist/react-COAP-MIW.js.map +1 -0
  20. package/dist/react-Cn4WlMcl.js +3108 -0
  21. package/dist/react-Cn4WlMcl.js.map +1 -0
  22. package/dist/react-CwTJArKY.js +459 -0
  23. package/dist/react-CwTJArKY.js.map +1 -0
  24. package/dist/react-DkfS_atT.js +373 -0
  25. package/dist/react-DkfS_atT.js.map +1 -0
  26. package/dist/react-Pea5fum1.js +286 -0
  27. package/dist/react-Pea5fum1.js.map +1 -0
  28. package/dist/react-RiBbsUDd.js +534 -0
  29. package/dist/react-RiBbsUDd.js.map +1 -0
  30. package/dist/react-dS1WBxxz.js +238 -0
  31. package/dist/react-dS1WBxxz.js.map +1 -0
  32. package/package.json +2 -1
  33. package/src/components/ChannelList/index.test.tsx +18 -0
  34. package/src/components/ChannelList/index.tsx +2 -0
  35. package/src/components/ChannelView.test.tsx +50 -1
  36. package/src/components/ChannelView.tsx +13 -3
  37. package/src/components/CustomMessage/CustomMessage.stories.tsx +61 -2
  38. package/src/components/CustomMessage/MessageTag.tsx +5 -0
  39. package/src/components/CustomMessage/index.tsx +46 -4
  40. package/src/components/LockedAttachmentCard/LockedAttachmentCard.stories.tsx +351 -0
  41. package/src/components/LockedAttachmentCard/index.tsx +378 -0
  42. package/src/components/LockedAttachmentCard/mimeType.test.ts +97 -0
  43. package/src/components/LockedAttachmentCard/mimeType.ts +35 -0
  44. package/src/components/MessagingShell/index.tsx +2 -0
  45. package/src/index.ts +4 -0
  46. package/src/stream-custom-data.ts +10 -3
  47. package/src/types.ts +41 -0
@@ -1,5 +1,6 @@
1
1
  import classNames from 'classnames'
2
2
  import React, { useMemo, useState } from 'react'
3
+ import { type Channel, type LocalMessage } from 'stream-chat'
3
4
  import {
4
5
  Attachment as DefaultAttachment,
5
6
  EditMessageModal as DefaultEditMessageModal,
@@ -20,6 +21,7 @@ import {
20
21
  isMessageBounced,
21
22
  messageHasAttachments,
22
23
  messageHasReactions,
24
+ useChannelStateContext,
23
25
  useComponentContext,
24
26
  useChatContext,
25
27
  useMessageContext,
@@ -30,18 +32,23 @@ import {
30
32
 
31
33
  import { useMessageVote } from '../../hooks/useMessageVote'
32
34
  import { Avatar } from '../Avatar'
35
+ import LockedAttachmentCard from '../LockedAttachmentCard'
33
36
 
34
- import { MessageTag, isChatbotMessage, isTipOnlyMessage } from './MessageTag'
37
+ import { MessageTag, isAttachmentMessage, isChatbotMessage, isTipOnlyMessage } from './MessageTag'
35
38
  import { MessageVoteButtons } from './MessageVoteButtons'
36
39
 
37
40
  type CustomMessageWithContextProps = MessageContextValue & {
38
41
  chatbotVotingEnabled?: boolean
42
+ onAttachmentUnlock?: (message: LocalMessage, channel: Channel) => Promise<string>
43
+ onAttachmentDownload?: (message: LocalMessage, channel: Channel) => void
39
44
  }
40
45
 
41
46
  const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
42
47
  const {
43
48
  additionalMessageInputProps,
44
49
  chatbotVotingEnabled,
50
+ onAttachmentUnlock,
51
+ onAttachmentDownload,
45
52
  editing,
46
53
  endOfGroup,
47
54
  firstOfGroup,
@@ -58,7 +65,10 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
58
65
  } = props
59
66
 
60
67
  const { client } = useChatContext('CustomMessage')
68
+ const { channel } = useChannelStateContext('CustomMessage')
61
69
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
70
+ const [unlocking, setUnlocking] = useState(false)
71
+ const [attachmentSource, setAttachmentSource] = useState<string | undefined>(undefined)
62
72
  const reminder = useMessageReminder(message.id)
63
73
  const { selected: voteState, voteUp, voteDown } = useMessageVote(message)
64
74
 
@@ -144,6 +154,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
144
154
  const poll = message.poll_id && client.polls.fromState(message.poll_id)
145
155
  const isTipOnly = isTipOnlyMessage(message)
146
156
  const isChatbot = isChatbotMessage(message)
157
+ const isAttachment = isAttachmentMessage(message)
147
158
  const hasRenderableAttachments = !!(
148
159
  finalAttachments?.length && !message.quoted_message
149
160
  )
@@ -192,7 +203,29 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
192
203
  marginInlineStart: 0,
193
204
  }}
194
205
  >
195
- {isTipOnly ? (
206
+ {isAttachment ? (
207
+ <div className="flex flex-col gap-1">
208
+ <LockedAttachmentCard
209
+ title={message.metadata?.attachment_title ?? ''}
210
+ mimeType={message.metadata?.attachment_mime_type ?? 'application/octet-stream'}
211
+ thumbnail={message.metadata?.attachment_thumbnail}
212
+ amountText={message.metadata?.amount_text}
213
+ detail={message.metadata?.attachment_detail}
214
+ source={attachmentSource}
215
+ isPurchased={message.metadata?.payment_status === 'paid'}
216
+ loading={unlocking}
217
+ onUnlock={onAttachmentUnlock ? async () => { setUnlocking(true); try { setAttachmentSource(await onAttachmentUnlock(message, channel)) } catch { /* caller handles errors (e.g. payment cancellation) */ } finally { setUnlocking(false) } } : undefined}
218
+ onDownload={attachmentSource && onAttachmentDownload ? () => onAttachmentDownload(message, channel) : undefined}
219
+ />
220
+ {message.text && (
221
+ <div className="str-chat__message-bubble-wrapper">
222
+ <div className="str-chat__message-bubble">
223
+ <MessageText message={message} renderText={renderText} />
224
+ </div>
225
+ </div>
226
+ )}
227
+ </div>
228
+ ) : isTipOnly ? (
196
229
  /* Tip-only messages render as a standalone bubble */
197
230
  <MessageTag message={message} standalone />
198
231
  ) : (
@@ -256,11 +289,20 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
256
289
 
257
290
  const MemoizedCustomMessage = React.memo(
258
291
  CustomMessageWithContext,
259
- areMessageUIPropsEqual
292
+ (prev, next) => {
293
+ if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
294
+ if (prev.onAttachmentUnlock !== next.onAttachmentUnlock) return false
295
+ if (prev.onAttachmentDownload !== next.onAttachmentDownload) return false
296
+ return areMessageUIPropsEqual(prev, next)
297
+ }
260
298
  ) as typeof CustomMessageWithContext
261
299
 
262
300
  export const CustomMessage = (
263
- props: MessageUIComponentProps & { chatbotVotingEnabled?: boolean }
301
+ props: MessageUIComponentProps & {
302
+ chatbotVotingEnabled?: boolean
303
+ onAttachmentUnlock?: (message: LocalMessage, channel: Channel) => Promise<string>
304
+ onAttachmentDownload?: (message: LocalMessage, channel: Channel) => void
305
+ }
264
306
  ) => {
265
307
  const messageContext = useMessageContext('CustomMessage')
266
308
  return <MemoizedCustomMessage {...messageContext} {...props} />
@@ -0,0 +1,351 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React, { useState } from 'react'
3
+
4
+ import LockedAttachmentCard, { type LockedAttachmentCardProps } from '.'
5
+
6
+ const VIDEO_THUMBNAIL = '/video-thumbnail.jpg'
7
+ const VIDEO_THUMBNAIL_BLURRED = '/video-thumbnail-blurred.jpg'
8
+ const VIDEO_SOURCE = '/video-source.mp4'
9
+
10
+ const IMAGE_THUMBNAIL = '/image-thumbnail.jpg'
11
+ const IMAGE_THUMBNAIL_BLURRED = '/image-thumbnail-blurred.jpg'
12
+ const IMAGE_SOURCE = '/image-source.jpg'
13
+
14
+ const DOCUMENT_THUMBNAIL = '/document-thumbnail.jpg'
15
+ const DOCUMENT_THUMBNAIL_BLURRED = '/document-thumbnail-blurred.jpg'
16
+ const DOCUMENT_SOURCE = '/document-source.pdf'
17
+
18
+ const AUDIO_THUMBNAIL = '/audio-thumbnail.jpg'
19
+ const AUDIO_THUMBNAIL_BLURRED = '/audio-thumbnail-blurred.jpg'
20
+ const AUDIO_SOURCE = '/audio-source.mp3'
21
+
22
+ const meta: Meta = {
23
+ title: 'LockedAttachmentCard',
24
+ component: LockedAttachmentCard,
25
+ parameters: { layout: 'centered' },
26
+ }
27
+ export default meta
28
+
29
+ const Labeled = ({ label, children }: { label: string; children: React.ReactNode }) => (
30
+ <div className="flex flex-col gap-2">
31
+ {children}
32
+ <span className="text-center text-xs text-black/40">{label}</span>
33
+ </div>
34
+ )
35
+
36
+ type InteractiveProps = Omit<LockedAttachmentCardProps, 'onUnlock' | 'onDownload'> & {
37
+ unlockedSource: string
38
+ unlockedThumbnail?: string
39
+ }
40
+
41
+ const Interactive = ({ unlockedSource, unlockedThumbnail, ...props }: InteractiveProps) => {
42
+ const [source, setSource] = useState<string | undefined>(undefined)
43
+ const [thumbnail, setThumbnail] = useState(props.thumbnail)
44
+ const [loading, setLoading] = useState(false)
45
+
46
+ const handleUnlock = async () => {
47
+ setLoading(true)
48
+ await new Promise(resolve => setTimeout(resolve, 1000))
49
+ const res = await fetch(unlockedSource)
50
+ const blob = await res.blob()
51
+ setSource(URL.createObjectURL(blob))
52
+ if (unlockedThumbnail) setThumbnail(unlockedThumbnail)
53
+ setLoading(false)
54
+ }
55
+
56
+ return (
57
+ <LockedAttachmentCard
58
+ {...props}
59
+ thumbnail={thumbnail}
60
+ source={source}
61
+ loading={loading}
62
+ onUnlock={source ? undefined : handleUnlock}
63
+ onDownload={source ? () => {} : undefined}
64
+ />
65
+ )
66
+ }
67
+
68
+ export const Video: StoryFn = () => (
69
+ <div className="flex items-start gap-4 p-12">
70
+ <Labeled label="Locked">
71
+ <Interactive
72
+ title="Alicia's Workout Plan"
73
+ amountText="AU$9.99"
74
+ thumbnail={VIDEO_THUMBNAIL_BLURRED}
75
+ unlockedThumbnail={VIDEO_THUMBNAIL}
76
+ mimeType="video/mp4"
77
+ detail="1:20"
78
+ unlockedSource={VIDEO_SOURCE}
79
+ />
80
+ </Labeled>
81
+ <Labeled label="Purchased">
82
+ <Interactive
83
+ title="Alicia's Workout Plan"
84
+ amountText="AU$9.99"
85
+ thumbnail={VIDEO_THUMBNAIL_BLURRED}
86
+ unlockedThumbnail={VIDEO_THUMBNAIL}
87
+ mimeType="video/mp4"
88
+ detail="1:20"
89
+ unlockedSource={VIDEO_SOURCE}
90
+ isPurchased
91
+ />
92
+ </Labeled>
93
+ <Labeled label="Unlocked">
94
+ <LockedAttachmentCard
95
+ title="Alicia's Workout Plan"
96
+ thumbnail={VIDEO_THUMBNAIL}
97
+ source={VIDEO_SOURCE}
98
+ mimeType="video/mp4"
99
+ detail="1:20"
100
+ onDownload={() => {}}
101
+ />
102
+ </Labeled>
103
+ </div>
104
+ )
105
+
106
+ export const Audio: StoryFn = () => (
107
+ <div className="flex items-start gap-4 p-12">
108
+ <Labeled label="Locked">
109
+ <Interactive
110
+ title="Morning Meditation"
111
+ amountText="AU$4.99"
112
+ thumbnail={AUDIO_THUMBNAIL_BLURRED}
113
+ unlockedThumbnail={AUDIO_THUMBNAIL}
114
+ mimeType="audio/mpeg"
115
+ detail="4:35"
116
+ unlockedSource={AUDIO_SOURCE}
117
+ />
118
+ </Labeled>
119
+ <Labeled label="Purchased">
120
+ <Interactive
121
+ title="Morning Meditation"
122
+ amountText="AU$4.99"
123
+ thumbnail={AUDIO_THUMBNAIL_BLURRED}
124
+ unlockedThumbnail={AUDIO_THUMBNAIL}
125
+ mimeType="audio/mpeg"
126
+ detail="4:35"
127
+ unlockedSource={AUDIO_SOURCE}
128
+ isPurchased
129
+ />
130
+ </Labeled>
131
+ <Labeled label="Unlocked">
132
+ <LockedAttachmentCard
133
+ title="Morning Meditation"
134
+ thumbnail={AUDIO_THUMBNAIL}
135
+ source={AUDIO_SOURCE}
136
+ mimeType="audio/mpeg"
137
+ detail="4:35"
138
+ onDownload={() => {}}
139
+ />
140
+ </Labeled>
141
+ </div>
142
+ )
143
+
144
+ export const Image: StoryFn = () => (
145
+ <div className="flex items-start gap-4 p-12">
146
+ <Labeled label="Locked">
147
+ <Interactive
148
+ title="Picture of my cat"
149
+ amountText="AU$2.99"
150
+ thumbnail={IMAGE_THUMBNAIL_BLURRED}
151
+ unlockedThumbnail={IMAGE_THUMBNAIL}
152
+ mimeType="image/jpeg"
153
+ detail="3.2 MB"
154
+ unlockedSource={IMAGE_SOURCE}
155
+ />
156
+ </Labeled>
157
+ <Labeled label="Purchased">
158
+ <Interactive
159
+ title="Picture of my cat"
160
+ amountText="AU$2.99"
161
+ thumbnail={IMAGE_THUMBNAIL_BLURRED}
162
+ unlockedThumbnail={IMAGE_THUMBNAIL}
163
+ mimeType="image/jpeg"
164
+ detail="3.2 MB"
165
+ unlockedSource={IMAGE_SOURCE}
166
+ isPurchased
167
+ />
168
+ </Labeled>
169
+ <Labeled label="Unlocked">
170
+ <LockedAttachmentCard
171
+ title="Picture of my cat"
172
+ thumbnail={IMAGE_THUMBNAIL}
173
+ source={IMAGE_SOURCE}
174
+ mimeType="image/jpeg"
175
+ detail="3.2 MB"
176
+ onDownload={() => {}}
177
+ />
178
+ </Labeled>
179
+ </div>
180
+ )
181
+
182
+ export const Document: StoryFn = () => (
183
+ <div className="flex items-start gap-4 p-12">
184
+ <Labeled label="Locked">
185
+ <Interactive
186
+ title="Strength Training Guide"
187
+ amountText="AU$4.99"
188
+ thumbnail={DOCUMENT_THUMBNAIL_BLURRED}
189
+ unlockedThumbnail={DOCUMENT_THUMBNAIL}
190
+ mimeType="application/zip"
191
+ detail="14 files"
192
+ unlockedSource={DOCUMENT_SOURCE}
193
+ />
194
+ </Labeled>
195
+ <Labeled label="Purchased">
196
+ <Interactive
197
+ title="Strength Training Guide"
198
+ amountText="AU$4.99"
199
+ thumbnail={DOCUMENT_THUMBNAIL_BLURRED}
200
+ unlockedThumbnail={DOCUMENT_THUMBNAIL}
201
+ mimeType="application/zip"
202
+ detail="14 files"
203
+ unlockedSource={DOCUMENT_SOURCE}
204
+ isPurchased
205
+ />
206
+ </Labeled>
207
+ <Labeled label="Unlocked">
208
+ <LockedAttachmentCard
209
+ title="Strength Training Guide"
210
+ thumbnail={DOCUMENT_THUMBNAIL}
211
+ source={DOCUMENT_SOURCE}
212
+ mimeType="application/zip"
213
+ detail="14 files"
214
+ onDownload={() => {}}
215
+ />
216
+ </Labeled>
217
+ </div>
218
+ )
219
+
220
+ const VARIANTS = [
221
+ { label: 'Video', title: "Alicia's Workout Plan", mimeType: 'video/mp4', detail: '1:20', thumbnail: VIDEO_THUMBNAIL, thumbnailBlurred: VIDEO_THUMBNAIL_BLURRED, source: VIDEO_SOURCE },
222
+ { label: 'Audio', title: 'Morning Meditation', mimeType: 'audio/mpeg', detail: '4:35', thumbnail: AUDIO_THUMBNAIL, thumbnailBlurred: AUDIO_THUMBNAIL_BLURRED, source: AUDIO_SOURCE },
223
+ { label: 'Image', title: 'Picture of my cat', mimeType: 'image/jpeg', detail: '3.2 MB', thumbnail: IMAGE_THUMBNAIL, thumbnailBlurred: IMAGE_THUMBNAIL_BLURRED, source: IMAGE_SOURCE },
224
+ { label: 'Document', title: 'Strength Training Guide', mimeType: 'application/zip', detail: '14 files', thumbnail: DOCUMENT_THUMBNAIL, thumbnailBlurred: DOCUMENT_THUMBNAIL_BLURRED, source: DOCUMENT_SOURCE },
225
+ ]
226
+
227
+ export const Fallback: StoryFn = () => (
228
+ <div className="flex items-start gap-4 p-12">
229
+ <Labeled label="Locked">
230
+ <Interactive
231
+ title="Unknown Attachment"
232
+ amountText="AU$9.99"
233
+ mimeType="application/octet-stream"
234
+ unlockedSource={DOCUMENT_SOURCE}
235
+ />
236
+ </Labeled>
237
+ <Labeled label="Purchased">
238
+ <Interactive
239
+ title="Unknown Attachment"
240
+ amountText="AU$9.99"
241
+ mimeType="application/octet-stream"
242
+ unlockedSource={DOCUMENT_SOURCE}
243
+ isPurchased
244
+ />
245
+ </Labeled>
246
+ <Labeled label="Unlocked">
247
+ <LockedAttachmentCard
248
+ title="Unknown Attachment"
249
+ source={DOCUMENT_SOURCE}
250
+ mimeType="application/octet-stream"
251
+ onDownload={() => {}}
252
+ />
253
+ </Labeled>
254
+ </div>
255
+ )
256
+
257
+ const NO_POSTER_VARIANTS = [
258
+ { label: 'Video', title: "Alicia's Workout Plan", mimeType: 'video/mp4', detail: '1:20', source: VIDEO_SOURCE },
259
+ { label: 'Audio', title: 'Morning Meditation', mimeType: 'audio/mpeg', detail: '4:35', source: AUDIO_SOURCE },
260
+ { label: 'Image', title: 'Picture of my cat', mimeType: 'image/jpeg', detail: '3.2 MB', source: IMAGE_SOURCE },
261
+ { label: 'Document', title: 'Strength Training Guide', mimeType: 'application/zip', detail: '14 files', source: DOCUMENT_SOURCE },
262
+ ]
263
+
264
+ export const NoPoster: StoryFn = () => (
265
+ <div className="p-12">
266
+ <table className="border-separate border-spacing-4">
267
+ <thead>
268
+ <tr>
269
+ <th className="text-left text-xs font-medium text-black/40 pb-2" />
270
+ {NO_POSTER_VARIANTS.map(({ label, mimeType }) => (
271
+ <th key={mimeType} className="text-left text-xs font-medium text-black/40 pb-2">{label}</th>
272
+ ))}
273
+ </tr>
274
+ </thead>
275
+ <tbody>
276
+ {(['Locked', 'Unlocked'] as const).map(state => (
277
+ <tr key={state}>
278
+ <td className="text-xs font-medium text-black/40 pr-4 align-top pt-2">{state}</td>
279
+ {NO_POSTER_VARIANTS.map(({ title, mimeType, detail, source }) => (
280
+ <td key={mimeType} className="align-top">
281
+ {state === 'Locked' ? (
282
+ <Interactive
283
+ title={title}
284
+ amountText="AU$9.99"
285
+ mimeType={mimeType}
286
+ detail={detail}
287
+ unlockedSource={source}
288
+ />
289
+ ) : (
290
+ <LockedAttachmentCard
291
+ title={title}
292
+ source={source}
293
+ mimeType={mimeType}
294
+ detail={detail}
295
+ onDownload={() => {}}
296
+ />
297
+ )}
298
+ </td>
299
+ ))}
300
+ </tr>
301
+ ))}
302
+ </tbody>
303
+ </table>
304
+ </div>
305
+ )
306
+
307
+ export const CreatorView: StoryFn = () => (
308
+ <div className="p-12">
309
+ <table className="border-separate border-spacing-4">
310
+ <thead>
311
+ <tr>
312
+ <th className="text-left text-xs font-medium text-black/40 pb-2" />
313
+ {VARIANTS.map(({ label, mimeType }) => (
314
+ <th key={mimeType} className="text-left text-xs font-medium text-black/40 pb-2">{label}</th>
315
+ ))}
316
+ </tr>
317
+ </thead>
318
+ <tbody>
319
+ <tr>
320
+ <td className="text-xs font-medium text-black/40 pr-4 align-top pt-2">Unsold</td>
321
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailBlurred }) => (
322
+ <td key={mimeType} className="align-top">
323
+ <LockedAttachmentCard
324
+ title={title}
325
+ thumbnail={thumbnailBlurred}
326
+ mimeType={mimeType}
327
+ detail={detail}
328
+ amountText="AU$9.99"
329
+ />
330
+ </td>
331
+ ))}
332
+ </tr>
333
+ <tr>
334
+ <td className="text-xs font-medium text-black/40 pr-4 align-top pt-2">Sold</td>
335
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailBlurred }) => (
336
+ <td key={mimeType} className="align-top">
337
+ <LockedAttachmentCard
338
+ title={title}
339
+ thumbnail={thumbnailBlurred}
340
+ mimeType={mimeType}
341
+ detail={detail}
342
+ amountText="AU$9.99"
343
+ isPurchased
344
+ />
345
+ </td>
346
+ ))}
347
+ </tr>
348
+ </tbody>
349
+ </table>
350
+ </div>
351
+ )