@linktr.ee/messaging-react 1.31.0-rc-1776748331 → 1.32.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.31.0-rc-1776748331",
3
+ "version": "1.32.0",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,7 +39,8 @@ const mockUser = {
39
39
  const createMockChannel = async (
40
40
  client: StreamChat,
41
41
  hasMessages = true,
42
- followerStatus?: string | boolean
42
+ followerStatus?: string | boolean,
43
+ isFrozen = false
43
44
  ) => {
44
45
  const participant = mockParticipants[0]
45
46
 
@@ -105,6 +106,7 @@ const createMockChannel = async (
105
106
  // Prepare channel data with optional follower status
106
107
  const channelData: Record<string, unknown> = {
107
108
  members: [mockUser.id, participant.id],
109
+ frozen: isFrozen,
108
110
  }
109
111
 
110
112
  // Add follower status if provided
@@ -155,10 +157,13 @@ const createMockChannel = async (
155
157
  return channel
156
158
  }
157
159
 
158
- type TemplateProps = ComponentProps & { followerStatus?: string | boolean }
160
+ type TemplateProps = ComponentProps & {
161
+ followerStatus?: string | boolean
162
+ isFrozen?: boolean
163
+ }
159
164
 
160
165
  const Template: StoryFn<TemplateProps> = (args) => {
161
- const { followerStatus, ...channelViewProps } = args
166
+ const { followerStatus, isFrozen = false, ...channelViewProps } = args
162
167
  const [client] = React.useState(() => {
163
168
  const client = new StreamChat('mock-api-key', {
164
169
  allowServerSideConnect: true,
@@ -171,10 +176,12 @@ const Template: StoryFn<TemplateProps> = (args) => {
171
176
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
172
177
 
173
178
  useEffect(() => {
174
- createMockChannel(client, true, followerStatus).then((mockChannel) => {
175
- setChannel(mockChannel)
176
- })
177
- }, [client, followerStatus])
179
+ createMockChannel(client, true, followerStatus, isFrozen).then(
180
+ (mockChannel) => {
181
+ setChannel(mockChannel)
182
+ }
183
+ )
184
+ }, [client, followerStatus, isFrozen])
178
185
 
179
186
  if (!channel) {
180
187
  return <div>Loading...</div>
@@ -524,3 +531,27 @@ NoFollowerStatus.parameters = {
524
531
  },
525
532
  },
526
533
  }
534
+
535
+ export const FrozenChannel: StoryFn<TemplateProps> = Template.bind({})
536
+ FrozenChannel.args = {
537
+ showBackButton: false,
538
+ isFrozen: true,
539
+ renderMessageInputActions: (channel) => (
540
+ <button
541
+ onClick={() => console.log('Custom action clicked', channel.id)}
542
+ className="p-2 hover:bg-sand rounded-lg"
543
+ aria-label="Attach file"
544
+ type="button"
545
+ >
546
+ 📎
547
+ </button>
548
+ ),
549
+ }
550
+ FrozenChannel.parameters = {
551
+ docs: {
552
+ description: {
553
+ story:
554
+ 'Channel view for a frozen conversation. The message composer renders in its disabled frozen state while the rest of the conversation remains readable.',
555
+ },
556
+ },
557
+ }
@@ -0,0 +1,127 @@
1
+ import React from 'react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { renderWithProviders, screen } from '../../test/utils'
5
+
6
+ import { CustomMessageInput } from '.'
7
+
8
+ let mockChannelData: Record<string, unknown> = {}
9
+
10
+ vi.mock('stream-chat-react', () => ({
11
+ MessageInput: ({
12
+ Input,
13
+ }: {
14
+ Input: React.ComponentType
15
+ }) => (
16
+ <div data-testid="stream-message-input">
17
+ <Input />
18
+ </div>
19
+ ),
20
+ SimpleAttachmentSelector: () => (
21
+ <div data-testid="simple-attachment-selector" />
22
+ ),
23
+ TextareaComposer: ({
24
+ maxRows: _maxRows,
25
+ ...props
26
+ }: React.TextareaHTMLAttributes<HTMLTextAreaElement> & { maxRows?: number }) => (
27
+ <textarea data-testid="textarea-composer" {...props} />
28
+ ),
29
+ AttachmentPreviewList: () => <div data-testid="attachment-preview-list" />,
30
+ QuotedMessagePreview: () => <div data-testid="quoted-message-preview" />,
31
+ useChannelStateContext: () => ({
32
+ channel: { data: mockChannelData },
33
+ }),
34
+ useMessageInputContext: () => ({
35
+ handleSubmit: vi.fn(),
36
+ }),
37
+ useMessageComposerHasSendableData: () => false,
38
+ }))
39
+
40
+ vi.mock('../CustomLinkPreviewList', () => ({
41
+ CustomLinkPreviewList: () => <div data-testid="custom-link-preview-list" />,
42
+ }))
43
+
44
+ describe('CustomMessageInput', () => {
45
+ it('renders the interactive message input when channel is not frozen', () => {
46
+ mockChannelData = {}
47
+
48
+ const { container } = renderWithProviders(<CustomMessageInput />)
49
+
50
+ const messageInput = container.firstElementChild
51
+ expect(messageInput).not.toHaveAttribute('aria-disabled')
52
+ expect(messageInput).not.toHaveAttribute('inert')
53
+ expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
54
+ })
55
+
56
+ it('renders the frozen message input when channel is frozen', () => {
57
+ mockChannelData = { frozen: true }
58
+
59
+ const { container } = renderWithProviders(<CustomMessageInput />)
60
+
61
+ const messageInput = container.firstElementChild
62
+ expect(messageInput).toHaveAttribute('aria-disabled', 'true')
63
+ expect(messageInput).toHaveAttribute('inert')
64
+ expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
65
+ })
66
+
67
+ it('makes the textarea read-only and disables send when channel is frozen', () => {
68
+ mockChannelData = { frozen: true }
69
+
70
+ renderWithProviders(<CustomMessageInput />)
71
+
72
+ const textarea = screen.getByTestId('textarea-composer')
73
+ expect(textarea).toHaveAttribute('aria-disabled', 'true')
74
+ expect(textarea).toHaveAttribute('readonly')
75
+ expect(textarea).toHaveAttribute('tabindex', '-1')
76
+ expect(textarea).not.toHaveAttribute('autofocus')
77
+
78
+ const sendButton = screen.getByRole('button', { name: /send/i })
79
+ expect(sendButton).toBeDisabled()
80
+ })
81
+
82
+ it('renders the existing attachment selector when channel is frozen', () => {
83
+ mockChannelData = { frozen: true }
84
+
85
+ renderWithProviders(<CustomMessageInput />)
86
+
87
+ expect(screen.getByTestId('simple-attachment-selector')).toBeInTheDocument()
88
+ })
89
+
90
+ it('renders adjacent actions inside the disabled container when frozen', () => {
91
+ mockChannelData = { frozen: true }
92
+
93
+ const { container } = renderWithProviders(
94
+ <CustomMessageInput
95
+ renderActions={() => (
96
+ <button data-testid="custom-action" type="button">
97
+ Action
98
+ </button>
99
+ )}
100
+ />
101
+ )
102
+
103
+ const messageInput = container.firstElementChild
104
+ const action = screen.getByTestId('custom-action')
105
+
106
+ expect(messageInput).toHaveAttribute('aria-disabled', 'true')
107
+ expect(messageInput?.contains(action)).toBe(true)
108
+ })
109
+
110
+ it('renders adjacent actions in the interactive container when not frozen', () => {
111
+ mockChannelData = {}
112
+
113
+ const { container } = renderWithProviders(
114
+ <CustomMessageInput
115
+ renderActions={() => (
116
+ <button data-testid="custom-action" type="button">
117
+ Action
118
+ </button>
119
+ )}
120
+ />
121
+ )
122
+
123
+ const messageInput = container.firstElementChild
124
+ expect(messageInput).not.toHaveAttribute('aria-disabled')
125
+ expect(screen.getByTestId('custom-action')).toBeInTheDocument()
126
+ })
127
+ })
@@ -6,6 +6,7 @@ import {
6
6
  QuotedMessagePreview,
7
7
  SimpleAttachmentSelector,
8
8
  TextareaComposer,
9
+ useChannelStateContext,
9
10
  useMessageComposerHasSendableData,
10
11
  useMessageInputContext,
11
12
  } from 'stream-chat-react'
@@ -13,8 +14,11 @@ import {
13
14
  import { CustomLinkPreviewList } from '../CustomLinkPreviewList'
14
15
 
15
16
  const CustomMessageInputInner: React.FC = () => {
17
+ const { channel } = useChannelStateContext()
18
+ const isFrozen = channel?.data?.frozen === true
16
19
  const { handleSubmit } = useMessageInputContext()
17
20
  const hasSendableData = useMessageComposerHasSendableData()
21
+ const isSendDisabled = isFrozen || !hasSendableData
18
22
 
19
23
  return (
20
24
  <>
@@ -28,20 +32,23 @@ const CustomMessageInputInner: React.FC = () => {
28
32
  <div className="flex">
29
33
  <div className="w-full ml-2 mr-4 self-center leading-[0]">
30
34
  <TextareaComposer
35
+ aria-disabled={isFrozen || undefined}
31
36
  className="w-full resize-none outline-none leading-6"
32
37
  // While this might usually be considered an anti-pattern, in most
33
38
  // cases, when a message thread is rendered, we want the input to
34
39
  // gain focus automatically.
35
40
  // eslint-disable-next-line jsx-a11y/no-autofocus
36
- autoFocus
41
+ autoFocus={!isFrozen}
37
42
  maxRows={4}
43
+ readOnly={isFrozen}
44
+ tabIndex={isFrozen ? -1 : undefined}
38
45
  />
39
46
  </div>
40
47
  <button
41
48
  aria-label="Send"
42
49
  className="str-chat__send-button mt-auto flex justify-center items-center flex-shrink-0 rounded-full size-8 bg-[#121110] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white focus-ring"
43
50
  data-testid="send-button"
44
- disabled={!hasSendableData}
51
+ disabled={isSendDisabled}
45
52
  onClick={handleSubmit}
46
53
  type="button"
47
54
  >
@@ -59,9 +66,19 @@ export interface CustomMessageInputProps {
59
66
 
60
67
  export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
61
68
  renderActions,
62
- }) => (
63
- <div className="message-input flex items-center gap-2 p-4">
64
- {renderActions && renderActions?.()}
65
- <MessageInput Input={CustomMessageInputInner} />
66
- </div>
67
- )
69
+ }) => {
70
+ const { channel } = useChannelStateContext()
71
+ const isFrozen = channel?.data?.frozen === true
72
+
73
+ return (
74
+ <div
75
+ // @ts-expect-error Only React 19 onwards has `inert` in its types.
76
+ inert={isFrozen ? '' : undefined}
77
+ aria-disabled={isFrozen || undefined}
78
+ className="message-input flex items-center gap-2 p-4 aria-disabled:opacity-40"
79
+ >
80
+ {renderActions?.()}
81
+ <MessageInput Input={CustomMessageInputInner} />
82
+ </div>
83
+ )
84
+ }
@@ -72,11 +72,9 @@ const VisitorCard: React.FC<VisitorCardProps> = ({
72
72
 
73
73
  const fetchSource = useCallback(async (): Promise<void> => {
74
74
  if (fetchingRef.current) return
75
- console.log('[LOCKED ATTACHMENT] fetchSource: starting fetch')
76
75
  fetchingRef.current = true
77
76
  try {
78
77
  const result = await onFetchSourceRef.current?.()
79
- console.log('[LOCKED ATTACHMENT] fetchSource: result received', { hasResult: Boolean(result) })
80
78
  if (result) setSource(result)
81
79
  } finally {
82
80
  fetchingRef.current = false
@@ -91,19 +89,15 @@ const VisitorCard: React.FC<VisitorCardProps> = ({
91
89
  return
92
90
  }
93
91
 
94
- console.log('[LOCKED ATTACHMENT] paymentStatus changed', { paymentStatus })
95
92
  if (paymentStatus === 'paid') {
96
- console.log('[LOCKED ATTACHMENT] paymentStatus=paid: auto-fetching source')
97
93
  void fetchSource()
98
94
  }
99
95
  }, [paymentStatus, fetchSource])
100
96
 
101
97
  const handleUnlockClick = useCallback(() => {
102
98
  if (paymentStatus === 'paid') {
103
- console.log('[LOCKED ATTACHMENT] handleUnlockClick: already paid, fetching source')
104
99
  void fetchSource()
105
100
  } else {
106
- console.log('[LOCKED ATTACHMENT] handleUnlockClick: not paid, opening checkout')
107
101
  onUnlockClick?.()
108
102
  }
109
103
  }, [paymentStatus, onUnlockClick, fetchSource])
@@ -1 +0,0 @@
1
- {"version":3,"file":"Card-B1gJkHn-.js","sources":["../src/components/LockedAttachment/components/Visitor/CardActions.tsx","../src/components/LockedAttachment/components/Visitor/CardThumbnailPreview.tsx","../src/components/LockedAttachment/components/Visitor/CardImagePreview.tsx","../src/components/LockedAttachment/components/Visitor/CardMediaPreview.tsx","../src/components/LockedAttachment/components/Visitor/Card.tsx"],"sourcesContent":["import { DownloadSimpleIcon, LockSimpleIcon } from '@phosphor-icons/react'\nimport React from 'react'\n\ninterface CardActionsProps {\n isLocked: boolean\n isUnlocking?: boolean\n sourceUrl?: string\n redeemUrl?: string\n onUnlockClicked?: () => void\n onDownloadClicked?: () => void\n}\n\nconst CardActions: React.FC<CardActionsProps> = (props) => {\n const {\n isLocked,\n isUnlocking = false,\n sourceUrl,\n redeemUrl,\n onUnlockClicked,\n onDownloadClicked,\n } = props\n\n if (isLocked && onUnlockClicked != null) {\n return (\n <button\n type=\"button\"\n onClick={onUnlockClicked}\n disabled={isUnlocking}\n className=\"mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none text-white hover:bg-[#2a2928] disabled:opacity-70\"\n >\n {isUnlocking ? (\n <LoadingDots />\n ) : (\n <React.Fragment>\n <LockSimpleIcon className=\"size-4\" weight=\"fill\" />\n Unlock\n </React.Fragment>\n )}\n </button>\n )\n }\n\n if (!isLocked && onDownloadClicked != null && sourceUrl != null) {\n return (\n <a\n href={redeemUrl ?? sourceUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={onDownloadClicked}\n className=\"mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none !text-white hover:bg-[#2a2928]\"\n >\n <DownloadSimpleIcon className=\"size-4\" weight=\"bold\" />\n Download\n </a>\n )\n }\n\n return null\n}\n\nconst LoadingDots: React.FC = () => {\n return (\n <span className=\"flex items-center gap-1\">\n <span className=\"size-1 rounded-full bg-white animate-bounce [animation-delay:-0.3s]\" />\n <span className=\"size-1 rounded-full bg-white animate-bounce [animation-delay:-0.15s]\" />\n <span className=\"size-1 rounded-full bg-white animate-bounce\" />\n </span>\n )\n}\n\nexport default CardActions\n","import React from 'react'\n\nimport { renderTypeIcon } from '../../utils/icons'\n\ninterface ThumbnailPreviewProps {\n thumbnailUrl?: string\n mimeType: string\n LockIcon?: React.ElementType\n}\n\nconst ThumbnailPreview: React.FC<ThumbnailPreviewProps> = (props) => {\n const { thumbnailUrl, mimeType, LockIcon } = props\n\n return (\n <div className=\"relative aspect-video overflow-hidden bg-black/5\">\n {thumbnailUrl != null ? (\n <img\n src={thumbnailUrl}\n alt=\"\"\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n ) : (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n {renderTypeIcon(mimeType, {\n className: 'size-12 text-black/20',\n weight: 'regular',\n })}\n </div>\n )}\n {LockIcon != null ? <LockOverlay icon={LockIcon} /> : null}\n </div>\n )\n}\n\nconst LockOverlay: React.FC<{ icon: React.ElementType }> = ({ icon: Icon }) => {\n return (\n <div className=\"absolute inset-0 bg-black/30\">\n <div className=\"absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60\">\n <Icon className=\"size-4 text-white\" weight=\"fill\" />\n </div>\n </div>\n )\n}\n\nexport default ThumbnailPreview\n","import React, { useState } from 'react'\n\nimport ThumbnailPreview from './CardThumbnailPreview'\n\ninterface ImagePreviewProps {\n sourceUrl?: string\n thumbnailUrl?: string\n mimeType: string\n title?: string\n LockIcon?: React.ElementType\n}\n\nconst ImagePreview: React.FC<ImagePreviewProps> = (props) => {\n const { sourceUrl, thumbnailUrl, mimeType, title, LockIcon } = props\n const [sourceReady, setSourceReady] = useState(false)\n\n if (LockIcon != null) {\n return (\n <ThumbnailPreview\n thumbnailUrl={thumbnailUrl}\n mimeType={mimeType}\n LockIcon={LockIcon}\n />\n )\n }\n\n return (\n <div className=\"relative overflow-hidden bg-black/5\">\n <img\n src={sourceUrl}\n alt={title}\n className={`block w-full transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}\n onLoad={() => setSourceReady(true)}\n />\n </div>\n )\n}\n\nexport default ImagePreview\n","import React from 'react'\n\nimport MediaPlayer from '../MediaPlayer'\n\nimport ThumbnailPreview from './CardThumbnailPreview'\n\ninterface MediaPreviewProps {\n sourceUrl?: string\n thumbnailUrl?: string\n mimeType: string\n LockIcon?: React.ElementType\n}\n\nconst MediaPreview: React.FC<MediaPreviewProps> = (props) => {\n const { sourceUrl, thumbnailUrl, mimeType, LockIcon } = props\n\n if (LockIcon != null) {\n return (\n <ThumbnailPreview\n thumbnailUrl={thumbnailUrl}\n mimeType={mimeType}\n LockIcon={LockIcon}\n />\n )\n }\n\n return (\n <MediaPlayer\n source={sourceUrl ?? ''}\n mimeType={mimeType}\n poster={thumbnailUrl}\n />\n )\n}\n\nexport default MediaPreview\n","import {\n CheckCircleIcon,\n LockOpenIcon,\n LockSimpleIcon,\n} from '@phosphor-icons/react'\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\n\nimport type {\n LockedAttachmentBaseProps,\n LockedAttachmentSource,\n PaymentStatus,\n} from '../../types'\nimport { renderTypeIcon } from '../../utils/icons'\nimport { getSourceType } from '../../utils/mimeType'\n\nimport CardActions from './CardActions'\nimport ImagePreview from './CardImagePreview'\nimport MediaPreview from './CardMediaPreview'\nimport ThumbnailPreview from './CardThumbnailPreview'\n\nexport interface VisitorCardProps extends LockedAttachmentBaseProps {\n /**\n * Called when the visitor clicks Unlock on an unpaid attachment.\n * Use this to open a checkout flow. Omit to hide the Unlock button.\n */\n onUnlockClick?: () => void\n /**\n * Called to fetch the attachment source — fired automatically when\n * paymentStatus transitions to 'paid', or immediately on click when\n * paymentStatus is already 'paid'. Return a LockedAttachmentSource to\n * unlock the card.\n */\n onFetchSource?: () => Promise<LockedAttachmentSource | void>\n /**\n * Called when the visitor clicks Download on an unlocked card.\n * Omit to hide the Download button.\n */\n onDownloadClick?: () => void\n /**\n * When true, shows loading dots on the Unlock button.\n * Driven by the LockedAttachmentContext (e.g. checkout in progress, payment processing).\n */\n isUnlocking?: boolean\n}\n\nfunction getLockIcon(paymentStatus?: PaymentStatus): React.ElementType {\n return paymentStatus === 'paid' ? LockOpenIcon : LockSimpleIcon\n}\n\nconst VisitorCard: React.FC<VisitorCardProps> = ({\n title,\n amountText,\n thumbnailUrl,\n mimeType = 'application/octet-stream',\n detail,\n onUnlockClick,\n onFetchSource,\n onDownloadClick,\n paymentStatus,\n isUnlocking = false,\n}) => {\n const [source, setSource] = useState<LockedAttachmentSource | undefined>()\n const hasMounted = useRef(false)\n const fetchingRef = useRef(false)\n // Stable ref so fetchSource doesn't change identity when onFetchSource prop changes\n const onFetchSourceRef = useRef(onFetchSource)\n onFetchSourceRef.current = onFetchSource\n\n const isLocked = source === undefined\n const sourceType = getSourceType(mimeType)\n const LockIcon = isLocked ? getLockIcon(paymentStatus) : undefined\n\n const fetchSource = useCallback(async (): Promise<void> => {\n if (fetchingRef.current) return\n console.log('[LOCKED ATTACHMENT] fetchSource: starting fetch')\n fetchingRef.current = true\n try {\n const result = await onFetchSourceRef.current?.()\n console.log('[LOCKED ATTACHMENT] fetchSource: result received', { hasResult: Boolean(result) })\n if (result) setSource(result)\n } finally {\n fetchingRef.current = false\n }\n }, []) // stable — reads onFetchSource via ref\n\n // When paymentStatus transitions to 'paid' (e.g. after checkout completes),\n // automatically fetch the source. Skipped on mount.\n useEffect(() => {\n if (!hasMounted.current) {\n hasMounted.current = true\n return\n }\n\n console.log('[LOCKED ATTACHMENT] paymentStatus changed', { paymentStatus })\n if (paymentStatus === 'paid') {\n console.log('[LOCKED ATTACHMENT] paymentStatus=paid: auto-fetching source')\n void fetchSource()\n }\n }, [paymentStatus, fetchSource])\n\n const handleUnlockClick = useCallback(() => {\n if (paymentStatus === 'paid') {\n console.log('[LOCKED ATTACHMENT] handleUnlockClick: already paid, fetching source')\n void fetchSource()\n } else {\n console.log('[LOCKED ATTACHMENT] handleUnlockClick: not paid, opening checkout')\n onUnlockClick?.()\n }\n }, [paymentStatus, onUnlockClick, fetchSource])\n\n let mediaPreview: React.ReactNode\n if (sourceType === 'image') {\n mediaPreview = (\n <ImagePreview\n key={source?.sourceUrl}\n sourceUrl={source?.sourceUrl}\n thumbnailUrl={thumbnailUrl}\n mimeType={mimeType}\n title={title}\n LockIcon={LockIcon}\n />\n )\n } else if (sourceType === 'document') {\n mediaPreview = (\n <ThumbnailPreview\n key={source?.sourceUrl}\n thumbnailUrl={thumbnailUrl}\n mimeType={mimeType}\n LockIcon={LockIcon}\n />\n )\n } else {\n mediaPreview = (\n <MediaPreview\n key={source?.sourceUrl}\n sourceUrl={source?.sourceUrl}\n thumbnailUrl={thumbnailUrl}\n mimeType={mimeType}\n LockIcon={LockIcon}\n />\n )\n }\n\n return (\n <div className=\"w-[280px] select-none overflow-hidden rounded-[24px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]\">\n {mediaPreview}\n <div className=\"px-4 pb-3 pt-3\">\n <p className=\"mb-1.5 truncate text-base font-medium text-black\">\n {title}\n </p>\n <div className=\"flex items-center gap-1\">\n {renderTypeIcon(mimeType, {\n className: 'size-5 shrink-0 text-black/55',\n weight: 'regular',\n })}\n {detail != null ? (\n <span className=\"text-xs font-medium text-black/55\">{detail}</span>\n ) : null}\n {paymentStatus === 'paid' ? (\n <>\n <span className=\"text-xs font-medium text-black/55\">•</span>\n <span className=\"text-xs font-medium text-[#008236]\">\n Purchased\n </span>\n <CheckCircleIcon\n className=\"size-4 text-[#008236]\"\n weight=\"bold\"\n />\n </>\n ) : amountText != null ? (\n <>\n <span className=\"text-xs font-medium text-black/55\">•</span>\n <span className=\"text-xs font-medium text-black/55\">\n {amountText}\n </span>\n </>\n ) : null}\n </div>\n <CardActions\n isLocked={isLocked}\n isUnlocking={isUnlocking}\n sourceUrl={source?.sourceUrl}\n redeemUrl={source?.redeemUrl}\n onUnlockClicked={handleUnlockClick}\n onDownloadClicked={onDownloadClick}\n />\n </div>\n </div>\n )\n}\n\nexport default VisitorCard\n"],"names":["CardActions","props","isLocked","isUnlocking","sourceUrl","redeemUrl","onUnlockClicked","onDownloadClicked","jsx","LoadingDots","jsxs","React","LockSimpleIcon","DownloadSimpleIcon","ThumbnailPreview","thumbnailUrl","mimeType","LockIcon","LockOverlay","Icon","ImagePreview","title","sourceReady","setSourceReady","useState","MediaPreview","MediaPlayer","getLockIcon","paymentStatus","LockOpenIcon","VisitorCard","amountText","detail","onUnlockClick","onFetchSource","onDownloadClick","source","setSource","hasMounted","useRef","fetchingRef","onFetchSourceRef","sourceType","getSourceType","fetchSource","useCallback","result","_a","useEffect","handleUnlockClick","mediaPreview","renderTypeIcon","Fragment","CheckCircleIcon"],"mappings":"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"}