@linktr.ee/messaging-react 1.31.0-rc-1776677746 → 1.31.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 (35) hide show
  1. package/dist/{Creator-DGe3CQ_j.js → Card-C5t3dZ5q.js} +177 -150
  2. package/dist/Card-C5t3dZ5q.js.map +1 -0
  3. package/dist/Card-Cn2va-Qr.js +205 -0
  4. package/dist/Card-Cn2va-Qr.js.map +1 -0
  5. package/dist/index.d.ts +35 -30
  6. package/dist/index.js +951 -956
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/ChannelView.tsx +24 -36
  10. package/src/components/CustomMessage/CustomMessage.stories.tsx +1 -14
  11. package/src/components/CustomMessage/context.tsx +20 -0
  12. package/src/components/CustomMessage/index.tsx +39 -28
  13. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +8 -13
  14. package/src/components/LockedAttachment/components/Creator/Card.tsx +159 -0
  15. package/src/components/LockedAttachment/components/Creator/CardAudioPreview.tsx +161 -0
  16. package/src/components/LockedAttachment/components/Creator/CardCollapsedThumbnail.tsx +58 -0
  17. package/src/components/LockedAttachment/components/Creator/CardImagePreview.tsx +56 -0
  18. package/src/components/LockedAttachment/components/Creator/CardVideoPreview.tsx +91 -0
  19. package/src/components/LockedAttachment/components/Creator/index.tsx +2 -0
  20. package/src/components/LockedAttachment/components/Visitor/Card.tsx +186 -0
  21. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +71 -0
  22. package/src/components/LockedAttachment/components/Visitor/CardImagePreview.tsx +39 -0
  23. package/src/components/LockedAttachment/components/Visitor/CardMediaPreview.tsx +36 -0
  24. package/src/components/LockedAttachment/components/Visitor/CardThumbnailPreview.tsx +45 -0
  25. package/src/components/LockedAttachment/components/Visitor/index.ts +2 -0
  26. package/src/components/LockedAttachment/index.tsx +16 -23
  27. package/src/components/LockedAttachment/types.ts +14 -1
  28. package/src/components/MessagingShell/index.tsx +0 -6
  29. package/src/index.ts +4 -1
  30. package/src/types.ts +0 -21
  31. package/dist/Creator-DGe3CQ_j.js.map +0 -1
  32. package/dist/Visitor-DyJTWB2_.js +0 -204
  33. package/dist/Visitor-DyJTWB2_.js.map +0 -1
  34. package/src/components/LockedAttachment/components/Creator.tsx +0 -470
  35. package/src/components/LockedAttachment/components/Visitor.tsx +0 -356
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.31.0-rc-1776677746",
3
+ "version": "1.31.0",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  import { ArrowLeftIcon, DotsThreeIcon, StarIcon } from '@phosphor-icons/react'
2
2
  import classNames from 'classnames'
3
3
  import React, { useCallback, useRef } from 'react'
