@linktr.ee/messaging-react 1.26.1 → 1.27.0

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 (58) hide show
  1. package/dist/Creator-B6M8dB0U.js +87 -0
  2. package/dist/Creator-B6M8dB0U.js.map +1 -0
  3. package/dist/MediaPlayer-DsjlYGGH.js +539 -0
  4. package/dist/MediaPlayer-DsjlYGGH.js.map +1 -0
  5. package/dist/Preview-DqAv16NS.js +87 -0
  6. package/dist/Preview-DqAv16NS.js.map +1 -0
  7. package/dist/Visitor-CpmFZRGO.js +175 -0
  8. package/dist/Visitor-CpmFZRGO.js.map +1 -0
  9. package/dist/dash.all.min-Duv4lvGS.js +18858 -0
  10. package/dist/dash.all.min-Duv4lvGS.js.map +1 -0
  11. package/dist/hls-Bogc7CBn.js +21710 -0
  12. package/dist/hls-Bogc7CBn.js.map +1 -0
  13. package/dist/index-Da-xN4Yq.js +16142 -0
  14. package/dist/index-Da-xN4Yq.js.map +1 -0
  15. package/dist/index-Dj9rqWcU.js +69 -0
  16. package/dist/index-Dj9rqWcU.js.map +1 -0
  17. package/dist/index.d.ts +73 -10
  18. package/dist/index.js +979 -934
  19. package/dist/index.js.map +1 -1
  20. package/dist/mixin-B6jYfIcp.js +808 -0
  21. package/dist/mixin-B6jYfIcp.js.map +1 -0
  22. package/dist/react-BxlQMOfz.js +419 -0
  23. package/dist/react-BxlQMOfz.js.map +1 -0
  24. package/dist/react-COAP-MIW.js +377 -0
  25. package/dist/react-COAP-MIW.js.map +1 -0
  26. package/dist/react-Cn4WlMcl.js +3108 -0
  27. package/dist/react-Cn4WlMcl.js.map +1 -0
  28. package/dist/react-CwTJArKY.js +459 -0
  29. package/dist/react-CwTJArKY.js.map +1 -0
  30. package/dist/react-DkfS_atT.js +373 -0
  31. package/dist/react-DkfS_atT.js.map +1 -0
  32. package/dist/react-Pea5fum1.js +286 -0
  33. package/dist/react-Pea5fum1.js.map +1 -0
  34. package/dist/react-RiBbsUDd.js +534 -0
  35. package/dist/react-RiBbsUDd.js.map +1 -0
  36. package/dist/react-dS1WBxxz.js +238 -0
  37. package/dist/react-dS1WBxxz.js.map +1 -0
  38. package/package.json +2 -1
  39. package/src/components/ChannelView.tsx +12 -2
  40. package/src/components/CustomMessage/CustomMessage.stories.tsx +173 -41
  41. package/src/components/CustomMessage/MessageTag.tsx +5 -0
  42. package/src/components/CustomMessage/index.tsx +43 -4
  43. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +249 -0
  44. package/src/components/LockedAttachment/components/Creator.tsx +171 -0
  45. package/src/components/LockedAttachment/components/MediaPlayer.tsx +299 -0
  46. package/src/components/LockedAttachment/components/Visitor.tsx +293 -0
  47. package/src/components/LockedAttachment/index.tsx +39 -0
  48. package/src/components/LockedAttachment/types.ts +18 -0
  49. package/src/components/LockedAttachment/utils/icons.ts +52 -0
  50. package/src/components/LockedAttachment/utils/mimeType.test.ts +97 -0
  51. package/src/components/LockedAttachment/utils/mimeType.ts +35 -0
  52. package/src/components/ParticipantPicker/index.tsx +8 -1
  53. package/src/hooks/useParticipants.ts +3 -2
  54. package/src/index.ts +4 -0
  55. package/src/stories/decorators/storyUser.tsx +37 -0
  56. package/src/stream-custom-data.ts +9 -3
  57. package/src/types.ts +21 -1
  58. package/src/utils/isDevBuild.ts +10 -0
@@ -1,37 +1,38 @@
1
+ import '../../stream-custom-data'
2
+
1
3
  import type { Meta, StoryFn } from '@storybook/react'
2
4
  import React, { useEffect } from 'react'
