@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
@@ -0,0 +1,97 @@
1
+ import { getDocumentIconType, getSourceType } from './mimeType'
2
+
3
+ describe('getSourceType', () => {
4
+ it('returns video for video/* types', () => {
5
+ expect(getSourceType('video/mp4')).toBe('video')
6
+ expect(getSourceType('video/webm')).toBe('video')
7
+ expect(getSourceType('video/quicktime')).toBe('video')
8
+ })
9
+
10
+ it('returns audio for audio/* types', () => {
11
+ expect(getSourceType('audio/mpeg')).toBe('audio')
12
+ expect(getSourceType('audio/mp4')).toBe('audio')
13
+ expect(getSourceType('audio/ogg')).toBe('audio')
14
+ })
15
+
16
+ it('returns image for image/* types', () => {
17
+ expect(getSourceType('image/jpeg')).toBe('image')
18
+ expect(getSourceType('image/png')).toBe('image')
19
+ expect(getSourceType('image/webp')).toBe('image')
20
+ })
21
+
22
+ it('returns document for application/* types', () => {
23
+ expect(getSourceType('application/pdf')).toBe('document')
24
+ expect(getSourceType('application/msword')).toBe('document')
25
+ expect(getSourceType('application/zip')).toBe('document')
26
+ })
27
+
28
+ it('returns document for text/* types', () => {
29
+ expect(getSourceType('text/plain')).toBe('document')
30
+ expect(getSourceType('text/csv')).toBe('document')
31
+ expect(getSourceType('text/markdown')).toBe('document')
32
+ })
33
+ })
34
+
35
+ describe('getDocumentIconType', () => {
36
+ it('returns pdf for application/pdf', () => {
37
+ expect(getDocumentIconType('application/pdf')).toBe('pdf')
38
+ })
39
+
40
+ it('returns doc for Word types', () => {
41
+ expect(getDocumentIconType('application/msword')).toBe('doc')
42
+ expect(getDocumentIconType('application/vnd.openxmlformats-officedocument.wordprocessingml.document')).toBe('doc')
43
+ })
44
+
45
+ it('returns xls for Excel types', () => {
46
+ expect(getDocumentIconType('application/vnd.ms-excel')).toBe('xls')
47
+ expect(getDocumentIconType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')).toBe('xls')
48
+ })
49
+
50
+ it('returns csv for text/csv', () => {
51
+ expect(getDocumentIconType('text/csv')).toBe('csv')
52
+ })
53
+
54
+ it('returns ppt for PowerPoint types', () => {
55
+ expect(getDocumentIconType('application/vnd.ms-powerpoint')).toBe('ppt')
56
+ expect(getDocumentIconType('application/vnd.openxmlformats-officedocument.presentationml.presentation')).toBe('ppt')
57
+ })
58
+
59
+ it('returns zip for archive types', () => {
60
+ expect(getDocumentIconType('application/zip')).toBe('zip')
61
+ expect(getDocumentIconType('application/x-zip-compressed')).toBe('zip')
62
+ expect(getDocumentIconType('application/gzip')).toBe('zip')
63
+ expect(getDocumentIconType('application/x-gzip')).toBe('zip')
64
+ expect(getDocumentIconType('application/x-rar-compressed')).toBe('zip')
65
+ expect(getDocumentIconType('application/x-7z-compressed')).toBe('zip')
66
+ expect(getDocumentIconType('application/x-tar')).toBe('zip')
67
+ })
68
+
69
+ it('returns text for plain text and rtf', () => {
70
+ expect(getDocumentIconType('text/plain')).toBe('text')
71
+ expect(getDocumentIconType('text/rtf')).toBe('text')
72
+ expect(getDocumentIconType('application/rtf')).toBe('text')
73
+ expect(getDocumentIconType('application/x-rtf')).toBe('text')
74
+ })
75
+
76
+ it('returns markdown for markdown types', () => {
77
+ expect(getDocumentIconType('text/markdown')).toBe('markdown')
78
+ expect(getDocumentIconType('text/x-markdown')).toBe('markdown')
79
+ })
80
+
81
+ it('returns xls for macro-enabled Excel', () => {
82
+ expect(getDocumentIconType('application/vnd.ms-excel.sheet.macroEnabled.12')).toBe('xls')
83
+ })
84
+
85
+ it('returns ppt for macro-enabled PowerPoint', () => {
86
+ expect(getDocumentIconType('application/vnd.ms-powerpoint.presentation.macroEnabled.12')).toBe('ppt')
87
+ })
88
+
89
+ it('returns generic for unknown types', () => {
90
+ expect(getDocumentIconType('application/octet-stream')).toBe('generic')
91
+ expect(getDocumentIconType('application/json')).toBe('generic')
92
+ expect(getDocumentIconType('text/html')).toBe('generic')
93
+ expect(getDocumentIconType('application/vnd.rar')).toBe('generic')
94
+ expect(getDocumentIconType('application/vnd.oasis.opendocument.text')).toBe('generic')
95
+ expect(getDocumentIconType('application/vnd.oasis.opendocument.spreadsheet')).toBe('generic')
96
+ })
97
+ })
@@ -0,0 +1,35 @@
1
+ export type AttachmentSourceType = 'image' | 'audio' | 'video' | 'document'
2
+
3
+ export type DocumentIconType =
4
+ | 'pdf'
5
+ | 'doc'
6
+ | 'xls'
7
+ | 'csv'
8
+ | 'ppt'
9
+ | 'zip'
10
+ | 'text'
11
+ | 'markdown'
12
+ | 'generic'
13
+
14
+ const DOCUMENT_ICON_PATTERNS: Array<[RegExp, DocumentIconType]> = [
15
+ [/pdf/, 'pdf'],
16
+ [/wordprocessingml|msword|\.doc/, 'doc'],
17
+ [/spreadsheetml|ms-excel|\.xls/, 'xls'],
18
+ [/csv/, 'csv'],
19
+ [/presentationml|ms-powerpoint|\.ppt/, 'ppt'],
20
+ [/zip|x-rar|x-7z|x-tar|x-gzip/, 'zip'],
21
+ [/plain|rtf/, 'text'],
22
+ [/markdown/, 'markdown'],
23
+ ]
24
+
25
+ export function getSourceType(mimeType: string): AttachmentSourceType {
26
+ if (mimeType.startsWith('video/')) return 'video'
27
+ if (mimeType.startsWith('audio/')) return 'audio'
28
+ if (mimeType.startsWith('image/')) return 'image'
29
+ return 'document'
30
+ }
31
+
32
+ export function getDocumentIconType(mimeType: string): DocumentIconType {
33
+ const match = DOCUMENT_ICON_PATTERNS.find(([pattern]) => pattern.test(mimeType))
34
+ return match ? match[1] : 'generic'
35
+ }
@@ -32,6 +32,12 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
32
32
  // Track if we've already loaded participants to prevent repeated loading
33
33
  const loadedRef = useRef(false)
34
34
 
35
+ // New source instance should be allowed to load again
36
+ useEffect(() => {
37
+ loadedRef.current = false
38
+ }, [participantSource])
39
+
40
+ /* eslint-disable react-hooks/exhaustive-deps -- syncs with participantSource + debug; inner async uses participantSource.loadParticipants */
35
41
  // Load participants initially - wait for participantSource to finish loading first
36
42
  useEffect(() => {
37
43
  // Wait for the participantSource to finish loading before we try to load participants
@@ -78,7 +84,8 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
78
84
  }
79
85
 
80
86
  loadInitialParticipants()
81
- }, [participantSource.loading, debug]) // Re-run when loading state changes
87
+ }, [participantSource, debug])
88
+ /* eslint-enable react-hooks/exhaustive-deps */
82
89
 
