@linktr.ee/messaging-react 1.33.3 → 1.34.0-rc-1777526669

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.
@@ -8,25 +8,17 @@ const meta: Meta<typeof MediaMessage> = {
8
8
  title: 'Components/MediaMessage',
9
9
  component: MediaMessage,
10
10
  parameters: {
11
- layout: 'centered',
11
+ layout: 'fullscreen',
12
12
  },
13
13
  }
14
14
  export default meta
15
15
 
16
- // ---------------------------------------------------------------------------
17
- // Shared helpers
18
- // ---------------------------------------------------------------------------
19
-
20
- const SENDER = { id: 'user-other', name: 'Jane Creator', image: 'https://i.pravatar.cc/40?img=5' }
21
- const ME = { id: 'user-me', name: 'Me' }
22
-
23
16
  const base = (overrides: Partial<LocalMessage> = {}): LocalMessage => ({
24
17
  id: 'msg-1',
25
18
  text: '',
26
19
  type: 'regular',
27
20
  created_at: new Date(),
28
21
  updated_at: new Date(),
29
- user: SENDER,
30
22
  attachments: [],
31
23
  ...overrides,
32
24
  })
@@ -121,20 +113,17 @@ const LINK_VARIANTS = [
121
113
  },
122
114
  ] as const
123
115
 
