@linktr.ee/messaging-react 3.9.0-rc-1783472582 → 3.9.0-rc-1783575293

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 (85) hide show
  1. package/dist/Card-B7ccxsKf.cjs +2 -0
  2. package/dist/Card-B7ccxsKf.cjs.map +1 -0
  3. package/dist/Card-CXRSSrCh.cjs +2 -0
  4. package/dist/Card-CXRSSrCh.cjs.map +1 -0
  5. package/dist/Card-DaUKmwtb.js +105 -0
  6. package/dist/Card-DaUKmwtb.js.map +1 -0
  7. package/dist/Card-Q1Ee_UCB.js +693 -0
  8. package/dist/Card-Q1Ee_UCB.js.map +1 -0
  9. package/dist/{LoadingDots-nW6r0-Kg.js → LoadingDots-Ct3WmVlT.js} +18 -21
  10. package/dist/LoadingDots-Ct3WmVlT.js.map +1 -0
  11. package/dist/LoadingDots-D9GGphgM.cjs +2 -0
  12. package/dist/LoadingDots-D9GGphgM.cjs.map +1 -0
  13. package/dist/assets/index.css +1 -1
  14. package/dist/{index-B8gqCioX.js → index-BiBvPADG.js} +1301 -1315
  15. package/dist/index-BiBvPADG.js.map +1 -0
  16. package/dist/index-DuPa2mUh.cjs +2 -0
  17. package/dist/index-DuPa2mUh.cjs.map +1 -0
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.ts +149 -129
  20. package/dist/index.js +1 -1
  21. package/package.json +2 -2
  22. package/src/components/ChannelList/CustomChannelPreview.test.tsx +95 -0
  23. package/src/components/ChannelList/CustomChannelPreview.tsx +21 -9
  24. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/LockedAttachment.stories.tsx +87 -78
  25. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/LockedAttachment.test.tsx +24 -11
  26. package/src/components/CustomMessage/LockedAttachment/components/Media/Card.tsx +358 -0
  27. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/Media/CardActions.tsx +5 -0
  28. package/src/components/CustomMessage/LockedAttachment/components/Media/types.ts +79 -0
  29. package/src/components/CustomMessage/LockedAttachment/components/Text/Card.tsx +155 -0
  30. package/src/components/CustomMessage/LockedAttachment/components/Text/types.ts +58 -0
  31. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/CardBody.tsx +5 -4
  32. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/GalleryThumbnail.tsx +2 -2
  33. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/LockBadge.tsx +1 -1
  34. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/PurchaseStatusBadge.tsx +0 -12
  35. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/SingleThumbnail.tsx +1 -1
  36. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/SkeletonTextLines.tsx +3 -13
  37. package/src/components/CustomMessage/LockedAttachment/components/_shared/TextShell.tsx +30 -0
  38. package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/usePreviewToggle.test.ts +1 -1
  39. package/src/components/CustomMessage/LockedAttachment/index.tsx +105 -0
  40. package/src/components/CustomMessage/LockedAttachment/types.ts +44 -0
  41. package/src/components/CustomMessage/context.tsx +1 -1
  42. package/src/components/CustomMessage/index.tsx +45 -50
  43. package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +5 -3
  44. package/src/index.ts +3 -5
  45. package/src/styles.css +8 -1
  46. package/src/types.ts +1 -1
  47. package/dist/Card-BVZM0-06.js +0 -108
  48. package/dist/Card-BVZM0-06.js.map +0 -1
  49. package/dist/Card-D0hLrOxX.js +0 -221
  50. package/dist/Card-D0hLrOxX.js.map +0 -1
  51. package/dist/Card-DIr1UnfC.cjs +0 -2
  52. package/dist/Card-DIr1UnfC.cjs.map +0 -1
  53. package/dist/Card-DeSgaRTL.cjs +0 -2
  54. package/dist/Card-DeSgaRTL.cjs.map +0 -1
  55. package/dist/Card-DnaXMVrT.cjs +0 -2
  56. package/dist/Card-DnaXMVrT.cjs.map +0 -1
  57. package/dist/Card-xv52Ug43.js +0 -61
  58. package/dist/Card-xv52Ug43.js.map +0 -1
  59. package/dist/LoadingDots-Cn2y7g7L.cjs +0 -2
  60. package/dist/LoadingDots-Cn2y7g7L.cjs.map +0 -1
  61. package/dist/LoadingDots-nW6r0-Kg.js.map +0 -1
  62. package/dist/TextShell-CKQOFxoH.cjs +0 -2
  63. package/dist/TextShell-CKQOFxoH.cjs.map +0 -1
  64. package/dist/TextShell-DIyj9zQ7.js +0 -40
  65. package/dist/TextShell-DIyj9zQ7.js.map +0 -1
  66. package/dist/index-B8gqCioX.js.map +0 -1
  67. package/dist/index-IkikzyWM.cjs +0 -2
  68. package/dist/index-IkikzyWM.cjs.map +0 -1
  69. package/dist/usePreviewToggle-B0B8cfxy.cjs +0 -2
  70. package/dist/usePreviewToggle-B0B8cfxy.cjs.map +0 -1
  71. package/dist/usePreviewToggle-DRhRTUou.js +0 -357
  72. package/dist/usePreviewToggle-DRhRTUou.js.map +0 -1
  73. package/src/components/LockedAttachment/components/Composer/Card.tsx +0 -201
  74. package/src/components/LockedAttachment/components/Media/Card.tsx +0 -235
  75. package/src/components/LockedAttachment/components/Text/Card.tsx +0 -177
  76. package/src/components/LockedAttachment/components/_shared/TextShell.tsx +0 -19
  77. package/src/components/LockedAttachment/index.tsx +0 -52
  78. package/src/components/LockedAttachment/types.ts +0 -75
  79. /package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/CardThumbnail.tsx +0 -0
  80. /package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/LoadingDots.tsx +0 -0
  81. /package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/MediaShell.tsx +0 -0
  82. /package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/PreviewToggle.tsx +0 -0
  83. /package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/PriceLockRow.tsx +0 -0
  84. /package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/usePreviewToggle.ts +0 -0
  85. /package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/useReceivedSource.ts +0 -0