83
90
  // Filter participants by search query and existing participants
84
91
  const availableParticipants = participants
@@ -74,10 +74,11 @@ export const useParticipants = (
74
74
  loadParticipants(true);
75
75
  }, [loadParticipants]);
76
76
 
77
- // Initial load - only run once when participantSource changes
77
+ /* eslint-disable react-hooks/exhaustive-deps -- initial load only; `loadParticipants` changes whenever `loading` flips */
78
78
  useEffect(() => {
79
79
  loadParticipants(true);
80
- }, [participantSource.loadParticipants]); // Only depend on the function to avoid loops
80
+ }, [participantSource.loadParticipants]);
81
+ /* eslint-enable react-hooks/exhaustive-deps */
81
82
 
82
83
  return {
83
84
  participants,
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { MessagingShell } from './components/MessagingShell'
6
6
  export { ChannelList } from './components/ChannelList'
7
7
  export { ChannelView } from './components/ChannelView'
8
8
  export { default as ActionButton } from './components/ActionButton'
9
+ export { default as LockedAttachment } from './components/LockedAttachment'
9
10
  export { ParticipantPicker } from './components/ParticipantPicker'
10
11
  export { Avatar } from './components/Avatar'
11
12
  export { FaqList } from './components/FaqList'
@@ -34,10 +35,13 @@ export type {
34
35
  MessagingCapabilities,
35
36
  ParticipantSource,
36
37
  Participant,
38
+ LockedAttachmentSource,
37
39
  } from './types'
38
40
  export type { MessageMetadata } from './stream-custom-data'
39
41
  export type { AvatarProps } from './components/Avatar'
40
42
  export type { ActionButtonProps } from './components/ActionButton'
43
+ export type { LockedAttachmentProps } from './components/LockedAttachment'
44
+ export type { AttachmentSourceType } from './components/LockedAttachment/utils/mimeType'
41
45
  export type { Faq, FaqListProps } from './components/FaqList'
42
46
  export type { FaqListItemProps } from './components/FaqList/FaqListItem'
43
47
  export type { VoteSelection } from './hooks/useMessageVote'
@@ -0,0 +1,37 @@
1
+ import type { ArgTypes } from '@storybook/react'
2
+
3
+ export const storyUsers = {
4
+ creator: {
5
+ id: 'creator-user',
6
+ name: 'Creator',
7
+ image: 'https://i.pravatar.cc/150?img=1',
8
+ },
9
+ visitor: {
10
+ id: 'visitor-user',
11
+ name: 'Visitor',
12
+ image: 'https://i.pravatar.cc/150?img=5',
13
+ },
14
+ } as const
15
+
16
+ export type StoryRole = keyof typeof storyUsers
17
+
18
+ export type StoryUser = typeof storyUsers[keyof typeof storyUsers]
19
+
20
+ const DEFAULT_ROLE: StoryRole = 'creator'
21
+
22
+ /**
23
+ * Storybook control: pick viewer identity; `args.currentUser` is the full user object.
24
+ * Use with `key={currentUser.id}` on the story root so Stream client remounts when it changes.
25
+ */
26
+ export const currentUserArgType: ArgTypes<{ currentUser?: StoryUser }> = {
27
+ currentUser: {
28
+ name: 'participant',
29
+ control: { type: 'inline-radio' as const },
30
+ options: ['creator', 'visitor'] as StoryRole[],
31
+ mapping: {
32
+ creator: storyUsers.creator,
33
+ visitor: storyUsers.visitor,
34
+ },
35
+ table: { defaultValue: { summary: DEFAULT_ROLE } },
36
+ },
37
+ } satisfies ArgTypes<{ currentUser?: StoryUser }>
@@ -24,6 +24,7 @@ export type MessageCustomType =
24
24
  | 'MESSAGE_TIP'
25
25
  | 'MESSAGE_PAID'
26
26
  | 'MESSAGE_CHATBOT'
27
+ | 'MESSAGE_ATTACHMENT'
27
28
  | AgeSafetySystemType
28
29
  | DmAgentSystemType
29
30
 
@@ -31,12 +32,17 @@ export type MessageCustomType =
31
32
  * Message metadata for paid messaging and chatbot flows.
32
33
  * Used to identify message types and payment status.
33
34
  */
35
+ export type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded'
36
+
34
37
  export interface MessageMetadata {
35
38
  custom_type?: MessageCustomType
36
- amount_text?: string
37
- payment_status?: string
38
- payment_intent_id?: string
39
39
  listing_id?: string
40
+ amount_text?: string
41
+ payment_status?: PaymentStatus
42
+ attachment_title?: string
43
+ attachment_mime_type?: string
44
+ attachment_thumbnail?: string
45
+ attachment_detail?: string
40
46
  }
41
47
 
42
48
  declare module 'stream-chat' {
package/src/types.ts CHANGED
@@ -117,6 +117,11 @@ export interface ChannelListProps {
117
117
  ) => React.ReactNode
118
118
  }
119
119
 
120
+ export interface LockedAttachmentSource {
121
+ source: string
122
+ poster?: string
123
+ }
124
+
120
125
  /**
121
126
  * ChannelView component props
122
127
  */
@@ -163,7 +168,7 @@ export interface ChannelViewProps {
163
168
  * @example
164
169
  * messageMetadata={{ custom_type: 'MESSAGE_PAID', listing_id: '...' }}
165
170
  */
166
- messageMetadata?: Pick<MessageMetadata, 'custom_type' | 'listing_id'>
171
+ messageMetadata?: Partial<MessageMetadata>
167
172
 
168
173
  /**
169
174
  * Callback fired after a message is successfully sent.
@@ -231,6 +236,19 @@ export interface ChannelViewProps {
231
236
  messageNode: React.ReactElement,
232
237
  message: LocalMessage
233
238
  ) => React.ReactNode
239
+
240
+ /**
241
+ * Called when the visitor clicks Unlock on a locked attachment message.
242
+ * Receives the message and channel. Show checkout, confirm payment, fetch
243
+ * the unlocked URL. `attachment_source` must NOT be stored on the Stream message metadata.
244
+ * The card shows a loading state for the full duration of the promise.
245
+ */
246
+ onAttachmentUnlock?: (message: LocalMessage, channel: Channel) => Promise<LockedAttachmentSource>
247
+
248
+ /**
249
+ * Called when the visitor clicks Download on an unlocked attachment message.
250
+ */
251
+ onAttachmentDownload?: (message: LocalMessage, channel: Channel) => void
234
252
  }
235
253
 
236
254
  /**
@@ -254,6 +272,8 @@ export type ChannelViewPassthroughProps = Pick<
254
272
  | 'customProfileContent'
255
273
  | 'customChannelActions'
256
274
  | 'renderMessage'
275
+ | 'onAttachmentUnlock'
276
+ | 'onAttachmentDownload'
257
277
  >
258
278
 
259
279
  /**
@@ -0,0 +1,10 @@
1
+ /**
2
+ * True in Vite dev builds (`import.meta.env.DEV`). Uses a type assertion so
3
+ * `tsc` does not rely on ambient `ImportMeta` merging (vite/client).
4
+ */
5
+ export function isDevBuild(): boolean {
6
+ return (
7
+ typeof import.meta !== 'undefined' &&
8
+ (import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV === true
9
+ )
10
+ }