124
- // ---------------------------------------------------------------------------
125
- // Layout primitives
126
- // ---------------------------------------------------------------------------
127
-
128
- const GridTable: React.FC<{ children: React.ReactNode }> = ({ children }) => (
129
- <div className="p-12 bg-[#F9F7F4]">
116
+ const Table: React.FC<{ children: React.ReactNode }> = ({ children }) => (
117
+ <div className="min-h-screen w-full p-12 bg-[#F9F7F4]">
130
118
  <table className="border-separate border-spacing-4">{children}</table>
131
119
  </div>
132
120
  )
133
121
 
134
- const GridHead: React.FC<{ labels: readonly string[] }> = ({ labels }) => (
122
+ const TableHead: React.FC<{ variants: readonly { label: string }[] }> = ({ variants }) => (
135
123
  <thead>
136
124
  <tr>
137
- {labels.map((label) => (
125
+ <th className="text-left text-xs font-medium text-black/40 pb-2" />
126
+ {variants.map(({ label }) => (
138
127
  <th key={label} className="text-left text-xs font-medium text-black/40 pb-2">
139
128
  {label}
140
129
  </th>
@@ -143,91 +132,70 @@ const GridHead: React.FC<{ labels: readonly string[] }> = ({ labels }) => (
143
132
  </thead>
144
133
  )
145
134
 
146
- // ---------------------------------------------------------------------------
147
- // Stories
148
- // ---------------------------------------------------------------------------
149
-
150
- export const Received: StoryFn = () => (
151
- <GridTable>
152
- <GridHead labels={VARIANTS.map((v) => v.label)} />
135
+ export const Attachment: StoryFn = () => (
136
+ <Table>
137
+ <TableHead variants={VARIANTS} />
153
138
  <tbody>
154
139
  <tr>
140
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
141
+ Received
142
+ </td>
155
143
  {VARIANTS.map(({ label, attachment }) => (
156
144
  <td key={label} className="align-top">
157
145
  <MediaMessage
158
146
  isMyMessage={false}
159
- message={base({ user: SENDER, attachments: [attachment as LocalMessage['attachments'][number]] })}
147
+ message={base({ attachments: [attachment as LocalMessage['attachments'][number]] })}
160
148
  />
161
149
  </td>
162
150
  ))}
163
151
  </tr>
164
- </tbody>
165
- </GridTable>
166
- )
167
- Received.parameters = {
168
- docs: {
169
- description: {
170
- story: 'Received messages — left-aligned with avatar, light gray card background.',
171
- },
172
- },
173
- }
174
-
175
- export const Sent: StoryFn = () => (
176
- <GridTable>
177
- <GridHead labels={VARIANTS.map((v) => v.label)} />
178
- <tbody>
179
152
  <tr>
153
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
154
+ Sent
155
+ </td>
180
156
  {VARIANTS.map(({ label, attachment }) => (
181
157
  <td key={label} className="align-top">
182
158
  <MediaMessage
183
159
  isMyMessage={true}
184
- message={base({ user: ME, attachments: [attachment as LocalMessage['attachments'][number]] })}
160
+ message={base({ attachments: [attachment as LocalMessage['attachments'][number]] })}
185
161
  />
186
162
  </td>
187
163
  ))}
188
164
  </tr>
189
165
  </tbody>
190
- </GridTable>
166
+ </Table>
191
167
  )
192
- Sent.parameters = {
193
- docs: {
194
- description: {
195
- story: 'Sent messages — right-aligned, no avatar, black card background.',
196
- },
197
- },
198
- }
199
168
 
200
169
  export const Links: StoryFn = () => (
201
- <GridTable>
202
- <GridHead labels={LINK_VARIANTS.map((v) => v.label)} />
170
+ <Table>
171
+ <TableHead variants={LINK_VARIANTS} />
203
172
  <tbody>
204
173
  <tr>
174
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
175
+ Received
176
+ </td>
205
177
  {LINK_VARIANTS.map(({ label, attachment }) => (
206
178
  <td key={label} className="align-top">
207
179
  <MediaMessage
208
180
  isMyMessage={false}
209
- message={base({ user: SENDER, attachments: [attachment as LocalMessage['attachments'][number]] })}
181
+ message={base({ attachments: [attachment as LocalMessage['attachments'][number]] })}
210
182
  />
211
183
  </td>
212
184
  ))}
213
185
  </tr>
214
186
  <tr>
187
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
188
+ Sent
189
+ </td>
215
190
  {LINK_VARIANTS.map(({ label, attachment }) => (
216
191
  <td key={label} className="align-top">
217
192
  <MediaMessage
218
193
  isMyMessage={true}
219
- message={base({ user: ME, attachments: [attachment as LocalMessage['attachments'][number]] })}
194
+ message={base({ attachments: [attachment as LocalMessage['attachments'][number]] })}
220
195
  />
221
196
  </td>
222
197
  ))}
223
198
  </tr>
224
199
  </tbody>
225
- </GridTable>
200
+ </Table>
226
201
  )
227
- Links.parameters = {
228
- docs: {
229
- description: {
230
- story: 'Link preview cards — top row received, bottom row sent. Shows thumbnail, title, description, and URL with image fallback.',
231
- },
232
- },
233
- }
@@ -113,7 +113,7 @@ const MediaMeta: React.FC<{
113
113
  <div className="flex items-start gap-2 px-4 pb-3 pt-3">
114
114
  <div className="min-w-0 flex-1">
115
115
  {title && (
116
- <p className={`mb-1.5 truncate text-base font-medium ${primaryText(isMyMessage)}`}>
116
+ <p className={`truncate text-base font-medium ${primaryText(isMyMessage)}`}>
117
117
  {title}
118
118
  </p>
119
119
  )}
@@ -0,0 +1,126 @@
1
+ import { StreamChatService } from '@linktr.ee/messaging-core'
2
+ import { render, waitFor, act } from '@testing-library/react'
3
+ import React from 'react'
4
+ import type { StreamChat } from 'stream-chat'
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
6
+
7
+ import { MessagingProvider, useMessagingContext } from './MessagingProvider'
8
+
9
+ // Stub stream-chat-react's <Chat /> so we don't need a real, fully-wired
10
+ // StreamChat instance just to verify provider state.
11
+ vi.mock('stream-chat-react', () => ({
12
+ Chat: ({ children }: { children: React.ReactNode }) => <>{children}</>,
13
+ }))
14
+
15
+ const setupServiceMock = () => {
16
+ const connectUser = vi.fn(
17
+ async () => ({ userID: 'mock' }) as unknown as StreamChat
18
+ )
19
+ const disconnectUser = vi.fn(async () => undefined)
20
+
21
+ vi.spyOn(StreamChatService.prototype, 'connectUser').mockImplementation(
22
+ connectUser
23
+ )
24
+ vi.spyOn(StreamChatService.prototype, 'disconnectUser').mockImplementation(
25
+ disconnectUser
26
+ )
27
+
28
+ return { connectUser, disconnectUser }
29
+ }
30
+
31
+ const Probe: React.FC<{ onState: (state: unknown) => void }> = ({
32
+ onState,
33
+ }) => {
34
+ const ctx = useMessagingContext()
35
+ React.useEffect(() => {
36
+ onState({
37
+ isConnected: ctx.isConnected,
38
+ isLoading: ctx.isLoading,
39
+ error: ctx.error,
40
+ hasClient: !!ctx.client,
41
+ })
42
+ }, [ctx, onState])
43
+ return null
44
+ }
45
+
46
+ describe('MessagingProvider', () => {
47
+ beforeEach(() => {
48
+ vi.restoreAllMocks()
49
+ })
50
+
51
+ it('connects a guest user through the underlying service', async () => {
52
+ const { connectUser } = setupServiceMock()
53
+ const states: unknown[] = []
54
+
55
+ render(
56
+ <MessagingProvider
57
+ apiKey="mock-api-key"
58
+ user={{ type: 'guest', id: 'guest-1', name: 'Guest' }}
59
+ serviceConfig={{
60
+ createChannel: async () => ({ channelId: 'ch-1' }),
61
+ }}
62
+ >
63
+ <Probe onState={(s) => states.push(s)} />
64
+ </MessagingProvider>
65
+ )
66
+
67
+ await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(1))
68
+ expect(connectUser).toHaveBeenCalledWith({
69
+ type: 'guest',
70
+ id: 'guest-1',
71
+ name: 'Guest',
72
+ })
73
+
74
+ await waitFor(() => {
75
+ const last = states[states.length - 1] as { isConnected: boolean }
76
+ expect(last.isConnected).toBe(true)
77
+ })
78
+ })
79
+
80
+ it('disconnects and reconnects when remounted with a new key', async () => {
81
+ const { connectUser, disconnectUser } = setupServiceMock()
82
+
83
+ const guestUser = {
84
+ type: 'guest' as const,
85
+ id: 'guest-1',
86
+ name: 'Guest',
87
+ }
88
+ const authedUser = { id: 'user-1', name: 'Authed User' }
89
+
90
+ const { rerender } = render(
91
+ <MessagingProvider
92
+ key="guest"
93
+ apiKey="mock-api-key"
94
+ user={guestUser}
95
+ serviceConfig={{
96
+ createChannel: async () => ({ channelId: 'ch-1' }),
97
+ }}
98
+ >
99
+ <div />
100
+ </MessagingProvider>
101
+ )
102
+
103
+ await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(1))
104
+ expect(connectUser).toHaveBeenLastCalledWith(guestUser)
105
+
106
+ await act(async () => {
107
+ rerender(
108
+ <MessagingProvider
109
+ key="authed"
110
+ apiKey="mock-api-key"
111
+ user={authedUser}
112
+ serviceConfig={{
113
+ fetchToken: async () => 'token',
114
+ createChannel: async () => ({ channelId: 'ch-1' }),
115
+ }}
116
+ >
117
+ <div />
118
+ </MessagingProvider>
119
+ )
120
+ })
121
+
122
+ await waitFor(() => expect(disconnectUser).toHaveBeenCalled())
123
+ await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(2))
124
+ expect(connectUser).toHaveBeenLastCalledWith(authedUser)
125
+ })
126
+ })
package/src/styles.css CHANGED
@@ -152,10 +152,37 @@
152
152
  background: transparent;
153
153
  }
154
154
 
155
- /* Wrapper to ensure tag stays below bubble */
156
- .str-chat__message-bubble-wrapper {
155
+ .str-chat__li .str-chat__message--me {
156
+ position: relative;
157
+ }
158
+
159
+ .str-chat__li .str-chat__message-inner {
160
+ grid-column-gap: 0;
161
+ }
162
+
163
+ .str-chat__li .str-chat__message--me .str-chat__message-bubble-wrapper {
164
+ display: flex;
165
+ flex-direction: column;
166
+ align-items: flex-end;
167
+ gap: 2px;
168
+ }
169
+
170
+ .str-chat__li .str-chat__message--other {
171
+ position: relative;
172
+ grid-template-columns: 40px 1fr;
173
+ }
174
+
175
+ .str-chat__li .str-chat__message--other .str-chat__message-bubble-wrapper {
157
176
  display: flex;
158
177
  flex-direction: column;
178
+ align-items: flex-start;
179
+ gap: 2px;
180
+ }
181
+
182
+ .str-chat__li .str-chat__message--other .str-chat__avatar {
183
+ position: absolute;
184
+ left: 0;
185
+ bottom: 0;
159
186
  }
160
187
 
161
188
  .str-chat__typing-indicator {
@@ -186,7 +213,6 @@
186
213
  display: inline-flex;
187
214
  align-items: center;
188
215
  gap: 4px;
189
- margin-top: 4px;
190
216
  padding: 0;
191
217
  font-size: 12px;
192
218
  font-weight: 500;
@@ -1,181 +0,0 @@
1
- import { jsxs as i, jsx as e } from "react/jsx-runtime";
2
- import { CheckCircleIcon as N, XIcon as v, EyeIcon as k, EyeSlashIcon as z, LockOpenIcon as I, LockIcon as y } from "@phosphor-icons/react";
3
- import d from "classnames";
4
- import h, { useState as j, useCallback as _ } from "react";
5
- import { r as b, g as C, M as S } from "./index-Ydi1pTAi.js";
6
- const M = ({
7
- title: a,
8
- sourceUrl: l,
9
- thumbnailUrl: t,
10
- mimeType: s,
11
- onToggle: r
12
- }) => {
13
- const n = r && l && t;
14
- return /* @__PURE__ */ i(
15
- "button",
16
- {
17
- type: "button",
18
- disabled: !r,
19
- className: d(
20
- "relative block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none",
21
- { "cursor-pointer": !!r, "cursor-default": !r }
22
- ),
23
- onClick: r,
24
- "aria-label": r ? "Toggle preview" : void 0,
25
- children: [
26
- n ? /* @__PURE__ */ e(
27
- E,
28
- {
29
- sourceUrl: l,
30
- thumbnailUrl: t,
31
- mimeType: s
32
- }
33
- ) : t ? /* @__PURE__ */ e("div", { className: "aspect-video overflow-hidden", children: /* @__PURE__ */ e(
34
- "img",
35
- {
36
- src: t,
37
- alt: a,
38
- draggable: !1,
39
- className: "absolute inset-0 h-full w-full object-cover"
40
- }
41
- ) }) : /* @__PURE__ */ e("div", { className: "aspect-video flex items-center justify-center", children: b(s, {
42
- className: "size-12 text-black/20",
43
- weight: "regular"
44
- }) }),
45
- !n && /* @__PURE__ */ e("div", { className: "pointer-events-none absolute inset-0 bg-black/30" })
46
- ]
47
- }
48
- );
49
- }, E = ({
50
- sourceUrl: a,
51
- thumbnailUrl: l,
52
- mimeType: t
53
- }) => {
54
- const s = C(t);
55
- return s === "video" || s === "audio" ? /* @__PURE__ */ e(
56
- S,
57
- {
58
- mimeType: t,
59
- source: a,
60
- poster: l,
61
- autoPlay: !0,
62
- loop: !0,
63
- controls: !0,
64
- muted: !1
65
- }
66
- ) : s === "image" ? /* @__PURE__ */ e("img", { src: a, alt: "", className: "block w-full", draggable: !1 }) : s === "document" ? /* @__PURE__ */ e(
67
- "img",
68
- {
69
- src: l,
70
- alt: "",
71
- className: "block w-full",
72
- draggable: !1
73
- }
74
- ) : null;
75
- }, X = ({
76
- title: a,
77
- mimeType: l = "application/octet-stream",
78
- thumbnailUrl: t,
79
- detail: s,
80
- amountText: r,
81
- placeholderTitle: n = "Attachment title",
82
- placeholderAmountText: p,
83
- paymentStatus: u,
84
- onDismiss: x,
85
- onPreviewClick: o
86
- }) => {
87
- const [c, m] = j(), g = c == null ? void 0 : c.sourceUrl, w = (c == null ? void 0 : c.thumbnailUrl) ?? t, f = _(() => {
88
- c ? m(void 0) : o && m(o());
89
- }, [c, o]);
90
- return /* @__PURE__ */ i("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)]", children: [
91
- /* @__PURE__ */ e(
92
- F,
93
- {
94
- onDismiss: x,
95
- onToggle: o ? f : void 0,
96
- isExpanded: !!c,
97
- paymentStatus: u
98
- }
99
- ),
100
- /* @__PURE__ */ e(
101
- M,
102
- {
103
- title: a,
104
- sourceUrl: g,
105
- thumbnailUrl: w,
106
- mimeType: l,
107
- onToggle: o ? f : void 0
108
- }
109
- ),
110
- /* @__PURE__ */ i("div", { className: "px-4 pb-3 pt-3 bg-black", children: [
111
- /* @__PURE__ */ e(
112
- "p",
113
- {
114
- className: d("mb-1.5 truncate text-base font-medium", {
115
- "text-white/30": !a,
116
- "text-white": !!a
117
- }),
118
- children: a || n
119
- }
120
- ),
121
- /* @__PURE__ */ i("div", { className: "flex items-center gap-1", children: [
122
- b(l, {
123
- className: "size-5 shrink-0 text-white/55",
124
- weight: "regular"
125
- }),
126
- s && /* @__PURE__ */ e("span", { className: "text-xs font-medium text-white/55", children: s }),
127
- u === "paid" ? /* @__PURE__ */ i(h.Fragment, { children: [
128
- /* @__PURE__ */ e("span", { className: "text-xs font-medium text-white/55", children: "•" }),
129
- /* @__PURE__ */ e("span", { className: "text-xs font-medium text-[#4ade80]", children: "Sold" }),
130
- /* @__PURE__ */ e(
131
- N,
132
- {
133
- className: "size-4 text-[#4ade80]",
134
- weight: "bold"
135
- }
136
- )
137
- ] }) : /* @__PURE__ */ i(h.Fragment, { children: [
138
- /* @__PURE__ */ e("span", { className: "text-xs font-medium text-white/55", children: "•" }),
139
- /* @__PURE__ */ e(
140
- "span",
141
- {
142
- className: d("text-xs font-medium", {
143
- "text-white/30": !r,
144
- "text-white/55": !!r
145
- }),
146
- children: r || p
147
- }
148
- )
149
- ] })
150
- ] })
151
- ] })
152
- ] });
153
- }, F = ({
154
- onDismiss: a,
155
- onToggle: l,
156
- isExpanded: t,
157
- paymentStatus: s
158
- }) => a ? /* @__PURE__ */ e(
159
- "button",
160
- {
161
- type: "button",
162
- onClick: a,
163
- className: "absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white right-3",
164
- "aria-label": "Dismiss attachment",
165
- children: /* @__PURE__ */ e(v, { className: "size-4", weight: "bold" })
166
- }
167
- ) : l ? /* @__PURE__ */ e(
168
- "button",
169
- {
170
- type: "button",
171
- onClick: l,
172
- className: "absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3",
173
- "aria-label": t ? "Hide preview" : "Show preview",
174
- "aria-pressed": t,
175
- children: /* @__PURE__ */ e(t ? k : z, { className: "size-4", weight: "fill" })
176
- }
177
- ) : /* @__PURE__ */ e("div", { className: "absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3", children: /* @__PURE__ */ e(s === "paid" ? I : y, { className: "size-4", weight: "fill" }) });
178
- export {
179
- X as default
180
- };
181
- //# sourceMappingURL=Card-BfA8wq8O.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Card-BfA8wq8O.js","sources":["../src/components/LockedAttachment/components/Creator/CardThumbnail.tsx","../src/components/LockedAttachment/components/Creator/Card.tsx"],"sourcesContent":["import classNames from 'classnames'\nimport React from 'react'\n\nimport { renderTypeIcon } from '../../utils/icons'\nimport { getSourceType } from '../../utils/mimeType'\nimport MediaPlayer from '../MediaPlayer'\n\ninterface CardThumbnailProps {\n title?: string\n sourceUrl?: string\n thumbnailUrl?: string\n mimeType: string\n onToggle?: () => void\n}\n\nconst CardThumbnail: React.FC<CardThumbnailProps> = ({\n title,\n sourceUrl,\n thumbnailUrl,\n mimeType,\n onToggle,\n}) => {\n const isExpanded = onToggle && sourceUrl && thumbnailUrl\n\n return (\n <button\n type=\"button\"\n disabled={!onToggle}\n className={classNames(\n 'relative block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none',\n { 'cursor-pointer': !!onToggle, 'cursor-default': !onToggle }\n )}\n onClick={onToggle}\n aria-label={onToggle ? 'Toggle preview' : undefined}\n >\n {isExpanded ? (\n <ThumbnailMedia\n sourceUrl={sourceUrl}\n thumbnailUrl={thumbnailUrl}\n mimeType={mimeType}\n />\n ) : thumbnailUrl ? (\n <div className=\"aspect-video overflow-hidden\">\n <img\n src={thumbnailUrl}\n alt={title}\n draggable={false}\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n </div>\n ) : (\n <div className=\"aspect-video flex items-center justify-center\">\n {renderTypeIcon(mimeType, {\n className: 'size-12 text-black/20',\n weight: 'regular',\n })}\n </div>\n )}\n\n {!isExpanded && (\n <div className=\"pointer-events-none absolute inset-0 bg-black/30\" />\n )}\n </button>\n )\n}\n\ninterface ThumbnailMediaProps {\n sourceUrl: string\n thumbnailUrl: string\n mimeType: string\n}\n\nconst ThumbnailMedia: React.FC<ThumbnailMediaProps> = ({\n sourceUrl,\n thumbnailUrl,\n mimeType,\n}) => {\n const sourceType = getSourceType(mimeType)\n\n if (sourceType === 'video' || sourceType === 'audio') {\n return (\n <MediaPlayer\n mimeType={mimeType}\n source={sourceUrl}\n poster={thumbnailUrl}\n autoPlay={true}\n loop={true}\n controls={true}\n muted={false}\n />\n )\n }\n\n if (sourceType === 'image') {\n return (\n <img src={sourceUrl} alt=\"\" className=\"block w-full\" draggable={false} />\n )\n }\n\n if (sourceType === 'document') {\n return (\n <img\n src={thumbnailUrl}\n alt=\"\"\n className=\"block w-full\"\n draggable={false}\n />\n )\n }\n\n return null\n}\n\nexport default CardThumbnail\n","import {\n CheckCircleIcon,\n EyeIcon,\n EyeSlashIcon,\n LockIcon,\n LockOpenIcon,\n XIcon,\n} from '@phosphor-icons/react'\nimport classNames from 'classnames'\nimport React, { useCallback, useState } from 'react'\n\nimport type {\n LockedAttachmentBaseProps,\n LockedAttachmentSource,\n PaymentStatus,\n} from '../../types'\nimport { renderTypeIcon } from '../../utils/icons'\n\nimport CardThumbnail from './CardThumbnail'\n\nexport interface CreatorCardProps extends LockedAttachmentBaseProps {\n placeholderTitle?: string\n placeholderAmountText?: string\n onDismiss?: () => void\n onPreviewClick?: () => LockedAttachmentSource\n}\n\nconst CreatorCard: React.FC<CreatorCardProps> = ({\n title,\n mimeType = 'application/octet-stream',\n thumbnailUrl,\n detail,\n amountText,\n placeholderTitle = 'Attachment title',\n placeholderAmountText,\n paymentStatus,\n onDismiss,\n onPreviewClick,\n}) => {\n const [source, setSource] = useState<LockedAttachmentSource | undefined>()\n\n const effectiveSourceUrl = source?.sourceUrl\n const effectiveThumbnailUrl = source?.thumbnailUrl ?? thumbnailUrl\n\n const handleToggle = useCallback(() => {\n if (source) {\n setSource(undefined)\n } else if (onPreviewClick) {\n setSource(onPreviewClick())\n }\n }, [source, onPreviewClick])\n\n return (\n <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)]\">\n <CardHeader\n onDismiss={onDismiss}\n onToggle={onPreviewClick ? handleToggle : undefined}\n isExpanded={!!source}\n paymentStatus={paymentStatus}\n />\n\n <CardThumbnail\n title={title}\n sourceUrl={effectiveSourceUrl}\n thumbnailUrl={effectiveThumbnailUrl}\n mimeType={mimeType}\n onToggle={onPreviewClick ? handleToggle : undefined}\n />\n\n <div className=\"px-4 pb-3 pt-3 bg-black\">\n <p\n className={classNames('mb-1.5 truncate text-base font-medium', {\n 'text-white/30': !title,\n 'text-white': !!title,\n })}\n >\n {title || placeholderTitle}\n </p>\n\n <div className=\"flex items-center gap-1\">\n {renderTypeIcon(mimeType, {\n className: 'size-5 shrink-0 text-white/55',\n weight: 'regular',\n })}\n\n {detail && (\n <span className=\"text-xs font-medium text-white/55\">{detail}</span>\n )}\n\n {paymentStatus === 'paid' ? (\n <React.Fragment>\n <span className=\"text-xs font-medium text-white/55\">&bull;</span>\n <span className=\"text-xs font-medium text-[#4ade80]\">Sold</span>\n <CheckCircleIcon\n className=\"size-4 text-[#4ade80]\"\n weight=\"bold\"\n />\n </React.Fragment>\n ) : (\n <React.Fragment>\n <span className=\"text-xs font-medium text-white/55\">&bull;</span>\n <span\n className={classNames('text-xs font-medium', {\n 'text-white/30': !amountText,\n 'text-white/55': !!amountText,\n })}\n >\n {amountText || placeholderAmountText}\n </span>\n </React.Fragment>\n )}\n </div>\n </div>\n </div>\n )\n}\n\ninterface CardHeaderProps {\n onDismiss?: () => void\n onToggle?: () => void\n isExpanded?: boolean\n paymentStatus?: PaymentStatus\n}\n\nconst CardHeader: React.FC<CardHeaderProps> = ({\n onDismiss,\n onToggle,\n isExpanded,\n paymentStatus,\n}) => {\n if (onDismiss) {\n return (\n <button\n type=\"button\"\n onClick={onDismiss}\n className=\"absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white right-3\"\n aria-label=\"Dismiss attachment\"\n >\n <XIcon className=\"size-4\" weight=\"bold\" />\n </button>\n )\n }\n\n if (onToggle) {\n const Icon = isExpanded ? EyeIcon : EyeSlashIcon\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3\"\n aria-label={isExpanded ? 'Hide preview' : 'Show preview'}\n aria-pressed={isExpanded}\n >\n <Icon className=\"size-4\" weight=\"fill\" />\n </button>\n )\n }\n\n const Icon = paymentStatus === 'paid' ? LockOpenIcon : LockIcon\n\n return (\n <div className=\"absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3\">\n <Icon className=\"size-4\" weight=\"fill\" />\n </div>\n )\n}\n\nexport default CreatorCard\n"],"names":["CardThumbnail","title","sourceUrl","thumbnailUrl","mimeType","onToggle","isExpanded","jsxs","classNames","jsx","ThumbnailMedia","sourceType","getSourceType","MediaPlayer","CreatorCard","detail","amountText","placeholderTitle","placeholderAmountText","paymentStatus","onDismiss","onPreviewClick","source","setSource","useState","effectiveSourceUrl","effectiveThumbnailUrl","handleToggle","useCallback","CardHeader","renderTypeIcon","React","CheckCircleIcon","XIcon","EyeIcon","EyeSlashIcon","LockOpenIcon","LockIcon"],"mappings":"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"}