@linktr.ee/messaging-react 3.10.0-rc-1783660962 → 3.10.0
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-B-L2MST4.js +107 -0
- package/dist/Card-B-L2MST4.js.map +1 -0
- package/dist/Card-B56QxJW3.cjs +2 -0
- package/dist/Card-B56QxJW3.cjs.map +1 -0
- package/dist/Card-Bqh-BQaX.js +132 -0
- package/dist/Card-Bqh-BQaX.js.map +1 -0
- package/dist/Card-ZCE6Lhfc.cjs +2 -0
- package/dist/Card-ZCE6Lhfc.cjs.map +1 -0
- package/dist/Card-g4JD-2ES.js +163 -0
- package/dist/Card-g4JD-2ES.js.map +1 -0
- package/dist/Card-wac6AMNc.cjs +2 -0
- package/dist/Card-wac6AMNc.cjs.map +1 -0
- package/dist/LockedThumbnail-BAQmoIff.js +220 -0
- package/dist/LockedThumbnail-BAQmoIff.js.map +1 -0
- package/dist/LockedThumbnail-Cr0Gqs3Y.cjs +2 -0
- package/dist/LockedThumbnail-Cr0Gqs3Y.cjs.map +1 -0
- package/dist/assets/index.css +1 -1
- package/dist/index-BPhZyaxT.cjs +2 -0
- package/dist/index-BPhZyaxT.cjs.map +1 -0
- package/dist/{index-DFZrAiLB.js → index-BxzzWK4K.js} +1249 -1276
- package/dist/index-BxzzWK4K.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +101 -164
- package/dist/index.js +1 -1
- package/dist/testing.d.ts +0 -1
- package/package.json +2 -2
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +0 -95
- package/src/components/ChannelList/CustomChannelPreview.tsx +9 -21
- package/src/components/CustomMessage/CustomMessage.stories.tsx +1 -54
- package/src/components/CustomMessage/MessageTag.tsx +0 -8
- package/src/components/CustomMessage/context.tsx +1 -1
- package/src/components/CustomMessage/index.tsx +42 -76
- package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +3 -5
- package/src/components/LockedAttachment/LockedAttachment.stories.tsx +450 -0
- package/src/components/LockedAttachment/components/Composer/Card.tsx +221 -0
- package/src/components/LockedAttachment/components/Composer/index.ts +2 -0
- package/src/components/LockedAttachment/components/Received/Card.tsx +191 -0
- package/src/components/{CustomMessage/LockedAttachment/components/Media → LockedAttachment/components/Received}/CardActions.tsx +2 -9
- package/src/components/LockedAttachment/components/Received/index.ts +2 -0
- package/src/components/LockedAttachment/components/Sent/Card.tsx +177 -0
- package/src/components/LockedAttachment/components/Sent/index.ts +2 -0
- package/src/components/{CustomMessage/LockedAttachment → LockedAttachment}/components/_shared/CardBody.tsx +7 -8
- package/src/components/{CustomMessage/LockedAttachment → LockedAttachment}/components/_shared/GalleryThumbnail.tsx +39 -31
- package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
- package/src/components/{CustomMessage/LockedAttachment/components/_shared/MediaShell.tsx → LockedAttachment/components/_shared/LockedCardShell.tsx} +8 -8
- package/src/components/{CustomMessage/LockedAttachment/components/_shared/SingleThumbnail.tsx → LockedAttachment/components/_shared/LockedThumbnail.tsx} +7 -11
- package/src/components/LockedAttachment/index.tsx +63 -0
- package/src/components/LockedAttachment/types.ts +51 -0
- package/src/index.ts +8 -3
- package/src/stream-custom-data.ts +0 -1
- package/src/styles.css +0 -12
- package/src/types.ts +1 -1
- package/dist/Card-415OfRhw.cjs +0 -2
- package/dist/Card-415OfRhw.cjs.map +0 -1
- package/dist/Card-CyWdUMCB.cjs +0 -2
- package/dist/Card-CyWdUMCB.cjs.map +0 -1
- package/dist/Card-V5d6qhDT.js +0 -694
- package/dist/Card-V5d6qhDT.js.map +0 -1
- package/dist/Card-qS17qnN0.js +0 -103
- package/dist/Card-qS17qnN0.js.map +0 -1
- package/dist/LoadingDots-Ct3WmVlT.js +0 -59
- package/dist/LoadingDots-Ct3WmVlT.js.map +0 -1
- package/dist/LoadingDots-D9GGphgM.cjs +0 -2
- package/dist/LoadingDots-D9GGphgM.cjs.map +0 -1
- package/dist/index-Bx_zjNMR.cjs +0 -2
- package/dist/index-Bx_zjNMR.cjs.map +0 -1
- package/dist/index-DFZrAiLB.js.map +0 -1
- package/src/components/CustomMessage/LockedAttachment/LockedAttachment.stories.tsx +0 -541
- package/src/components/CustomMessage/LockedAttachment/LockedAttachment.test.tsx +0 -117
- package/src/components/CustomMessage/LockedAttachment/components/Media/Card.tsx +0 -358
- package/src/components/CustomMessage/LockedAttachment/components/Media/types.ts +0 -79
- package/src/components/CustomMessage/LockedAttachment/components/Text/Card.tsx +0 -153
- package/src/components/CustomMessage/LockedAttachment/components/Text/types.ts +0 -58
- package/src/components/CustomMessage/LockedAttachment/components/_shared/CardThumbnail.tsx +0 -92
- package/src/components/CustomMessage/LockedAttachment/components/_shared/LoadingDots.tsx +0 -36
- package/src/components/CustomMessage/LockedAttachment/components/_shared/LockBadge.tsx +0 -63
- package/src/components/CustomMessage/LockedAttachment/components/_shared/PreviewToggle.tsx +0 -58
- package/src/components/CustomMessage/LockedAttachment/components/_shared/PriceLockRow.tsx +0 -21
- package/src/components/CustomMessage/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +0 -74
- package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx +0 -29
- package/src/components/CustomMessage/LockedAttachment/components/_shared/TextShell.tsx +0 -28
- package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.test.ts +0 -67
- package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.ts +0 -89
- package/src/components/CustomMessage/LockedAttachment/components/_shared/useReceivedSource.ts +0 -83
- package/src/components/CustomMessage/LockedAttachment/index.tsx +0 -105
- package/src/components/CustomMessage/LockedAttachment/types.ts +0 -44
|
@@ -1,92 +0,0 @@
|
|
|
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,36 +0,0 @@
|
|
|
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,63 +0,0 @@
|
|
|
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,58 +0,0 @@
|
|
|
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
|
|
@@ -1,21 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/SkeletonTextLines.tsx
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.test.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { act, renderHook } from '../../../../../test/utils'
|
|
4
|
-
|
|
5
|
-
import { usePreviewToggle } from './usePreviewToggle'
|
|
6
|
-
|
|
7
|
-
const source = { sourceUrl: 'https://example.test/asset.mp4' }
|
|
8
|
-
|
|
9
|
-
describe('usePreviewToggle', () => {
|
|
10
|
-
it('gallery: reveals without invoking the async source fetcher', async () => {
|
|
11
|
-
const onFetchSource = vi.fn().mockResolvedValue(source)
|
|
12
|
-
const onPreviewClick = vi.fn()
|
|
13
|
-
const { result } = renderHook(() =>
|
|
14
|
-
usePreviewToggle({ isGallery: true, onFetchSource, onPreviewClick })
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
await act(async () => {
|
|
18
|
-
await result.current.handleToggle()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
expect(result.current.isPreviewVisible).toBe(true)
|
|
22
|
-
expect(onPreviewClick).toHaveBeenCalledTimes(1)
|
|
23
|
-
expect(onFetchSource).not.toHaveBeenCalled()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('first reveal fetches the source, then caches it on subsequent toggles', async () => {
|
|
27
|
-
const onFetchSource = vi.fn().mockResolvedValue(source)
|
|
28
|
-
const { result } = renderHook(() =>
|
|
29
|
-
usePreviewToggle({ isGallery: false, onFetchSource })
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
// First reveal → fetches.
|
|
33
|
-
await act(async () => {
|
|
34
|
-
await result.current.handleToggle()
|
|
35
|
-
})
|
|
36
|
-
expect(result.current.isPreviewVisible).toBe(true)
|
|
37
|
-
expect(result.current.source).toEqual(source)
|
|
38
|
-
expect(onFetchSource).toHaveBeenCalledTimes(1)
|
|
39
|
-
|
|
40
|
-
// Toggle off.
|
|
41
|
-
await act(async () => {
|
|
42
|
-
await result.current.handleToggle()
|
|
43
|
-
})
|
|
44
|
-
expect(result.current.isPreviewVisible).toBe(false)
|
|
45
|
-
|
|
46
|
-
// Toggle back on → reuses the cache, no second fetch.
|
|
47
|
-
await act(async () => {
|
|
48
|
-
await result.current.handleToggle()
|
|
49
|
-
})
|
|
50
|
-
expect(result.current.isPreviewVisible).toBe(true)
|
|
51
|
-
expect(onFetchSource).toHaveBeenCalledTimes(1)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('stays locked when the fetcher resolves without a source', async () => {
|
|
55
|
-
const onFetchSource = vi.fn().mockResolvedValue(undefined)
|
|
56
|
-
const { result } = renderHook(() =>
|
|
57
|
-
usePreviewToggle({ isGallery: false, onFetchSource })
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
await act(async () => {
|
|
61
|
-
await result.current.handleToggle()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
expect(result.current.isPreviewVisible).toBe(false)
|
|
65
|
-
expect(result.current.source).toBeUndefined()
|
|
66
|
-
})
|
|
67
|
-
})
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/usePreviewToggle.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import type { LockedAttachmentSource } from '../../types'
|
|
4
|
-
|
|
5
|
-
export interface UsePreviewToggleOptions {
|
|
6
|
-
/** Gallery items carry their own per-item sources on the `gallery` prop already, so toggling skips the async fetch entirely. */
|
|
7
|
-
isGallery: boolean
|
|
8
|
-
/**
|
|
9
|
-
* Lazily loads the underlying source; the resolved value is cached in
|
|
10
|
-
* `source` and reused on subsequent toggles.
|
|
11
|
-
*/
|
|
12
|
-
onFetchSource?: () => Promise<LockedAttachmentSource | void>
|
|
13
|
-
/** Fired on every toggle tap, before any visibility/fetch logic runs. */
|
|
14
|
-
onPreviewClick?: () => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface UsePreviewToggleResult {
|
|
18
|
-
source: LockedAttachmentSource | undefined
|
|
19
|
-
isPreviewVisible: boolean
|
|
20
|
-
isLoadingPreview: boolean
|
|
21
|
-
handleToggle: () => Promise<void>
|
|
22
|
-
/**
|
|
23
|
-
* `handleToggle` when the card can actually be previewed (gallery, or a
|
|
24
|
-
* fetch/click handler is wired), else `undefined` — pass straight to
|
|
25
|
-
* `PreviewToggle.onToggle` so a non-previewable card renders no
|
|
26
|
-
* toggle affordance.
|
|
27
|
-
*/
|
|
28
|
-
togglePreview: (() => Promise<void>) | undefined
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Shared preview-toggle state for Composer/Media isMine: flips a locked thumbnail to
|
|
33
|
-
* its unlocked preview, lazily fetching and caching the source on first
|
|
34
|
-
* reveal. Media !isMine doesn't use this — it fetches on scroll-into-view/paid
|
|
35
|
-
* instead of on a toggle tap.
|
|
36
|
-
*/
|
|
37
|
-
export function usePreviewToggle({
|
|
38
|
-
isGallery,
|
|
39
|
-
onFetchSource,
|
|
40
|
-
onPreviewClick,
|
|
41
|
-
}: UsePreviewToggleOptions): UsePreviewToggleResult {
|
|
42
|
-
const [source, setSource] = useState<LockedAttachmentSource | undefined>()
|
|
43
|
-
const [isPreviewVisible, setIsPreviewVisible] = useState(false)
|
|
44
|
-
const [isLoadingPreview, setIsLoadingPreview] = useState(false)
|
|
45
|
-
const fetchingRef = useRef(false)
|
|
46
|
-
|
|
47
|
-
const handleToggle = useCallback(async () => {
|
|
48
|
-
onPreviewClick?.()
|
|
49
|
-
if (isPreviewVisible) {
|
|
50
|
-
setIsPreviewVisible(false)
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
if (isGallery) {
|
|
54
|
-
setIsPreviewVisible(true)
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
if (source) {
|
|
58
|
-
setIsPreviewVisible(true)
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
if (!onFetchSource) return
|
|
62
|
-
if (fetchingRef.current) return
|
|
63
|
-
fetchingRef.current = true
|
|
64
|
-
setIsLoadingPreview(true)
|
|
65
|
-
try {
|
|
66
|
-
const result = await onFetchSource()
|
|
67
|
-
if (result) {
|
|
68
|
-
setSource(result)
|
|
69
|
-
setIsPreviewVisible(true)
|
|
70
|
-
}
|
|
71
|
-
} finally {
|
|
72
|
-
fetchingRef.current = false
|
|
73
|
-
setIsLoadingPreview(false)
|
|
74
|
-
}
|
|
75
|
-
}, [isPreviewVisible, isGallery, source, onPreviewClick, onFetchSource])
|
|
76
|
-
|
|
77
|
-
const togglePreview =
|
|
78
|
-
isGallery || onFetchSource != null || onPreviewClick != null
|
|
79
|
-
? handleToggle
|
|
80
|
-
: undefined
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
source,
|
|
84
|
-
isPreviewVisible,
|
|
85
|
-
isLoadingPreview,
|
|
86
|
-
handleToggle,
|
|
87
|
-
togglePreview,
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/components/CustomMessage/LockedAttachment/components/_shared/useReceivedSource.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
FetchSourceHandler,
|
|
5
|
-
LockedAttachmentSource,
|
|
6
|
-
PaymentStatus,
|
|
7
|
-
} from '../../types'
|
|
8
|
-
|
|
9
|
-
export interface UseReceivedSourceOptions {
|
|
10
|
-
paymentStatus?: PaymentStatus
|
|
11
|
-
onFetchSource?: FetchSourceHandler
|
|
12
|
-
/** Called on Unlock click while the card is still unpaid (opens checkout). */
|
|
13
|
-
onUnlockClick?: () => void
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface UseReceivedSourceResult {
|
|
17
|
-
source: LockedAttachmentSource | undefined
|
|
18
|
-
/** Attach to the card root so the paid-source auto-fetch can observe it. */
|
|
19
|
-
cardRef: React.RefObject<HTMLDivElement>
|
|
20
|
-
/** Fetches the source when already paid, else defers to `onUnlockClick`. */
|
|
21
|
-
handleUnlockClick: () => void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Media !isMine's source-reveal logic: unlike Composer/Media isMine's
|
|
26
|
-
* tap-to-toggle (`usePreviewToggle`), the recipient's card reveals its source
|
|
27
|
-
* by fetching once — on click when already paid, or automatically the first
|
|
28
|
-
* time a paid card scrolls fully into view. The fetch is guarded so it runs
|
|
29
|
-
* at most once in flight.
|
|
30
|
-
*/
|
|
31
|
-
export function useReceivedSource({
|
|
32
|
-
paymentStatus,
|
|
33
|
-
onFetchSource,
|
|
34
|
-
onUnlockClick,
|
|
35
|
-
}: UseReceivedSourceOptions): UseReceivedSourceResult {
|
|
36
|
-
const [source, setSource] = useState<LockedAttachmentSource | undefined>()
|
|
37
|
-
|
|
38
|
-
const cardRef = useRef<HTMLDivElement>(null)
|
|
39
|
-
const fetchingRef = useRef(false)
|
|
40
|
-
|
|
41
|
-
const onFetchSourceRef = useRef(onFetchSource)
|
|
42
|
-
onFetchSourceRef.current = onFetchSource
|
|
43
|
-
|
|
44
|
-
const fetchSource = useCallback(async (): Promise<void> => {
|
|
45
|
-
if (fetchingRef.current) return
|
|
46
|
-
fetchingRef.current = true
|
|
47
|
-
try {
|
|
48
|
-
const result = await onFetchSourceRef.current?.()
|
|
49
|
-
if (result) setSource(result)
|
|
50
|
-
} finally {
|
|
51
|
-
fetchingRef.current = false
|
|
52
|
-
}
|
|
53
|
-
}, [])
|
|
54
|
-
|
|
55
|
-
const handleUnlockClick = useCallback(() => {
|
|
56
|
-
if (paymentStatus === 'paid') {
|
|
57
|
-
void fetchSource()
|
|
58
|
-
} else {
|
|
59
|
-
onUnlockClick?.()
|
|
60
|
-
}
|
|
61
|
-
}, [paymentStatus, fetchSource, onUnlockClick])
|
|
62
|
-
|
|
63
|
-
// Auto-fetch the source once the paid card scrolls into view.
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
if (!cardRef.current) return
|
|
66
|
-
if (paymentStatus !== 'paid' || source !== undefined) return
|
|
67
|
-
|
|
68
|
-
const observer = new IntersectionObserver(
|
|
69
|
-
([entry]) => {
|
|
70
|
-
if (entry.isIntersecting) {
|
|
71
|
-
void fetchSource()
|
|
72
|
-
observer.disconnect()
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
{ threshold: 1.0 }
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
observer.observe(cardRef.current)
|
|
79
|
-
return () => observer.disconnect()
|
|
80
|
-
}, [paymentStatus, source, fetchSource])
|
|
81
|
-
|
|
82
|
-
return { source, cardRef, handleUnlockClick }
|
|
83
|
-
}
|