@linktr.ee/messaging-react 3.8.1 → 3.9.0-rc-1783406511
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Card-BG-Mg_ss.js +221 -0
- package/dist/Card-BG-Mg_ss.js.map +1 -0
- package/dist/Card-CmRzkuPw.cjs +2 -0
- package/dist/Card-CmRzkuPw.cjs.map +1 -0
- package/dist/Card-DSG-wR8t.js +96 -0
- package/dist/Card-DSG-wR8t.js.map +1 -0
- package/dist/Card-DVEuTlf_.cjs +2 -0
- package/dist/Card-DVEuTlf_.cjs.map +1 -0
- package/dist/Card-K9ky-G-t.js +108 -0
- package/dist/Card-K9ky-G-t.js.map +1 -0
- package/dist/Card-iKTcn1wg.cjs +2 -0
- package/dist/Card-iKTcn1wg.cjs.map +1 -0
- package/dist/PurchaseStatusBadge-CJH4S-8Z.js +36 -0
- package/dist/PurchaseStatusBadge-CJH4S-8Z.js.map +1 -0
- package/dist/PurchaseStatusBadge-UjDzZhLj.cjs +2 -0
- package/dist/PurchaseStatusBadge-UjDzZhLj.cjs.map +1 -0
- package/dist/TextShell-BiqF2UB0.js +32 -0
- package/dist/TextShell-BiqF2UB0.js.map +1 -0
- package/dist/TextShell-Bnzh0T75.cjs +2 -0
- package/dist/TextShell-Bnzh0T75.cjs.map +1 -0
- package/dist/assets/index.css +1 -1
- package/dist/{index-BCgRoF_u.js → index-DO_dYsIf.js} +1154 -1113
- package/dist/index-DO_dYsIf.js.map +1 -0
- package/dist/index-DpzmKL5U.cjs +2 -0
- package/dist/index-DpzmKL5U.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +98 -55
- package/dist/index.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/usePreviewToggle-B5kmPELX.cjs +2 -0
- package/dist/usePreviewToggle-B5kmPELX.cjs.map +1 -0
- package/dist/usePreviewToggle-D27Xw9Xx.js +323 -0
- package/dist/usePreviewToggle-D27Xw9Xx.js.map +1 -0
- package/package.json +2 -2
- package/src/components/CustomMessage/CustomMessage.stories.tsx +54 -1
- package/src/components/CustomMessage/MessageTag.tsx +8 -0
- package/src/components/CustomMessage/index.tsx +61 -22
- package/src/components/LockedAttachment/LockedAttachment.stories.tsx +410 -333
- package/src/components/LockedAttachment/LockedAttachment.test.tsx +104 -0
- package/src/components/LockedAttachment/components/Composer/Card.tsx +97 -120
- package/src/components/LockedAttachment/components/Media/Card.tsx +237 -0
- package/src/components/LockedAttachment/components/Text/Card.tsx +177 -0
- package/src/components/LockedAttachment/components/_shared/CardBody.tsx +3 -3
- package/src/components/LockedAttachment/components/_shared/CardThumbnail.tsx +92 -0
- package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +11 -33
- package/src/components/LockedAttachment/components/_shared/LoadingDots.tsx +36 -0
- package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +21 -11
- package/src/components/LockedAttachment/components/_shared/{LockedCardShell.tsx → MediaShell.tsx} +5 -5
- package/src/components/LockedAttachment/components/_shared/PreviewToggle.tsx +58 -0
- package/src/components/LockedAttachment/components/_shared/PriceLockRow.tsx +21 -0
- package/src/components/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +87 -0
- package/src/components/LockedAttachment/components/_shared/{LockedThumbnail.tsx → SingleThumbnail.tsx} +5 -5
- package/src/components/LockedAttachment/components/_shared/SkeletonTextLines.tsx +39 -0
- package/src/components/LockedAttachment/components/_shared/TextShell.tsx +19 -0
- package/src/components/LockedAttachment/components/_shared/usePreviewToggle.test.ts +67 -0
- package/src/components/LockedAttachment/components/_shared/usePreviewToggle.ts +89 -0
- package/src/components/LockedAttachment/components/_shared/useReceivedSource.ts +83 -0
- package/src/components/LockedAttachment/index.tsx +17 -28
- package/src/components/LockedAttachment/types.ts +28 -4
- package/src/index.ts +2 -5
- package/src/stream-custom-data.ts +1 -0
- package/src/styles.css +5 -0
- package/dist/Card-BRvcFjsq.js +0 -132
- package/dist/Card-BRvcFjsq.js.map +0 -1
- package/dist/Card-BuXkok0A.cjs +0 -2
- package/dist/Card-BuXkok0A.cjs.map +0 -1
- package/dist/Card-CvNHv3me.js +0 -107
- package/dist/Card-CvNHv3me.js.map +0 -1
- package/dist/Card-DOSuQdnk.cjs +0 -2
- package/dist/Card-DOSuQdnk.cjs.map +0 -1
- package/dist/Card-NnQw99XE.cjs +0 -2
- package/dist/Card-NnQw99XE.cjs.map +0 -1
- package/dist/Card-jGOcSN1O.js +0 -163
- package/dist/Card-jGOcSN1O.js.map +0 -1
- package/dist/LockedThumbnail-EsjaAqLR.js +0 -220
- package/dist/LockedThumbnail-EsjaAqLR.js.map +0 -1
- package/dist/LockedThumbnail-uAauUd5c.cjs +0 -2
- package/dist/LockedThumbnail-uAauUd5c.cjs.map +0 -1
- package/dist/index-BCgRoF_u.js.map +0 -1
- package/dist/index-DRurVQ0-.cjs +0 -2
- package/dist/index-DRurVQ0-.cjs.map +0 -1
- package/src/components/LockedAttachment/components/Composer/index.ts +0 -2
- package/src/components/LockedAttachment/components/Received/Card.tsx +0 -191
- package/src/components/LockedAttachment/components/Received/index.ts +0 -2
- package/src/components/LockedAttachment/components/Sent/Card.tsx +0 -177
- package/src/components/LockedAttachment/components/Sent/index.ts +0 -2
- /package/src/components/LockedAttachment/components/{Received → Media}/CardActions.tsx +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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
|
|
@@ -3,10 +3,10 @@ import React from 'react'
|
|
|
3
3
|
|
|
4
4
|
import { renderTypeIcon } from '../../../AttachmentCard'
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { CardVariant } from './MediaShell'
|
|
7
7
|
|
|
8
8
|
export interface CardBodyProps {
|
|
9
|
-
variant:
|
|
9
|
+
variant: CardVariant
|
|
10
10
|
title?: string
|
|
11
11
|
placeholderTitle?: string
|
|
12
12
|
mimeType: string
|
|
@@ -20,7 +20,7 @@ export interface CardBodyProps {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Title + status row layout shared by Composer
|
|
23
|
+
* Title + status row layout shared by Composer and `LockedAttachment.Media`.
|
|
24
24
|
* Layout matches the Figma `Container > Labels` group (16px h-padding, 12px v-padding,
|
|
25
25
|
* 4px gap between title and status row, 4px gap inside status row).
|
|
26
26
|
*/
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ImagesIcon } from '@phosphor-icons/react'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
LockedAttachmentGalleryItem,
|
|
6
|
+
LockedAttachmentSource,
|
|
7
|
+
PaymentStatus,
|
|
8
|
+
} from '../../types'
|
|
9
|
+
|
|
10
|
+
import GalleryThumbnail from './GalleryThumbnail'
|
|
11
|
+
import type { CardVariant } from './MediaShell'
|
|
12
|
+
import SingleThumbnail from './SingleThumbnail'
|
|
13
|
+
|
|
14
|
+
export interface CardThumbnailProps {
|
|
15
|
+
variant: CardVariant
|
|
16
|
+
gallery: LockedAttachmentGalleryItem[]
|
|
17
|
+
title?: string
|
|
18
|
+
showLocked: boolean
|
|
19
|
+
paymentStatus?: PaymentStatus
|
|
20
|
+
/** Single-asset props — ignored when the card renders as a gallery. */
|
|
21
|
+
mimeType: string
|
|
22
|
+
thumbnailUrl?: string
|
|
23
|
+
source?: LockedAttachmentSource
|
|
24
|
+
containedImage?: boolean
|
|
25
|
+
/** Top-right overlay (e.g. Composer dismiss X). Rendered inside the thumbnail. */
|
|
26
|
+
topRight?: React.ReactNode
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when the card should render as a mixed-media carousel (2+ items). */
|
|
30
|
+
export const isGalleryCard = (
|
|
31
|
+
gallery: LockedAttachmentGalleryItem[]
|
|
32
|
+
): boolean => gallery.length >= 2
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The body-row icon a card shows when it renders as a gallery — `CardBody`
|
|
36
|
+
* otherwise auto-detects the icon from `mimeType`. Returned as a ready node so
|
|
37
|
+
* every card shares the same size/tone treatment.
|
|
38
|
+
*/
|
|
39
|
+
export const galleryBodyIcon = (
|
|
40
|
+
variant: CardVariant
|
|
41
|
+
): React.ReactNode => (
|
|
42
|
+
<ImagesIcon
|
|
43
|
+
className={
|
|
44
|
+
variant === 'dark'
|
|
45
|
+
? 'size-5 shrink-0 text-white/55'
|
|
46
|
+
: 'size-5 shrink-0 text-black/55'
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Owns the single-vs-gallery thumbnail decision shared by Composer /
|
|
53
|
+
* `LockedAttachment.Media` cards: renders `GalleryThumbnail` for a 2+ item `gallery`,
|
|
54
|
+
* else `SingleThumbnail` for the single asset. Callers pass the superset of
|
|
55
|
+
* props; the unused half is ignored by whichever branch renders.
|
|
56
|
+
*/
|
|
57
|
+
const CardThumbnail: React.FC<CardThumbnailProps> = ({
|
|
58
|
+
variant,
|
|
59
|
+
gallery,
|
|
60
|
+
title,
|
|
61
|
+
showLocked,
|
|
62
|
+
paymentStatus,
|
|
63
|
+
mimeType,
|
|
64
|
+
thumbnailUrl,
|
|
65
|
+
source,
|
|
66
|
+
containedImage,
|
|
67
|
+
topRight,
|
|
68
|
+
}) =>
|
|
69
|
+
isGalleryCard(gallery) ? (
|
|
70
|
+
<GalleryThumbnail
|
|
71
|
+
variant={variant}
|
|
72
|
+
gallery={gallery}
|
|
73
|
+
title={title}
|
|
74
|
+
showLocked={showLocked}
|
|
75
|
+
paymentStatus={paymentStatus}
|
|
76
|
+
topRight={topRight}
|
|
77
|
+
/>
|
|
78
|
+
) : (
|
|
79
|
+
<SingleThumbnail
|
|
80
|
+
variant={variant}
|
|
81
|
+
mimeType={mimeType}
|
|
82
|
+
thumbnailUrl={thumbnailUrl}
|
|
83
|
+
title={title}
|
|
84
|
+
source={source}
|
|
85
|
+
showLocked={showLocked}
|
|
86
|
+
paymentStatus={paymentStatus}
|
|
87
|
+
topRight={topRight}
|
|
88
|
+
containedImage={containedImage}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
export default CardThumbnail
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CaretLeftIcon,
|
|
3
|
-
CaretRightIcon,
|
|
4
|
-
LockOpenIcon,
|
|
5
|
-
LockSimpleIcon,
|
|
6
|
-
PlayIcon,
|
|
7
|
-
} from '@phosphor-icons/react'
|
|
1
|
+
import { CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react'
|
|
8
2
|
import classNames from 'classnames'
|
|
9
3
|
import React, { useState } from 'react'
|
|
10
4
|
|
|
@@ -14,10 +8,11 @@ import type {
|
|
|
14
8
|
PaymentStatus,
|
|
15
9
|
} from '../../types'
|
|
16
10
|
|
|
17
|
-
import
|
|
11
|
+
import { LockBadge } from './LockBadge'
|
|
12
|
+
import type { CardVariant } from './MediaShell'
|
|
18
13
|
|
|
19
14
|
export interface GalleryThumbnailProps {
|
|
20
|
-
variant:
|
|
15
|
+
variant: CardVariant
|
|
21
16
|
gallery: LockedAttachmentGalleryItem[]
|
|
22
17
|
title?: string
|
|
23
18
|
/** When true, all items are shown blurred + the locked/play badge is overlaid. */
|
|
@@ -52,24 +47,8 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
52
47
|
const current = gallery[safeIndex]
|
|
53
48
|
const isVideoItem = isPlayableItem(current)
|
|
54
49
|
|
|
55
|
-
// Center badge: PLAY for video items, LOCK for non-video locked items.
|
|
56
|
-
// Unlocked image items show no center badge — the image is the content.
|
|
57
|
-
const centerBadge = (() => {
|
|
58
|
-
if (isVideoItem) {
|
|
59
|
-
return <PlayIcon className="size-6" weight="fill" />
|
|
60
|
-
}
|
|
61
|
-
if (showLocked) {
|
|
62
|
-
return paymentStatus === 'paid' ? (
|
|
63
|
-
<LockOpenIcon className="size-6" weight="fill" />
|
|
64
|
-
) : (
|
|
65
|
-
<LockSimpleIcon className="size-6" weight="fill" />
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
return null
|
|
69
|
-
})()
|
|
70
|
-
|
|
71
50
|
// Stop propagation so arrow taps don't bubble up to the outer preview-toggle
|
|
72
|
-
// wrapper on ComposerCard /
|
|
51
|
+
// wrapper on ComposerCard / OwnMediaCard.
|
|
73
52
|
const goPrev = (e: React.MouseEvent) => {
|
|
74
53
|
e.stopPropagation()
|
|
75
54
|
setIndex((i) => Math.max(0, i - 1))
|
|
@@ -91,7 +70,7 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
91
70
|
//
|
|
92
71
|
// The locked branch uses `filter: blur` on a single full-size `<img>`
|
|
93
72
|
// — not `backdrop-filter` — so the rounded corners are clipped
|
|
94
|
-
// normally without any clip-path workarounds (see
|
|
73
|
+
// normally without any clip-path workarounds (see SingleThumbnail).
|
|
95
74
|
return (
|
|
96
75
|
<div className="relative aspect-[337/386] overflow-hidden rounded-t-md">
|
|
97
76
|
{showLocked ? (
|
|
@@ -123,12 +102,11 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
123
102
|
/>
|
|
124
103
|
)}
|
|
125
104
|
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</div>
|
|
105
|
+
{/* PLAY for video items (locked or not — video needs a play affordance
|
|
106
|
+
either way); LOCK for non-video locked items. Unlocked non-video
|
|
107
|
+
items show no badge — the image is the content. */}
|
|
108
|
+
{(isVideoItem || showLocked) && (
|
|
109
|
+
<LockBadge paymentStatus={paymentStatus} isPlayable={isVideoItem} />
|
|
132
110
|
)}
|
|
133
111
|
|
|
134
112
|
{total > 1 && (
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const Dot = ({ cx, index }: { cx: string; index: number }) => (
|
|
4
|
+
<circle cx={cx} cy="6.15" r="2.4" fill="currentColor">
|
|
5
|
+
<animateTransform
|
|
6
|
+
attributeName="transform"
|
|
7
|
+
type="translate"
|
|
8
|
+
values="0 0; 0 -2.25; 0 0;"
|
|
9
|
+
dur="900ms"
|
|
10
|
+
begin={`${120 * index}ms`}
|
|
11
|
+
repeatCount="indefinite"
|
|
12
|
+
/>
|
|
13
|
+
</circle>
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Three bouncing dots, inline-sized for use inside buttons/labels (e.g. the
|
|
18
|
+
* Unlock CTA while a purchase is in flight). Mirrors the typing indicator's
|
|
19
|
+
* animation but uses `currentColor` so it adopts the surrounding text color.
|
|
20
|
+
*/
|
|
21
|
+
const LoadingDots: React.FC<{ className?: string }> = ({ className }) => (
|
|
22
|
+
<svg
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
className={className}
|
|
25
|
+
viewBox="0 0 20 12.3"
|
|
26
|
+
width="20"
|
|
27
|
+
height="13"
|
|
28
|
+
overflow="visible"
|
|
29
|
+
>
|
|
30
|
+
<Dot cx="3" index={0} />
|
|
31
|
+
<Dot cx="10" index={1} />
|
|
32
|
+
<Dot cx="17" index={2} />
|
|
33
|
+
</svg>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export default LoadingDots
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LockOpenIcon,
|
|
3
|
-
LockSimpleIcon,
|
|
4
|
-
PlayIcon,
|
|
5
|
-
} from '@phosphor-icons/react'
|
|
1
|
+
import { LockOpenIcon, LockSimpleIcon, PlayIcon } from '@phosphor-icons/react'
|
|
6
2
|
import React from 'react'
|
|
7
3
|
|
|
8
4
|
import { getSourceType } from '../../../AttachmentCard'
|
|
9
5
|
import type { PaymentStatus } from '../../types'
|
|
10
6
|
|
|
7
|
+
export interface LockBadgeProps {
|
|
8
|
+
paymentStatus?: PaymentStatus
|
|
9
|
+
mimeType?: string
|
|
10
|
+
/**
|
|
11
|
+
* Overrides the `mimeType`-derived playable check. Lets a caller that
|
|
12
|
+
* already knows per-item playability (e.g. `GalleryThumbnail`, checking a
|
|
13
|
+
* carousel item's own mimeType rather than the card's single `mimeType`)
|
|
14
|
+
* skip re-deriving it.
|
|
15
|
+
*/
|
|
16
|
+
isPlayable?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* Centered 48x48 lock badge that overlays the thumbnail of any locked attachment.
|
|
13
21
|
* Uses `bg-black/25` from the design system's `alpha-black-25` token.
|
|
@@ -15,14 +23,16 @@ import type { PaymentStatus } from '../../types'
|
|
|
15
23
|
* For playable media (video/audio), the badge shows a play icon instead of a
|
|
16
24
|
* lock — matching the convention used by `GalleryThumbnail`.
|
|
17
25
|
*/
|
|
18
|
-
export const LockBadge: React.FC<{
|
|
19
|
-
paymentStatus
|
|
20
|
-
mimeType
|
|
21
|
-
|
|
26
|
+
export const LockBadge: React.FC<LockBadgeProps> = ({
|
|
27
|
+
paymentStatus,
|
|
28
|
+
mimeType,
|
|
29
|
+
isPlayable,
|
|
30
|
+
}) => {
|
|
22
31
|
const sourceType = mimeType ? getSourceType(mimeType) : undefined
|
|
23
|
-
const
|
|
32
|
+
const resolvedIsPlayable =
|
|
33
|
+
isPlayable ?? (sourceType === 'video' || sourceType === 'audio')
|
|
24
34
|
|
|
25
|
-
const Icon =
|
|
35
|
+
const Icon = resolvedIsPlayable
|
|
26
36
|
? PlayIcon
|
|
27
37
|
: paymentStatus === 'paid'
|
|
28
38
|
? LockOpenIcon
|
package/src/components/LockedAttachment/components/_shared/{LockedCardShell.tsx → MediaShell.tsx}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type CardVariant = 'dark' | 'light'
|
|
5
5
|
|
|
6
|
-
export interface
|
|
7
|
-
variant:
|
|
6
|
+
export interface MediaShellProps {
|
|
7
|
+
variant: CardVariant
|
|
8
8
|
children: React.ReactNode
|
|
9
9
|
rootRef?: React.Ref<HTMLDivElement>
|
|
10
10
|
'data-testid'?: string
|
|
@@ -14,7 +14,7 @@ export interface LockedCardShellProps {
|
|
|
14
14
|
* Outer chrome for every LockedAttachment card.
|
|
15
15
|
* Mirrors the `shadow-400` + 16px radius treatment from the Figma design system.
|
|
16
16
|
*/
|
|
17
|
-
const
|
|
17
|
+
const MediaShell: React.FC<MediaShellProps> = ({
|
|
18
18
|
variant,
|
|
19
19
|
children,
|
|
20
20
|
rootRef,
|
|
@@ -33,4 +33,4 @@ const LockedCardShell: React.FC<LockedCardShellProps> = ({
|
|
|
33
33
|
</div>
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
export default
|
|
36
|
+
export default MediaShell
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface PreviewToggleProps {
|
|
4
|
+
/** Toggles the preview; omit to render `children` with no wrapper at all (nothing to toggle). */
|
|
5
|
+
onToggle: (() => void | Promise<void>) | undefined
|
|
6
|
+
isBusy: boolean
|
|
7
|
+
isPreviewVisible: boolean
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Makes a locked thumbnail clickable to toggle its preview. Uses a
|
|
13
|
+
* `role="button"` div rather than a native `<button>` because the thumbnail
|
|
14
|
+
* it wraps can itself contain interactive children (gallery carousel arrows,
|
|
15
|
+
* a Composer dismiss X) — nesting a real `<button>` around those would
|
|
16
|
+
* produce invalid, inaccessible nested-interactive-element markup.
|
|
17
|
+
*/
|
|
18
|
+
const PreviewToggle: React.FC<PreviewToggleProps> = ({
|
|
19
|
+
onToggle,
|
|
20
|
+
isBusy,
|
|
21
|
+
isPreviewVisible,
|
|
22
|
+
children,
|
|
23
|
+
}) => {
|
|
24
|
+
if (!onToggle) return <React.Fragment>{children}</React.Fragment>
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
role="button"
|
|
29
|
+
tabIndex={isBusy ? -1 : 0}
|
|
30
|
+
aria-label="Toggle preview"
|
|
31
|
+
aria-busy={isBusy}
|
|
32
|
+
aria-pressed={isPreviewVisible}
|
|
33
|
+
aria-disabled={isBusy || undefined}
|
|
34
|
+
onClick={isBusy ? undefined : onToggle}
|
|
35
|
+
onKeyDown={(e) => {
|
|
36
|
+
if (isBusy) return
|
|
37
|
+
// Only handle keys that originate on the wrapper itself, not on
|
|
38
|
+
// inner interactive elements (dismiss X, carousel arrows). Without
|
|
39
|
+
// this guard, pressing Enter/Space on a focused child would bubble
|
|
40
|
+
// up and toggle the preview in addition to activating the child.
|
|
41
|
+
if (e.target !== e.currentTarget) return
|
|
42
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
43
|
+
e.preventDefault()
|
|
44
|
+
void onToggle()
|
|
45
|
+
}
|
|
46
|
+
}}
|
|
47
|
+
className={
|
|
48
|
+
!isBusy
|
|
49
|
+
? 'block w-full cursor-pointer text-left'
|
|
50
|
+
: 'block w-full text-left'
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default PreviewToggle
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LockSimpleIcon } from '@phosphor-icons/react'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The lock-icon + amount/status row shared by every LockedAttachment text-mode
|
|
6
|
+
* card. Only the wrapper and the leading lock icon are shared — the amount and
|
|
7
|
+
* any trailing status live in `children`, since they genuinely differ per card
|
|
8
|
+
* (placeholder fallback vs. paid badge vs. bare amount).
|
|
9
|
+
*/
|
|
10
|
+
const PriceLockRow: React.FC<React.PropsWithChildren> = ({ children }) => (
|
|
11
|
+
<div className="flex items-center gap-1 text-xs font-medium">
|
|
12
|
+
<LockSimpleIcon
|
|
13
|
+
className="size-3.5 shrink-0 opacity-55"
|
|
14
|
+
weight="fill"
|
|
15
|
+
aria-hidden
|
|
16
|
+
/>
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export default PriceLockRow
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { CheckCircleIcon } from '@phosphor-icons/react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import type { PaymentStatus } from '../../types'
|
|
6
|
+
|
|
7
|
+
export interface PurchaseStatusBadgeProps {
|
|
8
|
+
/** 'own' matches the sender's dark/own-message context; 'other' matches the recipient's light/other-message context. */
|
|
9
|
+
isMine: boolean
|
|
10
|
+
/** 'paid' renders the "Sold"/"Purchased" + checkmark variant; otherwise falls back to plain amount text, or nothing. */
|
|
11
|
+
paymentStatus?: PaymentStatus
|
|
12
|
+
amountText?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bullet + amount/label + checkmark row for a paid attachment or message.
|
|
17
|
+
* Shared by `LockedAttachment.Media`/`.Text`'s status badges.
|
|
18
|
+
*/
|
|
19
|
+
const PurchaseStatusBadge: React.FC<PurchaseStatusBadgeProps> = ({
|
|
20
|
+
isMine,
|
|
21
|
+
paymentStatus,
|
|
22
|
+
amountText,
|
|
23
|
+
}) => {
|
|
24
|
+
const labelColour = isMine ? 'text-white/55' : 'text-black/55'
|
|
25
|
+
const successColour = isMine ? 'text-[#34c759]' : 'text-[#008236]'
|
|
26
|
+
|
|
27
|
+
if (paymentStatus === 'paid') {
|
|
28
|
+
return (
|
|
29
|
+
<React.Fragment>
|
|
30
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
31
|
+
•
|
|
32
|
+
</span>
|
|
33
|
+
<span className={classNames('text-xs font-medium', successColour)}>
|
|
34
|
+
{isMine ? 'Sold' : 'Purchased'}
|
|
35
|
+
</span>
|
|
36
|
+
<CheckCircleIcon
|
|
37
|
+
className={classNames('shrink-0 size-3.5', successColour)}
|
|
38
|
+
weight="bold"
|
|
39
|
+
aria-hidden
|
|
40
|
+
/>
|
|
41
|
+
</React.Fragment>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (amountText != null) {
|
|
46
|
+
return (
|
|
47
|
+
<React.Fragment>
|
|
48
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
49
|
+
•
|
|
50
|
+
</span>
|
|
51
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
52
|
+
{amountText}
|
|
53
|
+
</span>
|
|
54
|
+
</React.Fragment>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (paymentStatus === 'pending') {
|
|
59
|
+
return (
|
|
60
|
+
<React.Fragment>
|
|
61
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
62
|
+
•
|
|
63
|
+
</span>
|
|
64
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
65
|
+
Sent
|
|
66
|
+
</span>
|
|
67
|
+
</React.Fragment>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (paymentStatus === 'failed' || paymentStatus === 'refunded') {
|
|
72
|
+
return (
|
|
73
|
+
<React.Fragment>
|
|
74
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
75
|
+
•
|
|
76
|
+
</span>
|
|
77
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
78
|
+
{paymentStatus === 'failed' ? 'Payment failed' : 'Refunded'}
|
|
79
|
+
</span>
|
|
80
|
+
</React.Fragment>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default PurchaseStatusBadge
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
import type { LockedAttachmentSource, PaymentStatus } from '../../types'
|
|
9
9
|
|
|
10
10
|
import { LockBadge } from './LockBadge'
|
|
11
|
-
import type {
|
|
11
|
+
import type { CardVariant } from './MediaShell'
|
|
12
12
|
|
|
13
|
-
export interface
|
|
14
|
-
variant:
|
|
13
|
+
export interface SingleThumbnailProps {
|
|
14
|
+
variant: CardVariant
|
|
15
15
|
mimeType: string
|
|
16
16
|
thumbnailUrl?: string
|
|
17
17
|
title?: string
|
|
@@ -40,7 +40,7 @@ export interface LockedThumbnailProps {
|
|
|
40
40
|
* - preview/paid: clear thumbnail (the underlying source)
|
|
41
41
|
* - empty: type-icon placeholder
|
|
42
42
|
*/
|
|
43
|
-
const
|
|
43
|
+
const SingleThumbnail: React.FC<SingleThumbnailProps> = ({
|
|
44
44
|
variant,
|
|
45
45
|
mimeType,
|
|
46
46
|
thumbnailUrl,
|
|
@@ -125,4 +125,4 @@ const LockedThumbnail: React.FC<LockedThumbnailProps> = ({
|
|
|
125
125
|
)
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export default
|
|
128
|
+
export default SingleThumbnail
|