@linktr.ee/messaging-react 1.26.1 → 1.28.0-rc-1776225927

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-D38dWn2X.js +318 -0
  2. package/dist/Creator-D38dWn2X.js.map +1 -0
  3. package/dist/MediaPlayer-DE9MC6k6.js +599 -0
  4. package/dist/MediaPlayer-DE9MC6k6.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-BG-9-3HU.js +199 -0
  8. package/dist/Visitor-BG-9-3HU.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 +74 -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 +343 -0
  44. package/src/components/LockedAttachment/components/Creator.tsx +469 -0
  45. package/src/components/LockedAttachment/components/MediaPlayer.tsx +359 -0
  46. package/src/components/LockedAttachment/components/Visitor.tsx +356 -0
  47. package/src/components/LockedAttachment/index.tsx +39 -0
  48. package/src/components/LockedAttachment/types.ts +17 -0
  49. package/src/components/LockedAttachment/utils/icons.ts +53 -0
  50. package/src/components/LockedAttachment/utils/mimeType.test.ts +119 -0
  51. package/src/components/LockedAttachment/utils/mimeType.ts +37 -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 +20 -1
  58. package/src/utils/isDevBuild.ts +10 -0
@@ -0,0 +1,39 @@
1
+ import React, { Suspense } from 'react'
2
+
3
+ import type { CreatorCardProps } from './components/Creator'
4
+ import type { VisitorCardProps } from './components/Visitor'
5
+
6
+ const CreatorCardLazy = React.lazy(() => import('./components/Creator'))
7
+ const VisitorCardLazy = React.lazy(() => import('./components/Visitor'))
8
+
9
+ const LockedAttachmentFallback = () => (
10
+ <div
11
+ className="w-[280px] min-h-[200px] animate-pulse rounded-3xl bg-black/[0.06] shadow-[0px_0px_0px_1px_rgba(0,0,0,0.04),0px_1px_2px_0px_rgba(0,0,0,0.04)]"
12
+ aria-hidden
13
+ />
14
+ )
15
+
16
+ export type LockedAttachmentProps =
17
+ | ({ isCreator: true } & CreatorCardProps)
18
+ | ({ isCreator?: false } & VisitorCardProps)
19
+
20
+ const LockedAttachment = (props: LockedAttachmentProps) => {
21
+ if (props.isCreator) {
22
+ const { isCreator: _, ...rest } = props
23
+ return (
24
+ <Suspense fallback={<LockedAttachmentFallback />}>
25
+ <CreatorCardLazy {...rest} />
26
+ </Suspense>
27
+ )
28
+ }
29
+ const { isCreator: _, ...rest } = props
30
+ return (
31
+ <Suspense fallback={<LockedAttachmentFallback />}>
32
+ <VisitorCardLazy {...rest} />
33
+ </Suspense>
34
+ )
35
+ }
36
+
37
+ export default LockedAttachment
38
+ export type { CreatorCardProps, VisitorCardProps }
39
+ export type { PaymentStatus, LockedAttachmentSource } from './types'
@@ -0,0 +1,17 @@
1
+ import type { PaymentStatus } from '../../stream-custom-data'
2
+
3
+ /** Shared fields for creator and visitor locked-attachment cards (internal). */
4
+ export interface LockedAttachmentBaseProps {
5
+ title?: string
6
+ mimeType?: string
7
+ /** Preview image. Video/image: pass blurred version. Audio/document: pass unblurred version. */
8
+ thumbnail?: string
9
+ /** Unlocked media URL. Undefined while locked or pending unlock. */
10
+ source?: string
11
+ detail?: string
12
+ amountText?: string
13
+ paymentStatus?: PaymentStatus
14
+ }
15
+
16
+ export type { PaymentStatus }
17
+ export type { LockedAttachmentSource } from '../../types'
@@ -0,0 +1,53 @@
1
+ import {
2
+ FileIcon,
3
+ FileCsvIcon,
4
+ FileDocIcon,
5
+ FileMdIcon,
6
+ FilePdfIcon,
7
+ FilePptIcon,
8
+ FileTextIcon,
9
+ FileXlsIcon,
10
+ FileZipIcon,
11
+ ImageIcon,
12
+ SpeakerHighIcon,
13
+ VideoCameraIcon,
14
+ IconProps,
15
+ } from '@phosphor-icons/react'
16
+ import React from 'react'
17
+
18
+ import { getDocumentIconType, getSourceType } from './mimeType'
19
+ import type { AttachmentSourceType } from './mimeType'
20
+
21
+ export const MEDIA_TYPE_ICON: Record<AttachmentSourceType, React.ElementType> =
22
+ {
23
+ video: VideoCameraIcon,
24
+ audio: SpeakerHighIcon,
25
+ image: ImageIcon,
26
+ document: FileIcon,
27
+ }
28
+
29
+ const DOCUMENT_ICON_COMPONENT = {
30
+ pdf: FilePdfIcon,
31
+ doc: FileDocIcon,
32
+ xls: FileXlsIcon,
33
+ csv: FileCsvIcon,
34
+ ppt: FilePptIcon,
35
+ zip: FileZipIcon,
36
+ text: FileTextIcon,
37
+ markdown: FileMdIcon,
38
+ generic: FileIcon,
39
+ } as const
40
+
41
+ export function getTypeIcon(mimeType: string): React.ElementType {
42
+ const sourceType = getSourceType(mimeType)
43
+ if (sourceType !== 'document') return MEDIA_TYPE_ICON[sourceType]
44
+ return DOCUMENT_ICON_COMPONENT[getDocumentIconType(mimeType)]
45
+ }
46
+
47
+ /** Use instead of `<TypeIcon />` where TypeIcon = getTypeIcon(mime) to satisfy react-hooks/static-components. */
48
+ export function renderTypeIcon(
49
+ mimeType: string,
50
+ props: IconProps
51
+ ): React.ReactElement {
52
+ return React.createElement(getTypeIcon(mimeType), props)
53
+ }
@@ -0,0 +1,119 @@
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(
43
+ getDocumentIconType(
44
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
45
+ )
46
+ ).toBe('doc')
47
+ })
48
+
49
+ it('returns xls for Excel types', () => {
50
+ expect(getDocumentIconType('application/vnd.ms-excel')).toBe('xls')
51
+ expect(
52
+ getDocumentIconType(
53
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
54
+ )
55
+ ).toBe('xls')
56
+ })
57
+
58
+ it('returns csv for text/csv', () => {
59
+ expect(getDocumentIconType('text/csv')).toBe('csv')
60
+ })
61
+
62
+ it('returns ppt for PowerPoint types', () => {
63
+ expect(getDocumentIconType('application/vnd.ms-powerpoint')).toBe('ppt')
64
+ expect(
65
+ getDocumentIconType(
66
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
67
+ )
68
+ ).toBe('ppt')
69
+ })
70
+
71
+ it('returns zip for archive types', () => {
72
+ expect(getDocumentIconType('application/zip')).toBe('zip')
73
+ expect(getDocumentIconType('application/x-zip-compressed')).toBe('zip')
74
+ expect(getDocumentIconType('application/gzip')).toBe('zip')
75
+ expect(getDocumentIconType('application/x-gzip')).toBe('zip')
76
+ expect(getDocumentIconType('application/x-rar-compressed')).toBe('zip')
77
+ expect(getDocumentIconType('application/x-7z-compressed')).toBe('zip')
78
+ expect(getDocumentIconType('application/x-tar')).toBe('zip')
79
+ })
80
+
81
+ it('returns text for plain text and rtf', () => {
82
+ expect(getDocumentIconType('text/plain')).toBe('text')
83
+ expect(getDocumentIconType('text/rtf')).toBe('text')
84
+ expect(getDocumentIconType('application/rtf')).toBe('text')
85
+ expect(getDocumentIconType('application/x-rtf')).toBe('text')
86
+ })
87
+
88
+ it('returns markdown for markdown types', () => {
89
+ expect(getDocumentIconType('text/markdown')).toBe('markdown')
90
+ expect(getDocumentIconType('text/x-markdown')).toBe('markdown')
91
+ })
92
+
93
+ it('returns xls for macro-enabled Excel', () => {
94
+ expect(
95
+ getDocumentIconType('application/vnd.ms-excel.sheet.macroEnabled.12')
96
+ ).toBe('xls')
97
+ })
98
+
99
+ it('returns ppt for macro-enabled PowerPoint', () => {
100
+ expect(
101
+ getDocumentIconType(
102
+ 'application/vnd.ms-powerpoint.presentation.macroEnabled.12'
103
+ )
104
+ ).toBe('ppt')
105
+ })
106
+
107
+ it('returns generic for unknown types', () => {
108
+ expect(getDocumentIconType('application/octet-stream')).toBe('generic')
109
+ expect(getDocumentIconType('application/json')).toBe('generic')
110
+ expect(getDocumentIconType('text/html')).toBe('generic')
111
+ expect(getDocumentIconType('application/vnd.rar')).toBe('generic')
112
+ expect(getDocumentIconType('application/vnd.oasis.opendocument.text')).toBe(
113
+ 'generic'
114
+ )
115
+ expect(
116
+ getDocumentIconType('application/vnd.oasis.opendocument.spreadsheet')
117
+ ).toBe('generic')
118
+ })
119
+ })
@@ -0,0 +1,37 @@
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]) =>
34
+ pattern.test(mimeType)
35
+ )
36
+ return match ? match[1] : 'generic'
37
+ }
@@ -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,10 @@ export interface ChannelListProps {
117
117
  ) => React.ReactNode
