@linktr.ee/messaging-react 1.36.0 → 1.38.0-rc-1777583423

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 (30) hide show
  1. package/dist/Card-BlXnKGaR.js +127 -0
  2. package/dist/Card-BlXnKGaR.js.map +1 -0
  3. package/dist/Card-DoNJA-jg.js +138 -0
  4. package/dist/Card-DoNJA-jg.js.map +1 -0
  5. package/dist/{index-DOsC03ZN.js → index-jnKl3mQ0.js} +1404 -1352
  6. package/dist/index-jnKl3mQ0.js.map +1 -0
  7. package/dist/index.d.ts +21 -1
  8. package/dist/index.js +15 -13
  9. package/package.json +2 -2
  10. package/src/components/{LockedAttachment/components → AttachmentCard}/MediaPlayer.tsx +4 -3
  11. package/src/components/AttachmentCard/Thumbnail.tsx +150 -0
  12. package/src/components/AttachmentCard/index.tsx +112 -0
  13. package/src/components/LockedAttachment/components/Creator/Card.tsx +123 -113
  14. package/src/components/LockedAttachment/components/Visitor/Card.tsx +43 -42
  15. package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +12 -0
  16. package/src/components/MediaMessage/MediaMessage.stories.tsx +45 -4
  17. package/src/components/MediaMessage/MediaMessage.test.tsx +125 -160
  18. package/src/components/MediaMessage/index.tsx +226 -349
  19. package/src/index.ts +7 -3
  20. package/src/providers/MessagingProvider.test.tsx +126 -0
  21. package/dist/Card-BHrnmHeu.js +0 -167
  22. package/dist/Card-BHrnmHeu.js.map +0 -1
  23. package/dist/Card-D4vEgqWt.js +0 -195
  24. package/dist/Card-D4vEgqWt.js.map +0 -1
  25. package/dist/index-DOsC03ZN.js.map +0 -1
  26. package/src/components/LockedAttachment/components/Creator/CardThumbnail.tsx +0 -114
  27. package/src/components/LockedAttachment/components/Visitor/CardThumbnail.tsx +0 -81
  28. /package/src/components/{LockedAttachment → AttachmentCard}/utils/icons.ts +0 -0
  29. /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.test.ts +0 -0
  30. /package/src/components/{LockedAttachment → AttachmentCard}/utils/mimeType.ts +0 -0
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Attachment } from 'stream-chat';
1
2
  import { Channel } from 'stream-chat';
2
3
  import { ChannelFilters } from 'stream-chat';
3
4
  import { ChannelListProps as ChannelListProps_2 } from 'stream-chat-react';
@@ -315,13 +316,28 @@ export declare interface LockedAttachmentSource {
315
316
  thumbnailUrl?: string;
316
317
  }
317
318
 
318
- export declare const MediaMessage: default_2.FC<MediaMessageProps>;
319
+ export declare const MediaMessage: default_2.FC<MediaMessageProps> & {
320
+ Creator: default_2.FC<{
321
+ message: LocalMessage;
322
+ }>;
323
+ Visitor: default_2.FC<{
324
+ message: LocalMessage;
325
+ }>;
326
+ };
319
327
 
320
328
  export declare interface MediaMessageProps {
321
329
  message: LocalMessage;
322
330
  isMyMessage?: boolean;
323
331
  }
324
332
 
333
+ export declare interface MediaMessageResolved {
334
+ resolvedUrl: string;
335
+ resolvedType: string;
336
+ title?: string;
337
+ fileSize?: number;
338
+ thumbnailUrl?: string;
339
+ }
340
+
325
341
  declare type MessageCustomType = 'MESSAGE_TIP' | 'MESSAGE_PAID' | 'MESSAGE_CHATBOT' | 'MESSAGE_ATTACHMENT' | AgeSafetySystemType | DmAgentSystemType;
326
342
 