3
5
  import {
4
6
  Channel as ChannelType,
7
+ LocalMessage,
5
8
  QueryChannelAPIResponse,
6
9
  StreamChat,
7
10
  } from 'stream-chat'
8
11
  import { Channel, Chat, MessageList, Window } from 'stream-chat-react'
9
12
 
10
- import { mockParticipants } from '../../stories/mocks'
13
+ import {
14
+ currentUserArgType,
15
+ StoryUser,
16
+ storyUsers,
17
+ } from '../../stories/decorators/storyUser'
11
18
 
12
19
  import { CustomMessage } from './index'
13
20
 
14
21
  const meta: Meta = {
15
22
  title: 'Components/CustomMessage',
16
23
  component: CustomMessage,
24
+ argTypes: currentUserArgType,
25
+ args: { currentUser: storyUsers.creator },
17
26
  parameters: {
18
27
  layout: 'fullscreen',
19
28
  },
20
29
  }
21
30
  export default meta
22
31
 
23
- const mockUser = {
24
- id: 'storybook-user',
25
- name: 'Storybook User',
26
- image: 'https://i.pravatar.cc/150?img=1',
27
- }
28
-
29
32
  const createMockChannel = async (
30
33
  client: StreamChat,
31
34
  messages: TemplateProps['messages']
32
35
  ) => {
33
- const participant = mockParticipants[0]
34
-
35
36
  const mockMessages = messages.map((msg, index) => ({
36
37
  ...msg,
37
38
  type: msg.type ?? ('regular' as const),
@@ -50,7 +51,7 @@ const createMockChannel = async (
50
51
  }))
51
52
 
52
53
  const channelData = {
53
- members: [mockUser.id, participant.id],
54
+ members: [storyUsers.creator.id, storyUsers.visitor.id],
54
55
  }
55
56
 
56
57
  const channel = client.channel(
@@ -63,8 +64,14 @@ const createMockChannel = async (
63
64
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
65
  channel.state.messages = mockMessages as unknown as any[]
65
66
  channel.state.members = {
66
- [mockUser.id]: { user: mockUser, user_id: mockUser.id },
67
- [participant.id]: { user: participant, user_id: participant.id },
67
+ [storyUsers.creator.id]: {
68
+ user: storyUsers.creator,
69
+ user_id: storyUsers.creator.id,
70
+ },
71
+ [storyUsers.visitor.id]: {
72
+ user: storyUsers.visitor,
73
+ user_id: storyUsers.visitor.id,
74
+ },
68
75
  }
69
76
  return {
70
77
  channel: channelData,
@@ -86,24 +93,36 @@ const createMockChannel = async (
86
93
  }
87
94
 
88
95
  interface TemplateProps {
96
+ currentUser: StoryUser
89
97
  messages: Array<{
90
98
  id: string
91
99
  text: string
92
- user: typeof mockUser | { id: string; name: string }
100
+ user: StoryUser
93
101
  type?: 'regular' | 'system'
94
102
  attachments?: Array<Record<string, unknown>>
95
103
  metadata?: {
96
- custom_type?: 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT'
104
+ custom_type?:
105
+ | 'MESSAGE_TIP'
106
+ | 'MESSAGE_PAID'
107
+ | 'MESSAGE_CHATBOT'
108
+ | 'MESSAGE_ATTACHMENT'
97
109
  amount_text?: string
110
+ attachment_title?: string
111
+ attachment_mime_type?: string
112
+ attachment_thumbnail?: string
113
+ attachment_detail?: string
98
114
  }
99
115
  }>
100
116
  }
101
117
 
102
- const Template: StoryFn<TemplateProps> = ({ messages }) => {
118
+ const TemplateInner: React.FC<{
119
+ currentUser: StoryUser
120
+ messages: TemplateProps['messages']
121
+ }> = ({ currentUser, messages }) => {
103
122
  const [client] = React.useState(() => {
104
123
  const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
105
- c.userID = mockUser.id
106
- c.user = mockUser
124
+ c.userID = currentUser.id
125
+ c.user = currentUser
107
126
  return c
108
127
  })
109
128
 
@@ -113,6 +132,32 @@ const Template: StoryFn<TemplateProps> = ({ messages }) => {
113
132
  createMockChannel(client, messages).then(setChannel)
114
133
  }, [client, messages])
115
134
 
135
+ const isVisitor = currentUser.id === storyUsers.visitor.id
136
+
137
+ const MessageComponent = React.useMemo(() => {
138
+ const handleAttachmentUnlock = async () => {
139
+ await new Promise((resolve) => setTimeout(resolve, 1000))
140
+ return { source: '/video-source.mp4', poster: '/video-thumbnail.jpg' }
141
+ }
142
+
143
+ const handleAttachmentDownload = (m: LocalMessage) => {
144
+ alert(`Download: ${m.metadata?.attachment_title}`)
145
+ }
146
+
147
+ // eslint-disable-next-line react/display-name
148
+ return function (props) {
149
+ return (
150
+ <CustomMessage
151
+ {...props}
152
+ onAttachmentUnlock={isVisitor ? handleAttachmentUnlock : undefined}
153
+ onAttachmentDownload={
154
+ isVisitor ? handleAttachmentDownload : undefined
155
+ }
156
+ />
157
+ )
158
+ }
159
+ }, [isVisitor])
160
+
116
161
  if (!channel) {
117
162
  return <div className="p-4">Loading...</div>
118
163
  }
@@ -120,7 +165,7 @@ const Template: StoryFn<TemplateProps> = ({ messages }) => {
120
165
  return (
121
166
  <Chat client={client}>
122
167
  <div className="h-screen w-full bg-white">
123
- <Channel channel={channel} Message={CustomMessage}>
168
+ <Channel channel={channel} Message={MessageComponent}>
124
169
  <Window>
125
170
  <MessageList />
126
171
  </Window>
@@ -130,81 +175,168 @@ const Template: StoryFn<TemplateProps> = ({ messages }) => {
130
175
  )
131
176
  }
132
177
 
133
- const participant = mockParticipants[0]
178
+ const Template: StoryFn<TemplateProps> = ({
179
+ currentUser = storyUsers.creator,
180
+ messages,
181
+ }) => (
182
+ <TemplateInner
183
+ key={currentUser.id}
184
+ currentUser={currentUser}
185
+ messages={messages}
186
+ />
187
+ )
134
188
 
135
189
  export const Default: StoryFn<TemplateProps> = Template.bind({})
136
190
  Default.args = {
137
191
  messages: [
138
- { id: 'msg-1', text: 'Hey, how are you?', user: participant },
139
- { id: 'msg-2', text: "I'm doing great, thanks!", user: mockUser },
140
- { id: 'msg-3', text: 'Awesome! Have a good day.', user: participant },
192
+ {
193
+ id: 'msg-1',
194
+ text: 'Hey, how are you?',
195
+ user: storyUsers.visitor,
196
+ },
197
+ {
198
+ id: 'msg-2',
199
+ text: "I'm doing great, thanks!",
200
+ user: storyUsers.creator,
201
+ },
202
+ {
203
+ id: 'msg-3',
204
+ text: 'Awesome! Have a good day.',
205
+ user: storyUsers.visitor,
206
+ },
141
207
  ],
142
208
  }
143
209
 
144
210
  export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})
145
211
  WithTipTag.args = {
146
212
  messages: [
147
- { id: 'msg-1', text: 'Love your content!', user: participant },
213
+ {
214
+ id: 'msg-1',
215
+ text: 'Love your content!',
216
+ user: storyUsers.visitor,
217
+ },
148
218
  {
149
219
  id: 'msg-2',
150
220
  text: "Here's a tip for you! Keep up the great work.",
151
- user: participant,
221
+ user: storyUsers.visitor,
152
222
  metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
153
223
  },
154
- { id: 'msg-3', text: 'Thank you so much! 🙏', user: mockUser },
224
+ {
225
+ id: 'msg-3',
226
+ text: 'Thank you so much! 🙏',
227
+ user: storyUsers.creator,
228
+ },
155
229
  ],
156
230
  }
157
231
 
158
232
  export const TipOnly: StoryFn<TemplateProps> = Template.bind({})
159
233
  TipOnly.args = {
160
234
  messages: [
161
- { id: 'msg-1', text: 'Hello!', user: participant },
235
+ {
236
+ id: 'msg-1',
237
+ text: 'Hello!',
238
+ user: storyUsers.visitor,
239
+ },
162
240
  {
163
241
  id: 'msg-2',
164
242
  text: '',
165
- user: participant,
243
+ user: storyUsers.visitor,
166
244
  metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
167
245
  },
168
- { id: 'msg-3', text: 'Wow, thank you for the tip!', user: mockUser },
246
+ {
247
+ id: 'msg-3',
248
+ text: 'Wow, thank you for the tip!',
249
+ user: storyUsers.creator,
250
+ },
169
251
  ],
170
252
  }
171
253
 
172
254
  export const MixedTags: StoryFn<TemplateProps> = Template.bind({})
173
255
  MixedTags.args = {
174
256
  messages: [
175
- { id: 'msg-1', text: 'Regular message from a fan', user: participant },
257
+ {
258
+ id: 'msg-1',
259
+ text: 'Regular message from a fan',
260
+ user: storyUsers.visitor,
261
+ },
176
262
  {
177
263
  id: 'msg-2',
178
264
  text: 'I wanted to tip you for your amazing work!',
179
- user: participant,
265
+ user: storyUsers.visitor,
180
266
  metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
181
267
  },
182
- { id: 'msg-3', text: 'Thank you!', user: mockUser },
268
+ {
269
+ id: 'msg-3',
270
+ text: 'Thank you!',
271
+ user: storyUsers.creator,
272
+ },
183
273
  {
184
274
  id: 'msg-4',
185
275
  text: '',
186
- user: participant,
276
+ user: storyUsers.visitor,
187
277
  metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$25.00' },
188
278
  },
189
279
  {
190
280
  id: 'msg-5',
191
281
  text: 'Wow, a tip with no message! Thanks!',
192
- user: mockUser,
282
+ user: storyUsers.creator,
193
283
  },
194
284
  {
195
285
  id: 'msg-6',
196
286
  text: 'This is a paid message!',
197
- user: participant,
287
+ user: storyUsers.visitor,
198
288
  metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' },
199
289
  },
200
- { id: 'msg-7', text: 'Got it, thanks!', user: mockUser },
290
+ {
291
+ id: 'msg-7',
292
+ text: 'Got it, thanks!',
293
+ user: storyUsers.creator,
294
+ },
201
295
  {
202
296
  id: 'msg-8',
203
297
  text: 'This is from a chatbot!',
204
- user: participant,
298
+ user: storyUsers.creator,
205
299
  metadata: { custom_type: 'MESSAGE_CHATBOT' },
206
300
  },
207
- { id: 'msg-9', text: 'Thanks for letting me know.', user: mockUser },
301
+ {
302
+ id: 'msg-9',
303
+ text: 'Thanks for letting me know.',
304
+ user: storyUsers.creator,
305
+ },
306
+ ],
307
+ }
308
+
309
+ export const PaidAttachment: StoryFn<TemplateProps> = Template.bind({})
310
+ PaidAttachment.args = {
311
+ messages: [
312
+ {
313
+ id: 'msg-1',
314
+ text: 'Can I have your workout plan?',
315
+ user: storyUsers.visitor,
316
+ },
317
+ {
318
+ id: 'msg-2',
319
+ text: 'yes here it is',
320
+ user: storyUsers.creator,
321
+ metadata: {
322
+ custom_type: 'MESSAGE_ATTACHMENT',
323
+ amount_text: 'AU$9.99',
324
+ attachment_title: "Alicia's Workout Plan",
325
+ attachment_mime_type: 'video/mp4',
326
+ attachment_thumbnail: '/video-thumbnail-blurred.jpg',
327
+ attachment_detail: '1:20',
328
+ },
329
+ },
330
+ {
331
+ id: 'msg-3',
332
+ text: 'let me know if you have any questions!',
333
+ user: storyUsers.creator,
334
+ },
335
+ {
336
+ id: 'msg-4',
337
+ text: 'Looks amazing, unlocking now!',
338
+ user: storyUsers.visitor,
339
+ },
208
340
  ],
209
341
  }
210
342
 
@@ -214,19 +346,19 @@ ChatbotVariants.args = {
214
346
  {
215
347
  id: 'msg-1',
216
348
  text: 'Would you like to share some details that I can relay to Rupi Kaur?',
217
- user: participant,
349
+ user: storyUsers.creator,
218
350
  metadata: { custom_type: 'MESSAGE_CHATBOT' },
219
351
  },
220
352
  {
221
353
  id: 'msg-2',
222
- text: 'I’m sorry to hear that. Are you referring to the course youve recently purchased?',
223
- user: mockUser,
354
+ text: "I'm sorry to hear that. Are you referring to the course you've recently purchased?",
355
+ user: storyUsers.creator,
224
356
  metadata: { custom_type: 'MESSAGE_CHATBOT' },
225
357
  },
226
358
  {
227
359
  id: 'msg-3',
228
360
  text: '',
229
- user: mockUser,
361
+ user: storyUsers.creator,
230
362
  metadata: { custom_type: 'MESSAGE_CHATBOT' },
231
363
  attachments: [
232
364
  {
@@ -42,6 +42,11 @@ export const isChatbotMessage = (message: LocalMessage): boolean => {
42
42
  return message.metadata?.custom_type === 'MESSAGE_CHATBOT'
43
43
  }
44
44
 
45
+ /** Check if a message is a locked attachment */
46
+ export const isAttachmentMessage = (message: LocalMessage): boolean => {
47
+ return message.metadata?.custom_type === 'MESSAGE_ATTACHMENT'
48
+ }
49
+
45
50
  /** Check if a message has a tip/paid tag (both render the same) */
46
51
  export const isTipOrPaidMessage = (message: LocalMessage): boolean => {
47
52
  return isTipMessage(message) || isPaidMessage(message)
@@ -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 LockedAttachment, { type LockedAttachmentSource } from '../LockedAttachment'
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<LockedAttachmentSource>
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,6 +65,7 @@ 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)
62
70
  const reminder = useMessageReminder(message.id)
63
71
  const { selected: voteState, voteUp, voteDown } = useMessageVote(message)
@@ -144,6 +152,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
144
152
  const poll = message.poll_id && client.polls.fromState(message.poll_id)
145
153
  const isTipOnly = isTipOnlyMessage(message)
146
154
  const isChatbot = isChatbotMessage(message)
155
+ const isAttachment = isAttachmentMessage(message)
147
156
  const hasRenderableAttachments = !!(
148
157
  finalAttachments?.length && !message.quoted_message
149
158
  )
@@ -192,7 +201,28 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
192
201
  marginInlineStart: 0,
193
202
  }}
194
203
  >
195
- {isTipOnly ? (
204
+ {isAttachment ? (
205
+ <div className="flex flex-col gap-2">
206
+ <LockedAttachment
207
+ isCreator={isMine}
208
+ title={message.metadata?.attachment_title}
209
+ mimeType={message.metadata?.attachment_mime_type}
210
+ thumbnail={message.metadata?.attachment_thumbnail}
211
+ amountText={message.metadata?.amount_text}
212
+ detail={message.metadata?.attachment_detail}
213
+ paymentStatus={message.metadata?.payment_status}
214
+ onUnlock={onAttachmentUnlock ? () => onAttachmentUnlock(message, channel) : undefined}
215
+ onDownload={onAttachmentDownload ? () => onAttachmentDownload(message, channel) : undefined}
216
+ />
217
+ {message.text && (
218
+ <div className="str-chat__message-bubble-wrapper">
219
+ <div className="str-chat__message-bubble">
220
+ <MessageText message={message} renderText={renderText} />
221
+ </div>
222
+ </div>
223
+ )}
224
+ </div>
225
+ ) : isTipOnly ? (
196
226
  /* Tip-only messages render as a standalone bubble */
197
227
  <MessageTag message={message} standalone />
198
228
  ) : (
@@ -256,11 +286,20 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
256
286
 
257
287
  const MemoizedCustomMessage = React.memo(
258
288
  CustomMessageWithContext,
259
- areMessageUIPropsEqual
289
+ (prev, next) => {
290
+ if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
291
+ if (prev.onAttachmentUnlock !== next.onAttachmentUnlock) return false
292
+ if (prev.onAttachmentDownload !== next.onAttachmentDownload) return false
293
+ return areMessageUIPropsEqual(prev, next)
294
+ }
260
295
  ) as typeof CustomMessageWithContext
261
296
 
262
297
  export const CustomMessage = (
263
- props: MessageUIComponentProps & { chatbotVotingEnabled?: boolean }
298
+ props: MessageUIComponentProps & {
299
+ chatbotVotingEnabled?: boolean
300
+ onAttachmentUnlock?: (message: LocalMessage, channel: Channel) => Promise<LockedAttachmentSource>
301
+ onAttachmentDownload?: (message: LocalMessage, channel: Channel) => void
302
+ }
264
303
  ) => {
265
304
  const messageContext = useMessageContext('CustomMessage')
266
305
  return <MemoizedCustomMessage {...messageContext} {...props} />