@@ -1,201 +0,0 @@
1
- import { PencilSimpleIcon, XIcon } from '@phosphor-icons/react'
2
- import React from 'react'
3
-
4
- import type { FetchSourceHandler, LockedAttachmentBaseProps } from '../../types'
5
- import CardBody from '../_shared/CardBody'
6
- import CardThumbnail, {
7
- galleryBodyIcon,
8
- isGalleryCard,
9
- } from '../_shared/CardThumbnail'
10
- import MediaShell from '../_shared/MediaShell'
11
- import PreviewToggle from '../_shared/PreviewToggle'
12
- import PriceLockRow from '../_shared/PriceLockRow'
13
- import SkeletonTextLines from '../_shared/SkeletonTextLines'
14
- import TextShell from '../_shared/TextShell'
15
- import { usePreviewToggle } from '../_shared/usePreviewToggle'
16
-
17
- export interface ComposerCardProps extends LockedAttachmentBaseProps {
18
- /**
19
- * The message's own text, shown as-is (not redacted yet — the message
20
- * hasn't been sent). Only used when `contentType` is `"text"`.
21
- */
22
- text?: string
23
- /** Placeholder shown in the amount slot before one is configured. */
24
- placeholderAmountText?: string
25
- /**
26
- * Renders a persistent dismiss X on the thumbnail (media) or next to the
27
- * text (text mode); called to remove the draft attachment. Unrelated to
28
- * `TextCardProps.onDismiss`, which only appears for `isMine` after a
29
- * failed payment.
30
- */
31
- onDismiss?: () => void
32
- /** Fired the first time the composer taps the thumbnail to preview. */
33
- onPreviewClick?: () => void
34
- /**
35
- * Lazily loads the underlying source so the composer can preview the
36
- * attachment they're about to send. Called the first time the thumbnail is
37
- * tapped; the returned source is cached and reused on subsequent toggles.
38
- * Ignored when `contentType` is `"text"`.
39
- */
40
- onFetchSource?: FetchSourceHandler
41
- /**
42
- * When provided, renders a pencil button in the body bottom-right that the
43
- * composer can use to edit the attachment metadata (e.g. open the price /
44
- * gallery editor). Matches the Composer "Button" instance in Figma.
45
- */
46
- onEditClick?: () => void
47
- }
48
-
49
- /**
50
- * Composer card in text mode: a normal outgoing chat bubble showing the real
51
- * draft text (it hasn't been sent/redacted yet) plus a dismiss X and a price
52
- * row. Media-only props are ignored here.
53
- */
54
- const ComposerTextCard: React.FC<ComposerCardProps> = ({
55
- text,
56
- amountText,
57
- placeholderAmountText,
58
- onDismiss,
59
- }) => (
60
- <TextShell isMine>
61
- <div className="pr-8">
62
- {text ? text : <SkeletonTextLines variant="dark" animate={false} />}
63
- </div>
64
- <PriceLockRow>
65
- <span className={amountText ? 'text-white/55' : 'text-white/30'}>
66
- {amountText || placeholderAmountText}
67
- </span>
68
- </PriceLockRow>
69
- {onDismiss && (
70
- <button
71
- type="button"
72
- onClick={onDismiss}
73
- className="absolute right-3 top-3 flex size-6 shrink-0 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/15"
74
- aria-label="Dismiss attachment"
75
- >
76
- <XIcon className="size-3" weight="bold" />
77
- </button>
78
- )}
79
- </TextShell>
80
- )
81
-
82
- /**
83
- * Composer card in media mode: the locked thumbnail (single or gallery) the
84
- * composer taps to preview the attachment they're about to send, plus the
85
- * title/price body with optional dismiss X and edit pencil.
86
- */
87
- const ComposerMediaCard: React.FC<ComposerCardProps> = ({
88
- title,
89
- mimeType = 'application/octet-stream',
90
- thumbnailUrl,
91
- detail,
92
- amountText,
93
- placeholderTitle = 'Attachment title',
94
- placeholderAmountText,
95
- gallery = [],
96
- onDismiss,
97
- onPreviewClick,
98
- onFetchSource,
99
- onEditClick,
100
- }) => {
101
- const isGallery = isGalleryCard(gallery)
102
-
103
- // Gallery is always previewable in the composer because each item's source
104
- // is already provided up-front via `gallery[*].sourceUrl`.
105
- const { source, isPreviewVisible, isLoadingPreview, togglePreview } =
106
- usePreviewToggle({ isGallery, onFetchSource, onPreviewClick })
107
-
108
- const showLocked = !isPreviewVisible
109
- const isBusy = isLoadingPreview
110
-
111
- const statusBadge = (
112
- <React.Fragment>
113
- <span className="text-xs font-medium text-white/55">&bull;</span>
114
- <span
115
- className={
116
- amountText
117
- ? 'text-xs font-medium text-white/55'
118
- : 'text-xs font-medium text-white/30'
119
- }
120
- >
121
- {amountText || placeholderAmountText}
122
- </span>
123
- </React.Fragment>
124
- )
125
-
126
- const dismissButton = onDismiss ? (
127
- <button
128
- type="button"
129
- onClick={(e) => {
130
- // Stop the click from bubbling up into the outer preview-toggle
131
- // wrapper when both are wired up at the same time.
132
- e.stopPropagation()
133
- onDismiss()
134
- }}
135
- className="flex size-6 items-center justify-center rounded-full bg-[#121110] text-white"
136
- aria-label="Dismiss attachment"
137
- >
138
- <XIcon className="size-3" weight="bold" />
139
- </button>
140
- ) : undefined
141
-
142
- const editButton = onEditClick ? (
143
- <button
144
- type="button"
145
- onClick={onEditClick}
146
- aria-label="Edit attachment"
147
- className="flex size-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/15"
148
- >
149
- <PencilSimpleIcon className="size-5" weight="regular" />
150
- </button>
151
- ) : undefined
152
-
153
- return (
154
- <MediaShell variant="dark">
155
- <PreviewToggle
156
- onToggle={togglePreview}
157
- isBusy={isBusy}
158
- isPreviewVisible={!showLocked}
159
- >
160
- <CardThumbnail
161
- variant="dark"
162
- gallery={gallery}
163
- title={title}
164
- showLocked={showLocked}
165
- mimeType={mimeType}
166
- thumbnailUrl={thumbnailUrl}
167
- source={source}
168
- topRight={dismissButton}
169
- />
170
- </PreviewToggle>
171
-
172
- <CardBody
173
- variant="dark"
174
- title={title}
175
- placeholderTitle={placeholderTitle}
176
- mimeType={mimeType}
177
- detail={detail}
178
- statusBadge={statusBadge}
179
- icon={isGallery ? galleryBodyIcon('dark') : undefined}
180
- trailingAction={editButton}
181
- />
182
- </MediaShell>
183
- )
184
- }
185
-
186
- /**
187
- * The card the composer sees while drafting a paid attachment.
188
- * Matches the Composer column of the messaging design system in Figma.
189
- *
190
- * When `contentType === "text"`, renders as a normal outgoing chat bubble
191
- * showing the real draft text (it hasn't been sent/redacted yet) plus a
192
- * dismiss X and a price row, instead of a media thumbnail card.
193
- */
194
- const ComposerCard: React.FC<ComposerCardProps> = (props) =>
195
- props.contentType === 'text' ? (
196
- <ComposerTextCard {...props} />
197
- ) : (
198
- <ComposerMediaCard {...props} />
199
- )
200
-
201
- export default ComposerCard
@@ -1,235 +0,0 @@
1
- import type React from 'react'
2
-
3
- import { getSourceType } from '../../../AttachmentCard'
4
- import type {
5
- FetchSourceHandler,
6
- LockedAttachmentGalleryItem,
7
- PaymentStatus,
8
- } from '../../types'
9
- import CardBody from '../_shared/CardBody'
10
- import CardThumbnail, {
11
- galleryBodyIcon,
12
- isGalleryCard,
13
- } from '../_shared/CardThumbnail'
14
- import MediaShell from '../_shared/MediaShell'
15
- import PreviewToggle from '../_shared/PreviewToggle'
16
- import PurchaseStatusBadge from '../_shared/PurchaseStatusBadge'
17
- import { usePreviewToggle } from '../_shared/usePreviewToggle'
18
- import { useReceivedSource } from '../_shared/useReceivedSource'
19
-
20
- import ReceivedCardActions from './CardActions'
21
-
22
- interface MediaCardSharedProps {
23
- title?: string
24
- mimeType?: string
25
- thumbnailUrl?: string
26
- detail?: string
27
- amountText?: string
28
- paymentStatus?: PaymentStatus
29
- gallery?: LockedAttachmentGalleryItem[]
30
- onFetchSource?: FetchSourceHandler
31
- }
32
-
33
- export type MediaCardProps =
34
- | (MediaCardSharedProps & {
35
- isMine: true
36
- /** Dimmed fallback shown in the title slot when `title` is unset. */
37
- placeholderTitle?: string
38
- /** Fired the first time the sender taps the thumbnail to preview their own attachment. */
39
- onPreviewClick?: () => void
40
- })
41
- | (MediaCardSharedProps & {
42
- isMine: false
43
- /**
44
- * When true, shows a loading spinner on the Unlock button. Driven by
45
- * the LockedAttachmentContext (e.g. checkout in progress).
46
- */
47
- isUnlocking?: boolean
48
- /**
49
- * Called when the recipient clicks Unlock on an unpaid attachment.
50
- * Use this to open a checkout flow. Omit to hide the Unlock button.
51
- */
52
- onUnlockClick?: () => void
53
- /**
54
- * Called when the recipient clicks Download on an unlocked card.
55
- * Omit to hide the Download button.
56
- */
57
- onDownloadClick?: () => void
58
- })
59
-
60
- /**
61
- * The sender's ('mine') view of a paid media attachment: the locked
62
- * thumbnail (single or gallery) they can tap to preview their own
63
- * attachment, plus the title/purchase-status body.
64
- */
65
- const OwnMediaCard: React.FC<
66
- MediaCardSharedProps & {
67
- isMine: true
68
- placeholderTitle?: string
69
- onPreviewClick?: () => void
70
- }
71
- > = ({
72
- title,
73
- mimeType = 'application/octet-stream',
74
- thumbnailUrl,
75
- detail,
76
- amountText,
77
- placeholderTitle = 'Attachment title',
78
- paymentStatus = 'pending',
79
- gallery = [],
80
- onPreviewClick,
81
- onFetchSource,
82
- }) => {
83
- const isGallery = isGalleryCard(gallery)
84
-
85
- // Gallery is always previewable for the sender because each item's source
86
- // is already provided up-front via `gallery[*].sourceUrl`.
87
- const { source, isPreviewVisible, isLoadingPreview, togglePreview } =
88
- usePreviewToggle({ isGallery, onFetchSource, onPreviewClick })
89
-
90
- const showLocked = !isPreviewVisible
91
- const isBusy = isLoadingPreview
92
-
93
- return (
94
- <MediaShell variant="dark">
95
- <PreviewToggle
96
- onToggle={togglePreview}
97
- isBusy={isBusy}
98
- isPreviewVisible={!showLocked}
99
- >
100
- <CardThumbnail
101
- variant="dark"
102
- gallery={gallery}
103
- title={title}
104
- showLocked={showLocked}
105
- paymentStatus={paymentStatus}
106
- mimeType={mimeType}
107
- thumbnailUrl={thumbnailUrl}
108
- source={source}
109
- />
110
- </PreviewToggle>
111
-
112
- <CardBody
113
- variant="dark"
114
- title={title}
115
- placeholderTitle={placeholderTitle}
116
- mimeType={mimeType}
117
- detail={detail}
118
- statusBadge={
119
- <PurchaseStatusBadge
120
- isMine={true}
121
- paymentStatus={paymentStatus}
122
- amountText={amountText}
123
- />
124
- }
125
- icon={isGallery ? galleryBodyIcon('dark') : undefined}
126
- />
127
- </MediaShell>
128
- )
129
- }
130
-
131
- /**
132
- * The recipient's ('other') view of a paid media attachment: the locked
133
- * thumbnail (single or gallery) with an Unlock/Download CTA. The source is
134
- * revealed by fetching once — on click when already paid, or automatically
135
- * the first time a paid card scrolls into view.
136
- */
137
- const OtherMediaCard: React.FC<
138
- MediaCardSharedProps & {
139
- isMine: false
140
- onUnlockClick?: () => void
141
- onDownloadClick?: () => void
142
- isUnlocking?: boolean
143
- }
144
- > = ({
145
- title,
146
- amountText,
147
- thumbnailUrl,
148
- mimeType = 'application/octet-stream',
149
- detail,
150
- gallery = [],
151
- onUnlockClick,
152
- onFetchSource,
153
- onDownloadClick,
154
- paymentStatus,
155
- isUnlocking = false,
156
- }) => {
157
- const isGallery = isGalleryCard(gallery)
158
-
159
- const { source, cardRef, handleUnlockClick } = useReceivedSource({
160
- paymentStatus,
161
- onFetchSource,
162
- onUnlockClick,
163
- })
164
-
165
- const effectiveSourceUrl = source?.sourceUrl
166
- const effectiveRedeemUrl = source?.redeemUrl
167
-
168
- // For gallery, the per-item sources are carried by `gallery` itself, so
169
- // the lock state is driven by paymentStatus rather than the single
170
- // `effectiveSourceUrl`. Everything else still respects the source fetch.
171
- const isLocked = isGallery
172
- ? paymentStatus !== 'paid'
173
- : effectiveSourceUrl === undefined
174
- const sourceType = getSourceType(mimeType)
175
-
176
- // For gallery, ReceivedCardActions falls back to a plain <button>
177
- // (no anchor) because the gallery has no single sourceUrl to link to —
178
- // the per-item sources live on `gallery` itself.
179
- const actionSourceUrl = isGallery ? undefined : effectiveSourceUrl
180
-
181
- return (
182
- <MediaShell
183
- variant="light"
184
- rootRef={cardRef}
185
- data-testid="locked-attachment"
186
- >
187
- <CardThumbnail
188
- variant="light"
189
- gallery={gallery}
190
- title={title}
191
- showLocked={isLocked}
192
- paymentStatus={paymentStatus}
193
- mimeType={mimeType}
194
- thumbnailUrl={thumbnailUrl}
195
- source={source}
196
- containedImage={!isLocked && sourceType === 'document'}
197
- />
198
-
199
- <CardBody
200
- variant="light"
201
- title={title}
202
- mimeType={mimeType}
203
- detail={detail}
204
- statusBadge={
205
- <PurchaseStatusBadge
206
- isMine={false}
207
- paymentStatus={paymentStatus}
208
- amountText={amountText}
209
- />
210
- }
211
- icon={isGallery ? galleryBodyIcon('light') : undefined}
212
- action={
213
- <ReceivedCardActions
214
- isLocked={isLocked}
215
- isUnlocking={isUnlocking}
216
- sourceUrl={actionSourceUrl}
217
- redeemUrl={effectiveRedeemUrl}
218
- onUnlockClicked={handleUnlockClick}
219
- onDownloadClicked={onDownloadClick}
220
- />
221
- }
222
- />
223
- </MediaShell>
224
- )
225
- }
226
-
227
- /**
228
- * The card either party sees for a paid media attachment. `isMine` selects
229
- * the sender's preview view (tap-to-preview own draft) vs. the recipient's
230
- * unlock/download view.
231
- */
232
- const MediaCard: React.FC<MediaCardProps> = (props) =>
233
- props.isMine ? <OwnMediaCard {...props} /> : <OtherMediaCard {...props} />
234
-
235
- export default MediaCard
@@ -1,177 +0,0 @@
1
- import { LockSimpleIcon } from '@phosphor-icons/react'
2
- import type React from 'react'
3
-
4
- import type { PaymentStatus } from '../../types'
5
- import LoadingDots from '../_shared/LoadingDots'
6
- import PriceLockRow from '../_shared/PriceLockRow'
7
- import PurchaseStatusBadge from '../_shared/PurchaseStatusBadge'
8
- import SkeletonTextLines from '../_shared/SkeletonTextLines'
9
- import TextShell from '../_shared/TextShell'
10
-
11
- interface TextCardSharedProps {
12
- amountText?: string
13
- paymentStatus?: PaymentStatus
14
- /**
15
- * The message's real, revealed content — shown instead of the skeleton
16
- * loader once `paymentStatus` is `'paid'`. Ignored otherwise. Lets the
17
- * caller supply its own text renderer (e.g. Stream's `MessageText`)
18
- * without `LockedAttachment` depending on it.
19
- */
20
- children?: React.ReactNode
21
- }
22
-
23
- export type TextCardProps =
24
- | (TextCardSharedProps & {
25
- isMine: true
26
- })
27
- | (TextCardSharedProps & {
28
- isMine: false
29
- /**
30
- * Called when the recipient clicks Unlock on an unpaid attachment.
31
- * Use this to open a checkout flow. Omit to hide the Unlock button.
32
- */
33
- onUnlockClick?: () => void
34
- /**
35
- * When true, shows a loading spinner on the Unlock button. Driven by
36
- * the LockedAttachmentContext (e.g. checkout in progress).
37
- */
38
- isUnlocking?: boolean
39
- })
40
-
41
- /**
42
- * The sender's ('mine') view of a paid text-only message: an outgoing chat
43
- * bubble showing a skeleton text loader in place of the redacted content (or
44
- * `children`, once paid), plus a lock/price/status row.
45
- */
46
- const OwnTextCard: React.FC<TextCardSharedProps & { isMine: true }> = ({
47
- amountText,
48
- paymentStatus = 'pending',
49
- children,
50
- }) => {
51
- const isFailed = paymentStatus === 'failed'
52
- const isPaid = paymentStatus === 'paid'
53
-
54
- if (isPaid && children) {
55
- return (
56
- <TextShell isMine>
57
- {children}
58
- <span className="sr-only" aria-live="polite">
59
- Sold.
60
- </span>
61
- <PriceLockRow>
62
- {amountText != null && (
63
- <span className="text-white/55">{amountText}</span>
64
- )}
65
- <PurchaseStatusBadge isMine={true} paymentStatus={paymentStatus} />
66
- </PriceLockRow>
67
- </TextShell>
68
- )
69
- }
70
-
71
- const liveMessage = isFailed
72
- ? 'Payment failed.'
73
- : paymentStatus === 'refunded'
74
- ? 'Refunded.'
75
- : 'Payment pending.'
76
-
77
- return (
78
- <TextShell isMine>
79
- <SkeletonTextLines variant="dark" animate={paymentStatus === 'pending'} />
80
- <span className="sr-only" aria-live="polite">
81
- {liveMessage}
82
- </span>
83
- <PriceLockRow>
84
- {amountText != null && (
85
- <span className="text-white/55">{amountText}</span>
86
- )}
87
- <PurchaseStatusBadge isMine={true} paymentStatus={paymentStatus} />
88
- </PriceLockRow>
89
- </TextShell>
90
- )
91
- }
92
-
93
- /**
94
- * The recipient's ('other') view of a paid text-only message: a plain chat
95
- * bubble whose content is a skeleton text loader (or `children`, once paid)
96
- * plus a lock/price/status row. Two pre-purchase states exist — idle and
97
- * unlocking (checkout in flight) — plus the revealed state once
98
- * `paymentStatus` is `'paid'`.
99
- */
100
- const OtherTextCard: React.FC<
101
- TextCardSharedProps & {
102
- isMine: false
103
- onUnlockClick?: () => void
104
- isUnlocking?: boolean
105
- }
106
- > = ({
107
- amountText,
108
- paymentStatus,
109
- onUnlockClick,
110
- isUnlocking = false,
111
- children,
112
- }) => {
113
- const isPaid = paymentStatus === 'paid'
114
-
115
- if (isPaid && children) {
116
- return (
117
- <TextShell isMine={false}>
118
- {children}
119
- <PriceLockRow>
120
- {amountText != null && (
121
- <span className="opacity-55">{amountText}</span>
122
- )}
123
- <PurchaseStatusBadge isMine={false} paymentStatus="paid" />
124
- </PriceLockRow>
125
- </TextShell>
126
- )
127
- }
128
-
129
- const ariaLabel = isUnlocking
130
- ? 'Unlocking.'
131
- : 'Locked message available to unlock.'
132
-
133
- return (
134
- <TextShell isMine={false}>
135
- <div className="flex items-center gap-3">
136
- <div className="flex flex-1 flex-col gap-2">
137
- <SkeletonTextLines variant="light" animate={isUnlocking} />
138
- <span className="sr-only" aria-live="polite">
139
- {ariaLabel}
140
- </span>
141
- <PriceLockRow>
142
- {amountText != null && (
143
- <span className="opacity-55">{amountText}</span>
144
- )}
145
- </PriceLockRow>
146
- </div>
147
- <button
148
- type="button"
149
- onClick={onUnlockClick}
150
- disabled={isUnlocking}
151
- aria-label={isUnlocking ? 'Unlocking' : 'Unlock'}
152
- aria-busy={isUnlocking}
153
- title={isUnlocking ? 'Unlocking' : 'Unlock'}
154
- className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[#121110] text-white hover:bg-[#2a2928]"
155
- >
156
- {isUnlocking ? (
157
- <LoadingDots />
158
- ) : (
159
- <LockSimpleIcon className="size-4" weight="fill" aria-hidden />
160
- )}
161
- </button>
162
- </div>
163
- </TextShell>
164
- )
165
- }
166
-
167
- /**
168
- * The card either party sees for a paid text-only message — a chat bubble
169
- * showing a skeleton text loader (or `children`, once revealed) plus a
170
- * lock/price/status row. `isMine` selects the sender's payment-lifecycle view
171
- * (pending/failed/refunded/paid + Dismiss) vs. the recipient's unlock-flow
172
- * view (idle/unlocking + Unlock CTA).
173
- */
174
- const TextCard: React.FC<TextCardProps> = (props) =>
175
- props.isMine ? <OwnTextCard {...props} /> : <OtherTextCard {...props} />
176
-
177
- export default TextCard
@@ -1,19 +0,0 @@
1
- import classNames from 'classnames'
2
- import React from 'react'
3
-
4
- const TextShell: React.FC<
5
- React.PropsWithChildren<{ isMine: boolean; className?: string }>
6
- > = ({ isMine, children, className }) => (
7
- <div
8
- className={classNames(
9
- 'str-chat__message-bubble message-locked-attachment-text',
10
- 'px-4 py-3 flex flex-col gap-1',
11
- className
12
- )}
13
- style={{ backgroundColor: isMine ? '#1e2330' : '#f2f1ef' }}
14
- >
15
- {children}
16
- </div>
17
- )
18
-
19
- export default TextShell
@@ -1,52 +0,0 @@
1
- import React, { Suspense } from 'react'
2
-
3
- import type { ComposerCardProps } from './components/Composer/Card'
4
- import type { MediaCardProps } from './components/Media/Card'
5
- import type { TextCardProps } from './components/Text/Card'
6
-
7
- const ComposerCardLazy = React.lazy(() => import('./components/Composer/Card'))
8
- const TextCardLazy = React.lazy(() => import('./components/Text/Card'))
9
- const MediaCardLazy = React.lazy(() => import('./components/Media/Card'))
10
-
11
- const LockedAttachmentFallback = () => (
12
- <div
13
- className="w-[280px] min-h-[200px] animate-pulse rounded-md bg-black/[0.06] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.04),0_8px_32px_rgba(0,0,0,0.1)]"
14
- aria-hidden
15
- />
16
- )
17
-
18
- const Composer = (props: ComposerCardProps) => (
19
- <Suspense fallback={<LockedAttachmentFallback />}>
20
- <ComposerCardLazy {...props} />
21
- </Suspense>
22
- )
23
-
24
- const Text = (props: TextCardProps) => (
25
- <Suspense fallback={<LockedAttachmentFallback />}>
26
- <TextCardLazy {...props} />
27
- </Suspense>
28
- )
29
-
30
- const Media = (props: MediaCardProps) => (
31
- <Suspense fallback={<LockedAttachmentFallback />}>
32
- <MediaCardLazy {...props} />
33
- </Suspense>
34
- )
35
-
36
- const LockedAttachment = {
37
- Composer,
38
- Text,
39
- Media,
40
- }
41
-
42
- export default LockedAttachment
43
- export { default as PurchaseStatusBadge } from './components/_shared/PurchaseStatusBadge'
44
- export type { PurchaseStatusBadgeProps } from './components/_shared/PurchaseStatusBadge'
45
- export type { ComposerCardProps, TextCardProps, MediaCardProps }
46
- export type {
47
- PaymentStatus,
48
- FetchSourceHandler,
49
- LockedAttachmentSource,
50
- LockedAttachmentGalleryItem,
51
- LockedAttachmentContextValue,
52
- } from './types'