118
118
  }
119
119
 
120
+ export interface LockedAttachmentSource {
121
+ source: string
122
+ }
123
+
120
124
  /**
121
125
  * ChannelView component props
122
126
  */
@@ -163,7 +167,7 @@ export interface ChannelViewProps {
163
167
  * @example
164
168
  * messageMetadata={{ custom_type: 'MESSAGE_PAID', listing_id: '...' }}
165
169
  */
166
- messageMetadata?: Pick<MessageMetadata, 'custom_type' | 'listing_id'>
170
+ messageMetadata?: Partial<MessageMetadata>
167
171
 
168
172
  /**
169
173
  * Callback fired after a message is successfully sent.
@@ -231,6 +235,19 @@ export interface ChannelViewProps {
231
235
  messageNode: React.ReactElement,
232
236
  message: LocalMessage
233
237
  ) => React.ReactNode
238
+
239
+ /**
240
+ * Called when the visitor clicks Unlock on a locked attachment message.
241
+ * Receives the message and channel. Show checkout, confirm payment, fetch
242
+ * the unlocked URL. `attachment_source` must NOT be stored on the Stream message metadata.
243
+ * The card shows a loading state for the full duration of the promise.
244
+ */
245
+ onAttachmentUnlock?: (message: LocalMessage, channel: Channel) => Promise<LockedAttachmentSource>
246
+
247
+ /**
248
+ * Called when the visitor clicks Download on an unlocked attachment message.
249
+ */
250
+ onAttachmentDownload?: (message: LocalMessage, channel: Channel) => void
234
251
  }
235
252
 
236
253
  /**
@@ -254,6 +271,8 @@ export type ChannelViewPassthroughProps = Pick<
254
271
  | 'customProfileContent'
255
272
  | 'customChannelActions'
256
273
  | 'renderMessage'
274
+ | 'onAttachmentUnlock'
275
+ | 'onAttachmentDownload'
257
276
  >
258
277
 
259
278
  /**
@@ -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
+ }