327
343
  export declare interface MessageMetadata {
@@ -505,6 +521,10 @@ export declare interface ParticipantSource {
505
521
  */
506
522
  declare type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded';
507
523
 
524
+ export declare function resolveLinkAttachment(message: LocalMessage): Attachment | undefined;
525
+
526
+ export declare function resolveMediaFromMessage(message: LocalMessage): MediaMessageResolved | null;
527
+
508
528
  export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K];
509
529
 
510
530
  /**
package/dist/index.js CHANGED
@@ -1,23 +1,25 @@
1
- import { A as e, a as t, C as i, b as n, c as o, d as g, F as r, e as m, L as u, f as M, h as c, i as l, j as h, P as C, k as P, u as d, l as L, m as p, n as v } from "./index-DOsC03ZN.js";
1
+ import { b as e, c as t, C as i, d as n, e as o, f as r, F as g, g as M, L as m, M as l, h as u, i as c, j as h, P as d, k as v, r as C, l as L, u as P, m as k, n as p, o as A } from "./index-jnKl3mQ0.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
5
5
  i as ChannelEmptyState,
6
6
  n as ChannelList,
7
7
  o as ChannelView,
8
- g as CustomMessageProvider,
9
- r as FaqList,
10
- m as FaqListItem,
11
- u as LockedAttachment,
12
- M as MediaMessage,
13
- c as MessageVoteButtons,
14
- l as MessagingProvider,
8
+ r as CustomMessageProvider,
9
+ g as FaqList,
10
+ M as FaqListItem,
11
+ m as LockedAttachment,
12
+ l as MediaMessage,
13
+ u as MessageVoteButtons,
14
+ c as MessagingProvider,
15
15
  h as MessagingShell,
16
- C as ParticipantPicker,
17
- P as formatRelativeTime,
18
- d as useCustomMessage,
19
- L as useMessageVote,
16
+ d as ParticipantPicker,
17
+ v as formatRelativeTime,
18
+ C as resolveLinkAttachment,
19
+ L as resolveMediaFromMessage,
20
+ P as useCustomMessage,
21
+ k as useMessageVote,
20
22
  p as useMessaging,
21
- v as useParticipants
23
+ A as useParticipants
22
24
  };
23
25
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.36.0",
3
+ "version": "1.38.0-rc-1777583423",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@linktr.ee/component-library": "11.8.6",
37
- "@linktr.ee/messaging-core": "^1.7.1",
37
+ "@linktr.ee/messaging-core": "^1.8.0",
38
38
  "@phosphor-icons/react": "^2.1.10"
39
39
  },
40
40
  "devDependencies": {
@@ -1,9 +1,10 @@
1
1
  import { CircleNotchIcon, PauseIcon, PlayIcon } from '@phosphor-icons/react'
2
2
  import React, { useCallback, useEffect, useRef, useState } from 'react'
3
3
 
4
- import { isDevBuild } from '../../../utils/isDevBuild'
5
- import { renderTypeIcon } from '../utils/icons'
6
- import { getSourceType } from '../utils/mimeType'
4
+ import { isDevBuild } from '../../utils/isDevBuild'
5
+
6
+ import { renderTypeIcon } from './utils/icons'
7
+ import { getSourceType } from './utils/mimeType'
7
8
 
8
9
  type TouchEventUnion =
9
10
  | MouseEvent
@@ -0,0 +1,150 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import MediaPlayer, { type MediaPlayerProps } from './MediaPlayer'
4
+ import { renderTypeIcon } from './utils/icons'
5
+ import { getSourceType } from './utils/mimeType'
6
+
7
+ export type AttachmentThumbnailVariant = 'light' | 'dark'
8
+
9
+ export interface AttachmentThumbnailProps {
10
+ mimeType: string
11
+ sourceUrl?: string
12
+ thumbnailUrl?: string
13
+ title?: string
14
+ variant: AttachmentThumbnailVariant
15
+ /** Extra props passed to MediaPlayer when source is video or audio. */
16
+ mediaPlayerProps?: Partial<
17
+ Pick<
18
+ MediaPlayerProps,
19
+ 'autoPlay' | 'loop' | 'muted' | 'controls' | 'onContainerClick'
20
+ >
21
+ >
22
+ /**
23
+ * When true (Visitor unlocked image/document), use aspect-video + object-contain fade-in.
24
+ */
25
+ containedImage?: boolean
26
+ }
27
+
28
+ const placeholderIconClass = (variant: AttachmentThumbnailVariant) =>
29
+ variant === 'dark' ? 'size-12 text-white/20' : 'size-12 text-black/20'
30
+
31
+ const posterShellClass = (variant: AttachmentThumbnailVariant) =>
32
+ variant === 'dark'
33
+ ? 'aspect-video overflow-hidden bg-white/10'
34
+ : 'aspect-video overflow-hidden bg-black/5'
35
+
36
+ /**
37
+ * Renders the media preview area for attachment cards (LockedAttachment, MediaMessage).
38
+ * Overlays (dim, lock, eye toggle) are composed by the parent.
39
+ */
40
+ const AttachmentThumbnail: React.FC<AttachmentThumbnailProps> = ({
41
+ mimeType,
42
+ sourceUrl,
43
+ thumbnailUrl,
44
+ title,
45
+ variant,
46
+ mediaPlayerProps,
47
+ containedImage = false,
48
+ }) => {
49
+ const sourceType = getSourceType(mimeType)
50
+ const [sourceReady, setSourceReady] = useState(false)
51
+
52
+ if (sourceUrl && (sourceType === 'video' || sourceType === 'audio')) {
53
+ return (
54
+ <MediaPlayer
55
+ source={sourceUrl}
56
+ mimeType={mimeType}
57
+ poster={thumbnailUrl}
58
+ controls
59
+ {...mediaPlayerProps}
60
+ />
61
+ )
62
+ }
63
+
64
+ if (sourceUrl && sourceType === 'image') {
65
+ if (containedImage) {
66
+ return (
67
+ <div className="relative aspect-video overflow-hidden bg-black/5">
68
+ <img
69
+ src={sourceUrl}
70
+ alt={title ?? ''}
71
+ className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
72
+ draggable={false}
73
+ onLoad={() => setSourceReady(true)}
74
+ />
75
+ </div>
76
+ )
77
+ }
78
+ return (
79
+ <img
80
+ src={sourceUrl}
81
+ alt={title ?? ''}
82
+ className="block w-full"
83
+ draggable={false}
84
+ />
85
+ )
86
+ }
87
+
88
+ if (sourceUrl && sourceType === 'document') {
89
+ if (thumbnailUrl) {
90
+ if (containedImage) {
91
+ return (
92
+ <div className="relative aspect-video overflow-hidden bg-black/5">
93
+ <img
94
+ src={thumbnailUrl}
95
+ alt={title ?? ''}
96
+ className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
97
+ draggable={false}
98
+ onLoad={() => setSourceReady(true)}
99
+ />
100
+ </div>
101
+ )
102
+ }
103
+ return (
104
+ <img
105
+ src={thumbnailUrl}
106
+ alt=""
107
+ className="block w-full"
108
+ draggable={false}
109
+ />
110
+ )
111
+ }
112
+ return (
113
+ <div
114
+ className={`flex aspect-video w-full items-center justify-center ${variant === 'dark' ? 'bg-white/10' : 'bg-black/5'}`}
115
+ >
116
+ {renderTypeIcon(mimeType, {
117
+ className: placeholderIconClass(variant),
118
+ weight: 'regular',
119
+ })}
120
+ </div>
121
+ )
122
+ }
123
+
124
+ // Poster-only or empty (no sourceUrl)
125
+ if (thumbnailUrl) {
126
+ return (
127
+ <div className={`relative ${posterShellClass(variant)}`}>
128
+ <img
129
+ src={thumbnailUrl}
130
+ alt={title ?? ''}
131
+ draggable={false}
132
+ className="absolute inset-0 h-full w-full object-cover"
133
+ />
134
+ </div>
135
+ )
136
+ }
137
+
138
+ return (
139
+ <div
140
+ className={`flex aspect-video w-full items-center justify-center ${variant === 'dark' ? 'bg-white/10' : 'bg-black/5'}`}
141
+ >
142
+ {renderTypeIcon(mimeType, {
143
+ className: placeholderIconClass(variant),
144
+ weight: 'regular',
145
+ })}
146
+ </div>
147
+ )
148
+ }
149
+
150
+ export default AttachmentThumbnail
@@ -0,0 +1,112 @@
1
+ import classNames from 'classnames'
2
+ import React from 'react'
3
+
4
+ import { renderTypeIcon } from './utils/icons'
5
+
6
+ export { default as AttachmentThumbnail } from './Thumbnail'
7
+ export type {
8
+ AttachmentThumbnailProps,
9
+ AttachmentThumbnailVariant,
10
+ } from './Thumbnail'
11
+ export { default as MediaPlayer } from './MediaPlayer'
12
+ export type { MediaPlayerProps } from './MediaPlayer'
13
+ export { renderTypeIcon, getTypeIcon, MEDIA_TYPE_ICON } from './utils/icons'
14
+ export {
15
+ getSourceType,
16
+ getDocumentIconType,
17
+ type AttachmentSourceType,
18
+ type DocumentIconType,
19
+ } from './utils/mimeType'
20
+
21
+ export interface AttachmentCardProps {
22
+ variant: 'light' | 'dark'
23
+ thumbnail: React.ReactNode
24
+ title?: string
25
+ placeholderTitle?: string
26
+ mimeType: string
27
+ detail?: string
28
+ statusBadge?: React.ReactNode
29
+ action?: React.ReactNode
30
+ topLeft?: React.ReactNode
31
+ topRight?: React.ReactNode
32
+ rootRef?: React.Ref<HTMLDivElement>
33
+ 'data-testid'?: string
34
+ }
35
+
36
+ const AttachmentCard: React.FC<AttachmentCardProps> = ({
37
+ variant,
38
+ thumbnail,
39
+ title,
40
+ placeholderTitle = 'Attachment title',
41
+ mimeType,
42
+ detail,
43
+ statusBadge,
44
+ action,
45
+ topLeft,
46
+ topRight,
47
+ rootRef,
48
+ 'data-testid': dataTestId,
49
+ }) => {
50
+ const isDark = variant === 'dark'
51
+ const displayTitle = isDark ? (title ?? placeholderTitle) : (title ?? '')
52
+ const titleDimmed = isDark && !title
53
+
54
+ return (
55
+ <div
56
+ ref={rootRef}
57
+ data-testid={dataTestId}
58
+ className={classNames(
59
+ 'relative w-[280px] select-none overflow-hidden rounded-[24px] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]',
60
+ isDark ? 'bg-[#121110]' : 'bg-white'
61
+ )}
62
+ >
63
+ {topLeft ? (
64
+ <div className="pointer-events-auto absolute left-3 top-3 z-50">{topLeft}</div>
65
+ ) : null}
66
+ {topRight ? (
67
+ <div className="pointer-events-auto absolute right-3 top-3 z-50">{topRight}</div>
68
+ ) : null}
69
+
70
+ {thumbnail}
71
+
72
+ <div className="px-4 pb-3 pt-3">
73
+ <p
74
+ className={classNames('mb-0.5 truncate text-base font-medium', {
75
+ 'text-black': !isDark,
76
+ 'text-white/30': isDark && titleDimmed,
77
+ 'text-white': isDark && !titleDimmed,
78
+ })}
79
+ >
80
+ {displayTitle}
81
+ </p>
82
+
83
+ <div className="flex flex-wrap items-center gap-1">
84
+ {renderTypeIcon(mimeType, {
85
+ className: classNames(
86
+ 'size-5 shrink-0',
87
+ isDark ? 'text-white/55' : 'text-black/55'
88
+ ),
89
+ weight: 'regular',
90
+ })}
91
+
92
+ {detail != null && detail !== '' && (
93
+ <span
94
+ className={classNames(
95
+ 'text-xs font-medium',
96
+ isDark ? 'text-white/55' : 'text-black/55'
97
+ )}
98
+ >
99
+ {detail}
100
+ </span>
101
+ )}
102
+
103
+ {statusBadge}
104
+ </div>
105
+
106
+ {action}
107
+ </div>
108
+ </div>
109
+ )
110
+ }
111
+
112
+ export default AttachmentCard
@@ -9,14 +9,12 @@ import {
9
9
  import classNames from 'classnames'
10
10
  import React, { useCallback, useRef, useState } from 'react'
11
11
 
12
+ import AttachmentCard, { AttachmentThumbnail } from '../../../AttachmentCard'
12
13
  import type {
13
14
  LockedAttachmentBaseProps,
14
15
  LockedAttachmentSource,
15
16
  PaymentStatus,
16
17
  } from '../../types'
17
- import { renderTypeIcon } from '../../utils/icons'
18
-
19
- import CardThumbnail from './CardThumbnail'
20
18
 
21
19
  export interface CreatorCardProps extends LockedAttachmentBaseProps {
22
20
  placeholderTitle?: string
@@ -27,6 +25,65 @@ export interface CreatorCardProps extends LockedAttachmentBaseProps {
27
25
  onFetchSource?: () => Promise<LockedAttachmentSource | void>
28
26
  }
29
27
 
28
+ function headerSlots(props: {
29
+ onDismiss?: () => void
30
+ onToggle?: () => void
31
+ isExpanded?: boolean
32
+ paymentStatus?: PaymentStatus
33
+ isLoading?: boolean
34
+ }): { topLeft?: React.ReactNode; topRight?: React.ReactNode } {
35
+ const { onDismiss, onToggle, isExpanded, paymentStatus, isLoading } = props
36
+
37
+ if (onDismiss) {
38
+ return {
39
+ topLeft: undefined,
40
+ topRight: (
41
+ <button
42
+ type="button"
43
+ onClick={onDismiss}
44
+ className="flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
45
+ aria-label="Dismiss attachment"
46
+ >
47
+ <XIcon className="size-4" weight="bold" />
48
+ </button>
49
+ ),
50
+ }
51
+ }
52
+
53
+ if (onToggle) {
54
+ const Icon = isExpanded ? EyeIcon : EyeSlashIcon
55
+ return {
56
+ topLeft: (
57
+ <button
58
+ type="button"
59
+ onClick={onToggle}
60
+ className="flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
61
+ aria-label={isExpanded ? 'Hide preview' : 'Show preview'}
62
+ aria-pressed={isExpanded}
63
+ >
64
+ <Icon className="size-4" weight="fill" />
65
+ </button>
66
+ ),
67
+ topRight: undefined,
68
+ }
69
+ }
70
+
71
+ const Icon = paymentStatus === 'paid' ? LockOpenIcon : LockIcon
72
+
73
+ return {
74
+ topLeft: (
75
+ <div className="flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
76
+ {isLoading ? (
77
+ <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
78
+ ) : (
79
+ <Icon className="size-4" weight="fill" />
80
+ )}
81
+ </div>
82
+ ),
83
+ topRight: undefined,
84
+ }
85
+ }
86
+
30
87
  const CreatorCard: React.FC<CreatorCardProps> = ({
31
88
  title,
32
89
  mimeType = 'application/octet-stream',
@@ -78,122 +135,75 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
78
135
 
79
136
  const toggleHandler = onFetchSource || onPreviewClick ? handleToggle : undefined
80
137
 
81
- return (
82
- <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-[#121110] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
83
- <CardHeader
84
- onDismiss={onDismiss}
85
- onToggle={isBusy ? undefined : toggleHandler}
86
- isExpanded={isPreviewVisible}
87
- paymentStatus={paymentStatus}
88
- isLoading={isBusy}
89
- />
90
-
91
- <CardThumbnail
92
- title={title}
93
- sourceUrl={effectiveSourceUrl}
94
- thumbnailUrl={effectiveThumbnailUrl}
95
- mimeType={mimeType}
96
- onToggle={isBusy ? undefined : toggleHandler}
97
- />
98
-
99
- <div className="px-4 pb-3 pt-3">
100
- <p
101
- className={classNames('mb-0.5 truncate text-base font-medium', {
102
- 'text-white/30': !title,
103
- 'text-white': !!title,
138
+ const statusBadge =
139
+ paymentStatus === 'paid' ? (
140
+ <React.Fragment>
141
+ <span className="text-xs font-medium text-white/55">&bull;</span>
142
+ <span className="text-xs font-medium text-[#34c759]">Sold</span>
143
+ <CheckCircleIcon className="size-4 text-[#34c759]" weight="bold" />
144
+ </React.Fragment>
145
+ ) : (
146
+ <React.Fragment>
147
+ <span className="text-xs font-medium text-white/55">&bull;</span>
148
+ <span
149
+ className={classNames('text-xs font-medium', {
150
+ 'text-white/30': !amountText,
151
+ 'text-white/55': !!amountText,
104
152
  })}
105
153
  >
106
- {title || placeholderTitle}
107
- </p>
108
-
109
- <div className="flex items-center gap-1">
110
- {renderTypeIcon(mimeType, {
111
- className: 'size-5 shrink-0 text-white/55',
112
- weight: 'regular',
113
- })}
114
-
115
- {detail && (
116
- <span className="text-xs font-medium text-white/55">{detail}</span>
117
- )}
118
-
119
- {paymentStatus === 'paid' ? (
120
- <React.Fragment>
121
- <span className="text-xs font-medium text-white/55">&bull;</span>
122
- <span className="text-xs font-medium text-[#34c759]">Sold</span>
123
- <CheckCircleIcon className="size-4 text-[#34c759]" weight="bold" />
124
- </React.Fragment>
125
- ) : (
126
- <React.Fragment>
127
- <span className="text-xs font-medium text-white/55">&bull;</span>
128
- <span
129
- className={classNames('text-xs font-medium', {
130
- 'text-white/30': !amountText,
131
- 'text-white/55': !!amountText,
132
- })}
133
- >
134
- {amountText || placeholderAmountText}
135
- </span>
136
- </React.Fragment>
137
- )}
138
- </div>
139
- </div>
140
- </div>
141
- )
142
- }
143
-
144
- interface CardHeaderProps {
145
- onDismiss?: () => void
146
- onToggle?: () => void
147
- isExpanded?: boolean
148
- paymentStatus?: PaymentStatus
149
- isLoading?: boolean
150
- }
151
-
152
- const CardHeader: React.FC<CardHeaderProps> = ({
153
- onDismiss,
154
- onToggle,
155
- isExpanded,
156
- paymentStatus,
157
- isLoading,
158
- }) => {
159
- if (onDismiss) {
160
- return (
161
- <button
162
- type="button"
163
- onClick={onDismiss}
164
- className="absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white right-3"
165
- aria-label="Dismiss attachment"
166
- >
167
- <XIcon className="size-4" weight="bold" />
168
- </button>
169
- )
170
- }
171
-
172
- if (onToggle) {
173
- const Icon = isExpanded ? EyeIcon : EyeSlashIcon
174
- return (
175
- <button
176
- type="button"
177
- onClick={onToggle}
178
- className="absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3"
179
- aria-label={isExpanded ? 'Hide preview' : 'Show preview'}
180
- aria-pressed={isExpanded}
181
- >
182
- <Icon className="size-4" weight="fill" />
183
- </button>
154
+ {amountText || placeholderAmountText}
155
+ </span>
156
+ </React.Fragment>
184
157
  )
185
- }
186
158
 
187
- const Icon = paymentStatus === 'paid' ? LockOpenIcon : LockIcon
159
+ const { topLeft, topRight } = headerSlots({
160
+ onDismiss,
161
+ onToggle: isBusy ? undefined : toggleHandler,
162
+ isExpanded: isPreviewVisible,
163
+ paymentStatus,
164
+ isLoading: isBusy,
165
+ })
188
166
 
189
167
  return (
190
- <div className="absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3">
191
- {isLoading ? (
192
- <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
193
- ) : (
194
- <Icon className="size-4" weight="fill" />
195
- )}
196
- </div>
168
+ <AttachmentCard
169
+ variant="dark"
170
+ title={title}
171
+ placeholderTitle={placeholderTitle}
172
+ mimeType={mimeType}
173
+ detail={detail}
174
+ statusBadge={statusBadge}
175
+ topLeft={topLeft}
176
+ topRight={topRight}
177
+ thumbnail={
178
+ <button
179
+ type="button"
180
+ disabled={!toggleHandler || isBusy}
181
+ className={classNames(
182
+ 'relative block w-full overflow-hidden border-0 bg-white/10 p-0 text-left appearance-none',
183
+ { 'cursor-pointer': !!toggleHandler && !isBusy, 'cursor-default': !toggleHandler || isBusy }
184
+ )}
185
+ onClick={isBusy ? undefined : toggleHandler}
186
+ aria-label={toggleHandler ? 'Toggle preview' : undefined}
187
+ aria-busy={isBusy}
188
+ >
189
+ <AttachmentThumbnail
190
+ mimeType={mimeType}
191
+ sourceUrl={effectiveSourceUrl}
192
+ thumbnailUrl={effectiveThumbnailUrl}
193
+ title={title}
194
+ variant="dark"
195
+ mediaPlayerProps={
196
+ effectiveSourceUrl
197
+ ? { autoPlay: true, loop: true, controls: true, muted: false }
198
+ : undefined
199
+ }
200
+ />
201
+ {!isPreviewVisible && (
202
+ <div className="pointer-events-none absolute inset-0 bg-black/30" />
203
+ )}
204
+ </button>
205
+ }
206
+ />
197
207
  )
198
208
  }
199
209