@linktr.ee/messaging-react 3.10.0 → 3.11.0-rc-1783661028
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-415OfRhw.cjs +2 -0
- package/dist/Card-415OfRhw.cjs.map +1 -0
- package/dist/Card-CyWdUMCB.cjs +2 -0
- package/dist/Card-CyWdUMCB.cjs.map +1 -0
- package/dist/Card-V5d6qhDT.js +694 -0
- package/dist/Card-V5d6qhDT.js.map +1 -0
- package/dist/Card-qS17qnN0.js +103 -0
- package/dist/Card-qS17qnN0.js.map +1 -0
- package/dist/LoadingDots-Ct3WmVlT.js +59 -0
- package/dist/LoadingDots-Ct3WmVlT.js.map +1 -0
- package/dist/LoadingDots-D9GGphgM.cjs +2 -0
- package/dist/LoadingDots-D9GGphgM.cjs.map +1 -0
- package/dist/assets/index.css +1 -1
- package/dist/index-Bx_zjNMR.cjs +2 -0
- package/dist/index-Bx_zjNMR.cjs.map +1 -0
- package/dist/{index-BxzzWK4K.js → index-DFZrAiLB.js} +1276 -1249
- package/dist/index-DFZrAiLB.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +164 -101
- package/dist/index.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/package.json +2 -2
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +95 -0
- package/src/components/ChannelList/CustomChannelPreview.tsx +21 -9
- package/src/components/CustomMessage/CustomMessage.stories.tsx +54 -1
- package/src/components/CustomMessage/LockedAttachment/LockedAttachment.stories.tsx +541 -0
- package/src/components/CustomMessage/LockedAttachment/LockedAttachment.test.tsx +117 -0
- package/src/components/CustomMessage/LockedAttachment/components/Media/Card.tsx +358 -0
- package/src/components/{LockedAttachment/components/Received → CustomMessage/LockedAttachment/components/Media}/CardActions.tsx +9 -2
- package/src/components/CustomMessage/LockedAttachment/components/Media/types.ts +79 -0
- package/src/components/CustomMessage/LockedAttachment/components/Text/Card.tsx +153 -0
- package/src/components/CustomMessage/LockedAttachment/components/Text/types.ts +58 -0
- package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/CardBody.tsx +8 -7
- package/src/components/CustomMessage/LockedAttachment/components/_shared/CardThumbnail.tsx +92 -0
- package/src/components/{LockedAttachment → CustomMessage/LockedAttachment}/components/_shared/GalleryThumbnail.tsx +31 -39
- package/src/components/CustomMessage/LockedAttachment/components/_shared/LoadingDots.tsx +36 -0
- package/src/components/CustomMessage/LockedAttachment/components/_shared/LockBadge.tsx +63 -0
- package/src/components/{LockedAttachment/components/_shared/LockedCardShell.tsx → CustomMessage/LockedAttachment/components/_shared/MediaShell.tsx} +8 -8
- package/src/components/CustomMessage/LockedAttachment/components/_shared/PreviewToggle.tsx +58 -0
- package/src/components/CustomMessage/LockedAttachment/components/_shared/PriceLockRow.tsx +21 -0
- package/src/components/CustomMessage/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +74 -0
- package/src/components/{LockedAttachment/components/_shared/LockedThumbnail.tsx → CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx} +11 -7
- package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx +29 -0
- package/src/components/CustomMessage/LockedAttachment/components/_shared/TextShell.tsx +28 -0
- package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.test.ts +67 -0
- package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.ts +89 -0
- package/src/components/CustomMessage/LockedAttachment/components/_shared/useReceivedSource.ts +83 -0
- package/src/components/CustomMessage/LockedAttachment/index.tsx +105 -0
- package/src/components/CustomMessage/LockedAttachment/types.ts +44 -0
- package/src/components/CustomMessage/MessageTag.tsx +8 -0
- package/src/components/CustomMessage/context.tsx +1 -1
- package/src/components/CustomMessage/index.tsx +76 -42
- package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +5 -3
- package/src/index.ts +3 -8
- package/src/stream-custom-data.ts +1 -0
- package/src/styles.css +12 -0
- package/src/types.ts +1 -1
- package/dist/Card-B-L2MST4.js +0 -107
- package/dist/Card-B-L2MST4.js.map +0 -1
- package/dist/Card-B56QxJW3.cjs +0 -2
- package/dist/Card-B56QxJW3.cjs.map +0 -1
- package/dist/Card-Bqh-BQaX.js +0 -132
- package/dist/Card-Bqh-BQaX.js.map +0 -1
- package/dist/Card-ZCE6Lhfc.cjs +0 -2
- package/dist/Card-ZCE6Lhfc.cjs.map +0 -1
- package/dist/Card-g4JD-2ES.js +0 -163
- package/dist/Card-g4JD-2ES.js.map +0 -1
- package/dist/Card-wac6AMNc.cjs +0 -2
- package/dist/Card-wac6AMNc.cjs.map +0 -1
- package/dist/LockedThumbnail-BAQmoIff.js +0 -220
- package/dist/LockedThumbnail-BAQmoIff.js.map +0 -1
- package/dist/LockedThumbnail-Cr0Gqs3Y.cjs +0 -2
- package/dist/LockedThumbnail-Cr0Gqs3Y.cjs.map +0 -1
- package/dist/index-BPhZyaxT.cjs +0 -2
- package/dist/index-BPhZyaxT.cjs.map +0 -1
- package/dist/index-BxzzWK4K.js.map +0 -1
- package/src/components/LockedAttachment/LockedAttachment.stories.tsx +0 -450
- package/src/components/LockedAttachment/components/Composer/Card.tsx +0 -221
- 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/_shared/LockBadge.tsx +0 -39
- package/src/components/LockedAttachment/index.tsx +0 -63
- package/src/components/LockedAttachment/types.ts +0 -51
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
-
import { renderTypeIcon } from '
|
|
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
|
*/
|
|
@@ -36,8 +36,8 @@ const CardBody: React.FC<CardBodyProps> = ({
|
|
|
36
36
|
trailingAction,
|
|
37
37
|
}) => {
|
|
38
38
|
const isDark = variant === 'dark'
|
|
39
|
-
const displayTitle =
|
|
40
|
-
const titleDimmed =
|
|
39
|
+
const displayTitle = title || placeholderTitle
|
|
40
|
+
const titleDimmed = !title
|
|
41
41
|
|
|
42
42
|
const typeIcon =
|
|
43
43
|
icon ??
|
|
@@ -56,7 +56,8 @@ const CardBody: React.FC<CardBodyProps> = ({
|
|
|
56
56
|
{displayTitle.trim() !== '' && (
|
|
57
57
|
<p
|
|
58
58
|
className={classNames('truncate text-base font-medium leading-6', {
|
|
59
|
-
'text-black/90': !isDark,
|
|
59
|
+
'text-black/90': !isDark && !titleDimmed,
|
|
60
|
+
'text-black/30': !isDark && titleDimmed,
|
|
60
61
|
'text-white/30': isDark && titleDimmed,
|
|
61
62
|
'text-white': isDark && !titleDimmed,
|
|
62
63
|
})}
|
|
@@ -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,23 +1,18 @@
|
|
|
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
|
|
|
11
|
-
import { AttachmentThumbnail, getSourceType } from '
|
|
5
|
+
import { AttachmentThumbnail, getSourceType } from '../../../../AttachmentCard'
|
|
12
6
|
import type {
|
|
13
7
|
LockedAttachmentGalleryItem,
|
|
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
|
|
51
|
+
// wrapper on PreviewMediaCard / OwnMediaCard.
|
|
73
52
|
const goPrev = (e: React.MouseEvent) => {
|
|
74
53
|
e.stopPropagation()
|
|
75
54
|
setIndex((i) => Math.max(0, i - 1))
|
|
@@ -86,14 +65,22 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
86
65
|
// Locked + unlocked render entirely separate trees so the locked state
|
|
87
66
|
// doesn't end up stacking a blurred `<img>` on top of an underlying
|
|
88
67
|
// `AttachmentThumbnail` (whose `bg-white/10` poster shell otherwise
|
|
89
|
-
// shows through as a visible horizontal seam
|
|
90
|
-
//
|
|
68
|
+
// shows through as a visible horizontal seam).
|
|
69
|
+
//
|
|
70
|
+
// The locked placeholder is absolutely positioned, so it needs the fixed
|
|
71
|
+
// 337/386 box for height; unlocked content (video, natural-sized image)
|
|
72
|
+
// manages its own height and can expand/shrink per item.
|
|
91
73
|
//
|
|
92
74
|
// The locked branch uses `filter: blur` on a single full-size `<img>`
|
|
93
75
|
// — not `backdrop-filter` — so the rounded corners are clipped
|
|
94
|
-
// normally without any clip-path workarounds (see
|
|
76
|
+
// normally without any clip-path workarounds (see SingleThumbnail).
|
|
95
77
|
return (
|
|
96
|
-
<div
|
|
78
|
+
<div
|
|
79
|
+
className={classNames(
|
|
80
|
+
'group relative overflow-hidden rounded-t-md',
|
|
81
|
+
showLocked ? 'aspect-[337/386]' : undefined
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
97
84
|
{showLocked ? (
|
|
98
85
|
<div
|
|
99
86
|
className={classNames(
|
|
@@ -115,20 +102,25 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
115
102
|
</div>
|
|
116
103
|
) : (
|
|
117
104
|
<AttachmentThumbnail
|
|
105
|
+
key={safeIndex}
|
|
118
106
|
mimeType={current.mimeType}
|
|
119
107
|
sourceUrl={current.sourceUrl}
|
|
120
108
|
thumbnailUrl={current.thumbnailUrl}
|
|
121
109
|
title={title}
|
|
122
110
|
variant={variant}
|
|
111
|
+
mediaPlayerProps={{ autoPlay: true }}
|
|
123
112
|
/>
|
|
124
113
|
)}
|
|
125
114
|
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
115
|
+
{/* LOCK while locked. Once unlocked, video/audio autoplays via
|
|
116
|
+
`mediaPlayerProps` and MediaPlayer already has its own inline
|
|
117
|
+
controls, so no separate Play affordance is shown. */}
|
|
118
|
+
{showLocked && (
|
|
119
|
+
<LockBadge
|
|
120
|
+
paymentStatus={paymentStatus}
|
|
121
|
+
isPlayable={isVideoItem}
|
|
122
|
+
isLocked={showLocked}
|
|
123
|
+
/>
|
|
132
124
|
)}
|
|
133
125
|
|
|
134
126
|
{total > 1 && (
|
|
@@ -158,8 +150,8 @@ const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
|
158
150
|
<CaretRightIcon className="size-5" weight="bold" />
|
|
159
151
|
</button>
|
|
160
152
|
|
|
161
|
-
<div className="pointer-events-none absolute bottom-
|
|
162
|
-
<span className="text-xs font-medium leading-
|
|
153
|
+
<div className="pointer-events-none absolute bottom-10 left-1/2 flex size-8 -translate-x-1/2 items-center justify-center rounded-full bg-black/30">
|
|
154
|
+
<span className="text-xs font-medium leading-none text-white">
|
|
163
155
|
{safeIndex + 1}/{total}
|
|
164
156
|
</span>
|
|
165
157
|
</div>
|
|
@@ -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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { LockOpenIcon, LockSimpleIcon, PlayIcon } from '@phosphor-icons/react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import { getSourceType } from '../../../../AttachmentCard'
|
|
6
|
+
import type { PaymentStatus } from '../../types'
|
|
7
|
+
|
|
8
|
+
export interface LockBadgeProps {
|
|
9
|
+
paymentStatus?: PaymentStatus
|
|
10
|
+
mimeType?: string
|
|
11
|
+
/**
|
|
12
|
+
* Overrides the `mimeType`-derived playable check. Lets a caller that
|
|
13
|
+
* already knows per-item playability (e.g. `GalleryThumbnail`, checking a
|
|
14
|
+
* carousel item's own mimeType rather than the card's single `mimeType`)
|
|
15
|
+
* skip re-deriving it.
|
|
16
|
+
*/
|
|
17
|
+
isPlayable?: boolean
|
|
18
|
+
/** Whether the item is still locked. Play only ever shows once unlocked —
|
|
19
|
+
* a locked item always shows the lock, even for otherwise-playable media. */
|
|
20
|
+
isLocked: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Centered 48x48 lock badge that overlays the thumbnail of any locked attachment.
|
|
25
|
+
* Uses `bg-black/30`. Stays `pointer-events-none` so clicks pass through to
|
|
26
|
+
* the underlying player/toggle; the play state highlights via `group-hover`
|
|
27
|
+
* (same `bg-black/40` as the gallery's prev/next arrows) — the ancestor
|
|
28
|
+
* providing `group` must wrap the actual clickable/hoverable player area.
|
|
29
|
+
*
|
|
30
|
+
* For playable media (video/audio) that's unlocked, the badge shows a play
|
|
31
|
+
* icon instead of a lock — matching the convention used by `GalleryThumbnail`.
|
|
32
|
+
*/
|
|
33
|
+
export const LockBadge: React.FC<LockBadgeProps> = ({
|
|
34
|
+
paymentStatus,
|
|
35
|
+
mimeType,
|
|
36
|
+
isPlayable,
|
|
37
|
+
isLocked,
|
|
38
|
+
}) => {
|
|
39
|
+
const sourceType = mimeType ? getSourceType(mimeType) : undefined
|
|
40
|
+
const resolvedIsPlayable =
|
|
41
|
+
isPlayable ?? (sourceType === 'video' || sourceType === 'audio')
|
|
42
|
+
|
|
43
|
+
const Icon =
|
|
44
|
+
resolvedIsPlayable && !isLocked
|
|
45
|
+
? PlayIcon
|
|
46
|
+
: paymentStatus === 'paid'
|
|
47
|
+
? LockOpenIcon
|
|
48
|
+
: LockSimpleIcon
|
|
49
|
+
return (
|
|
50
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
51
|
+
<div
|
|
52
|
+
className={classNames(
|
|
53
|
+
'flex size-12 items-center justify-center rounded-full bg-black/30 text-white',
|
|
54
|
+
Icon === PlayIcon && 'group-hover:bg-black/40'
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<Icon className="size-6" weight="fill" />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default LockBadge
|
|
@@ -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
|
|
@@ -12,9 +12,9 @@ export interface LockedCardShellProps {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Outer chrome for every LockedAttachment card.
|
|
15
|
-
* Mirrors the
|
|
15
|
+
* Mirrors the 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,
|
|
@@ -25,12 +25,12 @@ const LockedCardShell: React.FC<LockedCardShellProps> = ({
|
|
|
25
25
|
data-testid={dataTestId}
|
|
26
26
|
className={classNames(
|
|
27
27
|
'relative w-[280px] select-none overflow-hidden rounded-md',
|
|
28
|
-
'
|
|
29
|
-
|
|
28
|
+
variant === 'dark' ? 'bg-[#1e2330]' : 'bg-white',
|
|
29
|
+
'shadow-[0_1px_2px_rgba(0,0,0,0.08)]'
|
|
30
30
|
)}
|
|
31
31
|
>
|
|
32
32
|
{children}
|
|
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
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
if (paymentStatus === 'paid') {
|
|
27
|
+
return (
|
|
28
|
+
<React.Fragment>
|
|
29
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
30
|
+
•
|
|
31
|
+
</span>
|
|
32
|
+
<span className={classNames('text-xs font-medium', successColour)}>
|
|
33
|
+
{isMine ? 'Sold' : 'Purchased'}
|
|
34
|
+
</span>
|
|
35
|
+
<CheckCircleIcon
|
|
36
|
+
className={classNames('shrink-0 size-3.5', successColour)}
|
|
37
|
+
weight="bold"
|
|
38
|
+
aria-hidden
|
|
39
|
+
/>
|
|
40
|
+
</React.Fragment>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (amountText != null) {
|
|
45
|
+
return (
|
|
46
|
+
<React.Fragment>
|
|
47
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
48
|
+
•
|
|
49
|
+
</span>
|
|
50
|
+
<span className={classNames('text-xs font-medium', labelColour)}>
|
|
51
|
+
{amountText}
|
|
52
|
+
</span>
|
|
53
|
+
</React.Fragment>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if (paymentStatus === 'failed' || paymentStatus === 'refunded') {
|
|
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
|
+
{paymentStatus === 'failed' ? 'Payment failed' : 'Refunded'}
|
|
66
|
+
</span>
|
|
67
|
+
</React.Fragment>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default PurchaseStatusBadge
|
|
@@ -4,14 +4,14 @@ import React from 'react'
|
|
|
4
4
|
import {
|
|
5
5
|
AttachmentThumbnail,
|
|
6
6
|
renderTypeIcon,
|
|
7
|
-
} from '
|
|
7
|
+
} from '../../../../AttachmentCard'
|
|
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,
|
|
@@ -98,7 +98,11 @@ const LockedThumbnail: React.FC<LockedThumbnailProps> = ({
|
|
|
98
98
|
})}
|
|
99
99
|
</div>
|
|
100
100
|
)}
|
|
101
|
-
<LockBadge
|
|
101
|
+
<LockBadge
|
|
102
|
+
paymentStatus={paymentStatus}
|
|
103
|
+
mimeType={mimeType}
|
|
104
|
+
isLocked={showLocked}
|
|
105
|
+
/>
|
|
102
106
|
</div>
|
|
103
107
|
) : (
|
|
104
108
|
<AttachmentThumbnail
|
|
@@ -125,4 +129,4 @@ const LockedThumbnail: React.FC<LockedThumbnailProps> = ({
|
|
|
125
129
|
)
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
export default
|
|
132
|
+
export default SingleThumbnail
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export interface SkeletonTextLinesProps {
|
|
5
|
+
/** Matches the bubble it sits in — "dark" for the sender's own bubble. */
|
|
6
|
+
variant: 'light' | 'dark'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Two skeleton bars standing in for the redacted text of a paid text-only
|
|
11
|
+
* message — the real content is never sent to the client until purchase, so
|
|
12
|
+
* there's nothing to render yet.
|
|
13
|
+
*
|
|
14
|
+
* Uses explicit black/white (not `bg-current`) because Tailwind doesn't
|
|
15
|
+
* support opacity modifiers on the `current` color — `bg-current/30` silently
|
|
16
|
+
* produces no CSS rule at all.
|
|
17
|
+
*/
|
|
18
|
+
const SkeletonTextLines: React.FC<SkeletonTextLinesProps> = ({ variant = 'dark' }) => {
|
|
19
|
+
const colour = variant === 'dark' ? 'bg-white/25' : 'bg-black/15'
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-col gap-2 str-chat__message-text my-1 animate-pulse" aria-hidden>
|
|
23
|
+
<div className={classNames('h-3 w-40 rounded-full', colour)} />
|
|
24
|
+
<div className={classNames('h-3 w-24 rounded-full', colour)} />
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default SkeletonTextLines
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Chat-bubble chrome for a paid text-only message/preview. Deliberately
|
|
6
|
+
* doesn't lean on `.str-chat`'s CSS variables (`--str-chat__message-bubble-*`)
|
|
7
|
+
* — this renders in the Composer preview too, which sits outside any
|
|
8
|
+
* `.str-chat` ancestor, so every value here (radius, colour, type scale) is
|
|
9
|
+
* hardcoded to match the Figma spec directly.
|
|
10
|
+
*/
|
|
11
|
+
const TextShell: React.FC<
|
|
12
|
+
React.PropsWithChildren<{ isMine: boolean; className?: string }>
|
|
13
|
+
> = ({ isMine, children, className }) => (
|
|
14
|
+
<div
|
|
15
|
+
className={classNames(
|
|
16
|
+
'message-locked-attachment-text',
|
|
17
|
+
'relative flex min-w-[225px] max-w-[280px] flex-col gap-1 rounded-[1.5rem] px-4 py-3',
|
|
18
|
+
'text-[0.875rem] leading-[1.3125rem] tracking-[0.14px]',
|
|
19
|
+
isMine ? 'bg-[#1e2330] text-white' : 'bg-[#f2f1ef] text-black',
|
|
20
|
+
'shadow-[0_1px_2px_rgba(0,0,0,0.08)]',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
export default TextShell
|