4
- import { Channel as ChannelType, LocalMessage } from 'stream-chat'
4
+ import { Channel as ChannelType } from 'stream-chat'
5
5
  import {
6
6
  Channel,
7
7
  Window,
@@ -13,7 +13,7 @@ import {
13
13
  } from 'stream-chat-react'
14
14
 
15
15
  import { useChannelStar } from '../hooks/useChannelStar'
16
- import type { ChannelViewProps, LockedAttachmentSource } from '../types'
16
+ import type { ChannelViewProps } from '../types'
17
17
 
18
18
  import { Avatar } from './Avatar'
19
19
  import { ChannelInfoDialog } from './ChannelInfoDialog'
@@ -207,9 +207,6 @@ const ChannelViewInner: React.FC<{
207
207
  onReportParticipantClick?: () => void
208
208
  showStarButton?: boolean
209
209
  chatbotVotingEnabled?: boolean
210
- onAttachmentUnlockClick?: (message: LocalMessage, channel: ChannelType) => void
211
- onAttachmentDownloadClick?: (message: LocalMessage, channel: ChannelType) => void
212
- onAttachmentUnlocked?: (message: LocalMessage, channel: ChannelType) => LockedAttachmentSource
213
210
  renderChannelBanner?: () => React.ReactNode
214
211
  customProfileContent?: React.ReactNode
215
212
  customChannelActions?: React.ReactNode
@@ -230,9 +227,6 @@ const ChannelViewInner: React.FC<{
230
227
  onReportParticipantClick,
231
228
  showStarButton = false,
232
229
  chatbotVotingEnabled = false,
233
- onAttachmentUnlockClick,
234
- onAttachmentDownloadClick,
235
- onAttachmentUnlocked,
236
230
  renderChannelBanner,
237
231
  customProfileContent,
238
232
  customChannelActions,
@@ -278,30 +272,30 @@ const ChannelViewInner: React.FC<{
278
272
  infoDialogRef.current?.close()
279
273
  }, [])
280
274
 
281
- return (
282
- <>
283
- <WithComponents
284
- overrides={{
285
- Message: (props: MessageUIComponentProps) => {
286
- const { message } = useMessageContext('ChannelView')
287
- const messageNode = (
288
- <CustomMessage
289
- {...props}
290
- chatbotVotingEnabled={chatbotVotingEnabled}
291
- onAttachmentUnlockClick={onAttachmentUnlockClick}
292
- onAttachmentDownloadClick={onAttachmentDownloadClick}
293
- onAttachmentUnlocked={onAttachmentUnlocked}
294
- />
295
- )
275
+ // Prevents all message instances from unmounting when ChannelViewInner re-renders
276
+ const MessageOverride = useCallback(
277
+ (props: MessageUIComponentProps) => {
278
+ // eslint-disable-next-line react-hooks/rules-of-hooks
279
+ const { message } = useMessageContext('ChannelView')
280
+ const messageNode = (
281
+ <CustomMessage
282
+ {...props}
283
+ chatbotVotingEnabled={chatbotVotingEnabled}
284
+ />
285
+ )
286
+
287
+ if (!renderMessage || !message) {
288
+ return messageNode
289
+ }
296
290
 
297
- if (!renderMessage || !message) {
298
- return messageNode
299
- }
291
+ return renderMessage(messageNode, message)
292
+ },
293
+ [chatbotVotingEnabled, renderMessage]
294
+ )
300
295
 
301
- return renderMessage(messageNode, message)
302
- },
303
- }}
304
- >
296
+ return (
297
+ <>
298
+ <WithComponents overrides={{ Message: MessageOverride }}>
305
299
  <Window>
306
300
  {/* Custom Channel Header */}
307
301
  <div className="p-4">
@@ -378,9 +372,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
378
372
  onMessageSent,
379
373
  showStarButton = false,
380
374
  chatbotVotingEnabled = false,
381
- onAttachmentUnlockClick,
382
- onAttachmentDownloadClick,
383
- onAttachmentUnlocked,
384
375
  renderChannelBanner,
385
376
  customProfileContent,
386
377
  customChannelActions,
@@ -459,9 +450,6 @@ export const ChannelView = React.memo<ChannelViewProps>(
459
450
  onReportParticipantClick={onReportParticipantClick}
460
451
  showStarButton={showStarButton}
461
452
  chatbotVotingEnabled={chatbotVotingEnabled}
462
- onAttachmentUnlockClick={onAttachmentUnlockClick}
463
- onAttachmentDownloadClick={onAttachmentDownloadClick}
464
- onAttachmentUnlocked={onAttachmentUnlocked}
465
453
  renderChannelBanner={renderChannelBanner}
466
454
  customProfileContent={customProfileContent}
467
455
  customChannelActions={customChannelActions}
@@ -132,28 +132,15 @@ const TemplateInner: React.FC<{
132
132
  createMockChannel(client, messages).then(setChannel)
133
133
  }, [client, messages])
134
134
 
135
- const isVisitor = currentUser.id === storyUsers.visitor.id
136
-
137
135
  const MessageComponent = React.useMemo(() => {
138
- const handleAttachmentUnlockClicked = () => {
139
- alert('Unlocking...')
140
- }
141
-
142
- const handleAttachmentDownloadClicked = () => {
143
- alert(`Downloading...`)
144
- }
145
-
146
136
  return function CustomMessageComponent(props: MessageUIComponentProps) {
147
137
  return (
148
138
  <CustomMessage
149
139
  {...props}
150
- onAttachmentUnlockClick={isVisitor ? () => handleAttachmentUnlockClicked() : undefined}
151
- onAttachmentDownloadClick={isVisitor ? () => handleAttachmentDownloadClicked() : undefined}
152
- onAttachmentUnlocked={undefined}
153
140
  />
154
141
  )
155
142
  }
156
- }, [isVisitor])
143
+ }, [])
157
144
 
158
145
  if (!channel) {
159
146
  return <div className="p-4">Loading...</div>
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ import { defaultLockedAttachmentContextValue, type LockedAttachmentContextValue } from '../LockedAttachment/types'
4
+
5
+ export interface CustomMessageRegistry {
6
+ LockedAttachment: LockedAttachmentContextValue
7
+ }
8
+
9
+ const customMessageDefaults: CustomMessageRegistry = {
10
+ LockedAttachment: defaultLockedAttachmentContextValue,
11
+ }
12
+
13
+ const CustomMessageContext = createContext<Partial<CustomMessageRegistry>>({})
14
+
15
+ export const CustomMessageProvider = CustomMessageContext.Provider
16
+
17
+ export function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K] {
18
+ const ctx = useContext(CustomMessageContext)
19
+ return (ctx[key] ?? customMessageDefaults[key]) as CustomMessageRegistry[K]
20
+ }
@@ -1,6 +1,5 @@
1
1
  import classNames from 'classnames'
2
2
  import React, { useMemo, useState } from 'react'
3
- import { type Channel, type LocalMessage } from 'stream-chat'
4
3
  import {
5
4
  Attachment as DefaultAttachment,
6
5
  EditMessageModal as DefaultEditMessageModal,
@@ -32,29 +31,29 @@ import {
32
31
 
33
32
  import { useMessageVote } from '../../hooks/useMessageVote'
34
33
  import { Avatar } from '../Avatar'
35
- import LockedAttachment, { type LockedAttachmentSource } from '../LockedAttachment'
34
+ import LockedAttachment from '../LockedAttachment'
36
35
 
37
- import { MessageTag, isAttachmentMessage, isChatbotMessage, isTipOnlyMessage } from './MessageTag'
36
+ import { useCustomMessage } from './context'
37
+ import {
38
+ MessageTag,
39
+ isAttachmentMessage,
40
+ isChatbotMessage,
41
+ isTipOnlyMessage,
42
+ } from './MessageTag'
38
43
  import { MessageVoteButtons } from './MessageVoteButtons'
39
44
 
40
- type CustomMessageProps = {
45
+ type CustomMessageUIComponentProps = MessageUIComponentProps & {
41
46
  chatbotVotingEnabled?: boolean
42
- onAttachmentUnlockClick?: (message: LocalMessage, channel: Channel) => void
43
- onAttachmentDownloadClick?: (message: LocalMessage, channel: Channel) => void
44
- onAttachmentUnlocked?: (message: LocalMessage, channel: Channel) => LockedAttachmentSource
45
47
  }
46
48
 
47
- type CustomMessageUIComponentProps = MessageUIComponentProps & CustomMessageProps
48
-
49
- type CustomMessageWithContextProps = MessageContextValue & CustomMessageProps
49
+ type CustomMessageWithContextProps = MessageContextValue & {
50
+ chatbotVotingEnabled?: boolean
51
+ }
50
52
 
51
53
  const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
52
54
  const {
53
55
  additionalMessageInputProps,
54
56
  chatbotVotingEnabled,
55
- onAttachmentUnlockClick,
56
- onAttachmentDownloadClick,
57
- onAttachmentUnlocked,
58
57
  editing,
59
58
  endOfGroup,
60
59
  firstOfGroup,
@@ -72,6 +71,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
72
71
 
73
72
  const { client } = useChatContext('CustomMessage')
74
73
  const { channel } = useChannelStateContext('CustomMessage')
74
+ const { isUnlocking, onUnlockClick, onFetchSource, onDownloadClick } = useCustomMessage('LockedAttachment')
75
75
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
76
76
  const reminder = useMessageReminder(message.id)
77
77
  const { selected: voteState, voteUp, voteDown } = useMessageVote(message)
@@ -208,18 +208,32 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
208
208
  }}
209
209
  >
210
210
  {isAttachment ? (
211
- <div className={classNames("flex flex-col gap-1", isMine ? "items-end" : "items-start")}>
212
- <LockedAttachment
213
- title={message.metadata?.attachment_title}
214
- mimeType={message.metadata?.attachment_mime_type ?? 'application/octet-stream'}
215
- thumbnailUrl={message.metadata?.attachment_thumbnail}
216
- amountText={message.metadata?.amount_text}
217
- detail={message.metadata?.attachment_detail}
218
- paymentStatus={message.metadata?.payment_status}
219
- onUnlockClick={onAttachmentUnlockClick ? () => onAttachmentUnlockClick(message, channel) : undefined}
220
- onDownloadClick={onAttachmentDownloadClick ? () => onAttachmentDownloadClick(message, channel) : undefined}
221
- onUnlocked={onAttachmentUnlocked ? () => onAttachmentUnlocked(message, channel) : undefined}
222
- />
211
+ <div className={`flex flex-col gap-1 ${isMine ? 'items-end' : 'items-start'}`}>
212
+ {isMine ? (
213
+ <LockedAttachment.Creator
214
+ title={message.metadata?.attachment_title}
215
+ mimeType={message.metadata?.attachment_mime_type}
216
+ thumbnailUrl={message.metadata?.attachment_thumbnail}
217
+ amountText={message.metadata?.amount_text}
218
+ detail={message.metadata?.attachment_detail}
219
+ paymentStatus={message.metadata?.payment_status}
220
+ />
221
+ ) : (
222
+ <LockedAttachment.Visitor
223
+ title={message.metadata?.attachment_title}
224
+ mimeType={message.metadata?.attachment_mime_type}
225
+ thumbnailUrl={message.metadata?.attachment_thumbnail}
226
+ amountText={message.metadata?.amount_text}
227
+ detail={message.metadata?.attachment_detail}
228
+ paymentStatus={message.metadata?.payment_status}
229
+ isUnlocking={isUnlocking(message.id)}
230
+ onUnlockClick={() => onUnlockClick?.(message, channel)}
231
+ onFetchSource={async () =>
232
+ await onFetchSource?.(message, channel)
233
+ }
234
+ onDownloadClick={() => onDownloadClick?.(message, channel)}
235
+ />
236
+ )}
223
237
  {message.text && (
224
238
  <div className="str-chat__message-bubble-wrapper">
225
239
  <div className="str-chat__message-bubble">
@@ -294,9 +308,6 @@ const MemoizedCustomMessage = React.memo(
294
308
  CustomMessageWithContext,
295
309
  (prev, next) => {
296
310
  if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
297
- if (prev.onAttachmentUnlockClick !== next.onAttachmentUnlockClick) return false
298
- if (prev.onAttachmentDownloadClick !== next.onAttachmentDownloadClick) return false
299
- if (prev.onAttachmentUnlocked !== next.onAttachmentUnlocked) return false
300
311
  return areMessageUIPropsEqual(prev, next)
301
312
  }
302
313
  ) as typeof CustomMessageWithContext
@@ -17,7 +17,6 @@ const AUDIO_SOURCE = '/audio-source.mp3'
17
17
 
18
18
  const meta: Meta = {
19
19
  title: 'Components/LockedAttachment',
20
- component: LockedAttachment,
21
20
  parameters: { layout: 'centered' },
22
21
  }
23
22
  export default meta
@@ -99,7 +98,7 @@ export const Visitor: StoryFn = () => (
99
98
  </td>
100
99
  {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
101
100
  <td key={mimeType} className="align-top">
102
- <LockedAttachment
101
+ <LockedAttachment.Visitor
103
102
  title={title}
104
103
  amountText="AU$9.99"
105
104
  thumbnailUrl={thumbnailUrl}
@@ -107,7 +106,6 @@ export const Visitor: StoryFn = () => (
107
106
  detail={detail}
108
107
  onUnlockClick={() => {}}
109
108
  onDownloadClick={() => {}}
110
- onUnlocked={undefined}
111
109
  />
112
110
  </td>
113
111
  ))}
@@ -118,7 +116,7 @@ export const Visitor: StoryFn = () => (
118
116
  </td>
119
117
  {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
120
118
  <td key={mimeType} className="align-top">
121
- <LockedAttachment
119
+ <LockedAttachment.Visitor
122
120
  title={title}
123
121
  amountText="AU$9.99"
124
122
  thumbnailUrl={thumbnailUrl}
@@ -127,7 +125,6 @@ export const Visitor: StoryFn = () => (
127
125
  paymentStatus="paid"
128
126
  onUnlockClick={() => {}}
129
127
  onDownloadClick={() => {}}
130
- onUnlocked={undefined}
131
128
  />
132
129
  </td>
133
130
  ))}
@@ -138,16 +135,15 @@ export const Visitor: StoryFn = () => (
138
135
  </td>
139
136
  {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
140
137
  <td key={mimeType} className="align-top">
141
- <LockedAttachment
138
+ <LockedAttachment.Visitor
142
139
  title={title}
143
140
  thumbnailUrl={thumbnailUrl}
144
141
  mimeType={mimeType}
145
142
  detail={detail}
146
143
  amountText="AU$9.99"
147
144
  paymentStatus="paid"
148
- onUnlockClick={() => {}}
145
+ onUnlockClick={() => Promise.resolve({ sourceUrl })}
149
146
  onDownloadClick={() => {}}
150
- onUnlocked={() => ({ sourceUrl })}
151
147
  />
152
148
  </td>
153
149
  ))}
@@ -166,7 +162,7 @@ export const Creator: StoryFn = () => (
166
162
  </td>
167
163
  {VARIANTS.map(({ mimeType, detail, thumbnailUrl, sourceUrl }) => (
168
164
  <td key={mimeType} className="align-top">
169
- <LockedAttachment
165
+ <LockedAttachment.Creator
170
166
  isPreview={true}
171
167
  thumbnailUrl={thumbnailUrl}
172
168
  sourceUrl={sourceUrl}
@@ -184,7 +180,7 @@ export const Creator: StoryFn = () => (
184
180
  </td>
185
181
  {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
186
182
  <td key={mimeType} className="align-top">
187
- <LockedAttachment
183
+ <LockedAttachment.Creator
188
184
  title={title}
189
185
  thumbnailUrl={thumbnailUrl}
190
186
  sourceUrl={sourceUrl}
@@ -202,7 +198,7 @@ export const Creator: StoryFn = () => (
202
198
  </td>
203
199
  {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
204
200
  <td key={mimeType} className="align-top">
205
- <LockedAttachment
201
+ <LockedAttachment.Creator
206
202
  title={title}
207
203
  thumbnailUrl={thumbnailUrl}
208
204
  sourceUrl={sourceUrl}
@@ -220,7 +216,7 @@ export const Creator: StoryFn = () => (
220
216
  </td>
221
217
  {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
222
218
  <td key={mimeType} className="align-top">
223
- <LockedAttachment
219
+ <LockedAttachment.Creator
224
220
  title={title}
225
221
  thumbnailUrl={thumbnailUrl}
226
222
  sourceUrl={sourceUrl}
@@ -236,4 +232,3 @@ export const Creator: StoryFn = () => (
236
232
  </tbody>
237
233
  </Table>
238
234
  )
239
-
@@ -0,0 +1,159 @@
1
+ import {
2
+ CheckCircleIcon,
3
+ LockIcon,
4
+ LockOpenIcon,
5
+ XIcon,
6
+ } from '@phosphor-icons/react'
7
+ import classNames from 'classnames'
8
+ import React from 'react'
9
+
10
+ import type { LockedAttachmentBaseProps } from '../../types'
11
+ import { renderTypeIcon } from '../../utils/icons'
12
+ import { getSourceType } from '../../utils/mimeType'
13
+
14
+ import AudioPreview from './CardAudioPreview'
15
+ import CollapsedThumbnail from './CardCollapsedThumbnail'
16
+ import ImagePreview from './CardImagePreview'
17
+ import VideoPreview from './CardVideoPreview'
18
+
19
+ export interface CreatorCardProps extends LockedAttachmentBaseProps {
20
+ isPreview?: boolean
21
+ placeholderTitle?: string
22
+ placeholderAmountText?: string
23
+ sourceUrl?: string
24
+ onDismiss?: () => void
25
+ }
26
+
27
+ const CreatorCard: React.FC<CreatorCardProps> = (props) => {
28
+ const {
29
+ title,
30
+ mimeType = 'application/octet-stream',
31
+ thumbnailUrl,
32
+ sourceUrl,
33
+ detail,
34
+ amountText,
35
+ placeholderTitle = 'Attachment title',
36
+ placeholderAmountText,
37
+ paymentStatus,
38
+ onDismiss,
39
+ isPreview = false,
40
+ } = props
41
+ const sourceType = getSourceType(mimeType)
42
+ const displayAmountText = amountText ?? placeholderAmountText
43
+ const isPlaceholderAmount = !amountText && !!placeholderAmountText
44
+
45
+ let mediaPreview: React.ReactNode
46
+ if (isPreview && sourceType === 'audio') {
47
+ mediaPreview = (
48
+ <AudioPreview
49
+ key={sourceUrl}
50
+ sourceUrl={sourceUrl}
51
+ thumbnailUrl={thumbnailUrl}
52
+ mimeType={mimeType}
53
+ />
54
+ )
55
+ } else if (isPreview && sourceType === 'video') {
56
+ mediaPreview = (
57
+ <VideoPreview
58
+ key={sourceUrl}
59
+ sourceUrl={sourceUrl}
60
+ thumbnailUrl={thumbnailUrl}
61
+ mimeType={mimeType}
62
+ />
63
+ )
64
+ } else if (isPreview && sourceType === 'image') {
65
+ mediaPreview = (
66
+ <ImagePreview
67
+ key={sourceUrl}
68
+ sourceUrl={sourceUrl}
69
+ thumbnailUrl={thumbnailUrl}
70
+ mimeType={mimeType}
71
+ title={title}
72
+ />
73
+ )
74
+ } else {
75
+ const lockedOverlayIcon = onDismiss
76
+ ? undefined
77
+ : paymentStatus === 'paid'
78
+ ? LockOpenIcon
79
+ : LockIcon
80
+ mediaPreview = (
81
+ <CollapsedThumbnail
82
+ thumbnailUrl={thumbnailUrl}
83
+ mimeType={mimeType}
84
+ overlayIcon={lockedOverlayIcon}
85
+ darkOverlay
86
+ />
87
+ )
88
+ }
89
+
90
+ return (
91
+ <div className="relative 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)]">
92
+ {onDismiss && (
93
+ <button
94
+ type="button"
95
+ onClick={onDismiss}
96
+ className="absolute right-3 top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
97
+ aria-label="Dismiss attachment"
98
+ >
99
+ <XIcon className="size-4" weight="bold" />
100
+ </button>
101
+ )}
102
+ {mediaPreview}
103
+ <div className="px-4 pb-3 pt-3">
104
+ <p
105
+ className={classNames('mb-1.5 truncate text-base font-medium', {
106
+ 'text-black/30': !title,
107
+ 'text-black': !!title,
108
+ })}
109
+ >
110
+ {title || placeholderTitle}
111
+ </p>
112
+ <div className="flex items-center gap-1">
113
+ {renderTypeIcon(mimeType, {
114
+ className: 'size-5 shrink-0 text-black/55',
115
+ weight: 'regular',
116
+ })}
117
+ {detail && (
118
+ <span className="text-xs font-medium text-black/55">{detail}</span>
119
+ )}
120
+ {paymentStatus === 'paid' ? (
121
+ <>
122
+ <span className="text-xs font-medium text-black/55">•</span>
123
+ <span className="text-xs font-medium text-[#008236]">
124
+ Purchased
125
+ </span>
126
+ <CheckCircleIcon
127
+ className="size-4 text-[#008236]"
128
+ weight="bold"
129
+ />
130
+ </>
131
+ ) : (
132
+ displayAmountText && (
133
+ <>
134
+ <span
135
+ className={classNames('text-xs font-medium', {
136
+ 'text-black/30': isPlaceholderAmount,
137
+ 'text-black/55': !isPlaceholderAmount,
138
+ })}
139
+ >
140
+
141
+ </span>
142
+ <span
143
+ className={classNames('text-xs font-medium', {
144
+ 'text-black/30': isPlaceholderAmount,
145
+ 'text-black/55': !isPlaceholderAmount,
146
+ })}
147
+ >
148
+ {displayAmountText}
149
+ </span>
150
+ </>
151
+ )
152
+ )}
153
+ </div>
154
+ </div>
155
+ </div>
156
+ )
157
+ }
158
+
159
+ export default CreatorCard