@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.
Files changed (87) hide show
  1. package/dist/Card-BG-Mg_ss.js +221 -0
  2. package/dist/Card-BG-Mg_ss.js.map +1 -0
  3. package/dist/Card-CmRzkuPw.cjs +2 -0
  4. package/dist/Card-CmRzkuPw.cjs.map +1 -0
  5. package/dist/Card-DSG-wR8t.js +96 -0
  6. package/dist/Card-DSG-wR8t.js.map +1 -0
  7. package/dist/Card-DVEuTlf_.cjs +2 -0
  8. package/dist/Card-DVEuTlf_.cjs.map +1 -0
  9. package/dist/Card-K9ky-G-t.js +108 -0
  10. package/dist/Card-K9ky-G-t.js.map +1 -0
  11. package/dist/Card-iKTcn1wg.cjs +2 -0
  12. package/dist/Card-iKTcn1wg.cjs.map +1 -0
  13. package/dist/PurchaseStatusBadge-CJH4S-8Z.js +36 -0
  14. package/dist/PurchaseStatusBadge-CJH4S-8Z.js.map +1 -0
  15. package/dist/PurchaseStatusBadge-UjDzZhLj.cjs +2 -0
  16. package/dist/PurchaseStatusBadge-UjDzZhLj.cjs.map +1 -0
  17. package/dist/TextShell-BiqF2UB0.js +32 -0
  18. package/dist/TextShell-BiqF2UB0.js.map +1 -0
  19. package/dist/TextShell-Bnzh0T75.cjs +2 -0
  20. package/dist/TextShell-Bnzh0T75.cjs.map +1 -0
  21. package/dist/assets/index.css +1 -1
  22. package/dist/{index-BCgRoF_u.js → index-DO_dYsIf.js} +1154 -1113
  23. package/dist/index-DO_dYsIf.js.map +1 -0
  24. package/dist/index-DpzmKL5U.cjs +2 -0
  25. package/dist/index-DpzmKL5U.cjs.map +1 -0
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.ts +98 -55
  28. package/dist/index.js +1 -1
  29. package/dist/testing.d.ts +1 -0
  30. package/dist/usePreviewToggle-B5kmPELX.cjs +2 -0
  31. package/dist/usePreviewToggle-B5kmPELX.cjs.map +1 -0
  32. package/dist/usePreviewToggle-D27Xw9Xx.js +323 -0
  33. package/dist/usePreviewToggle-D27Xw9Xx.js.map +1 -0
  34. package/package.json +2 -2
  35. package/src/components/CustomMessage/CustomMessage.stories.tsx +54 -1
  36. package/src/components/CustomMessage/MessageTag.tsx +8 -0
  37. package/src/components/CustomMessage/index.tsx +61 -22
  38. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +410 -333
  39. package/src/components/LockedAttachment/LockedAttachment.test.tsx +104 -0
  40. package/src/components/LockedAttachment/components/Composer/Card.tsx +97 -120
  41. package/src/components/LockedAttachment/components/Media/Card.tsx +237 -0
  42. package/src/components/LockedAttachment/components/Text/Card.tsx +177 -0
  43. package/src/components/LockedAttachment/components/_shared/CardBody.tsx +3 -3
  44. package/src/components/LockedAttachment/components/_shared/CardThumbnail.tsx +92 -0
  45. package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +11 -33
  46. package/src/components/LockedAttachment/components/_shared/LoadingDots.tsx +36 -0
  47. package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +21 -11
  48. package/src/components/LockedAttachment/components/_shared/{LockedCardShell.tsx → MediaShell.tsx} +5 -5
  49. package/src/components/LockedAttachment/components/_shared/PreviewToggle.tsx +58 -0
  50. package/src/components/LockedAttachment/components/_shared/PriceLockRow.tsx +21 -0
  51. package/src/components/LockedAttachment/components/_shared/PurchaseStatusBadge.tsx +87 -0
  52. package/src/components/LockedAttachment/components/_shared/{LockedThumbnail.tsx → SingleThumbnail.tsx} +5 -5
  53. package/src/components/LockedAttachment/components/_shared/SkeletonTextLines.tsx +39 -0
  54. package/src/components/LockedAttachment/components/_shared/TextShell.tsx +19 -0
  55. package/src/components/LockedAttachment/components/_shared/usePreviewToggle.test.ts +67 -0
  56. package/src/components/LockedAttachment/components/_shared/usePreviewToggle.ts +89 -0
  57. package/src/components/LockedAttachment/components/_shared/useReceivedSource.ts +83 -0
  58. package/src/components/LockedAttachment/index.tsx +17 -28
  59. package/src/components/LockedAttachment/types.ts +28 -4
  60. package/src/index.ts +2 -5
  61. package/src/stream-custom-data.ts +1 -0
  62. package/src/styles.css +5 -0
  63. package/dist/Card-BRvcFjsq.js +0 -132
  64. package/dist/Card-BRvcFjsq.js.map +0 -1
  65. package/dist/Card-BuXkok0A.cjs +0 -2
  66. package/dist/Card-BuXkok0A.cjs.map +0 -1
  67. package/dist/Card-CvNHv3me.js +0 -107
  68. package/dist/Card-CvNHv3me.js.map +0 -1
  69. package/dist/Card-DOSuQdnk.cjs +0 -2
  70. package/dist/Card-DOSuQdnk.cjs.map +0 -1
  71. package/dist/Card-NnQw99XE.cjs +0 -2
  72. package/dist/Card-NnQw99XE.cjs.map +0 -1
  73. package/dist/Card-jGOcSN1O.js +0 -163
  74. package/dist/Card-jGOcSN1O.js.map +0 -1
  75. package/dist/LockedThumbnail-EsjaAqLR.js +0 -220
  76. package/dist/LockedThumbnail-EsjaAqLR.js.map +0 -1
  77. package/dist/LockedThumbnail-uAauUd5c.cjs +0 -2
  78. package/dist/LockedThumbnail-uAauUd5c.cjs.map +0 -1
  79. package/dist/index-BCgRoF_u.js.map +0 -1
  80. package/dist/index-DRurVQ0-.cjs +0 -2
  81. package/dist/index-DRurVQ0-.cjs.map +0 -1
  82. package/src/components/LockedAttachment/components/Composer/index.ts +0 -2
  83. package/src/components/LockedAttachment/components/Received/Card.tsx +0 -191
  84. package/src/components/LockedAttachment/components/Received/index.ts +0 -2
  85. package/src/components/LockedAttachment/components/Sent/Card.tsx +0 -177
  86. package/src/components/LockedAttachment/components/Sent/index.ts +0 -2
  87. /package/src/components/LockedAttachment/components/{Received → Media}/CardActions.tsx +0 -0
@@ -0,0 +1,39 @@
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
+ * Whether the bars pulse. Pass `false` once the outcome is settled (paid,
9
+ * failed, refunded) — a static skeleton reads as "nothing more will
10
+ * change", while a pulsing one implies something is still in flight.
11
+ */
12
+ animate: boolean
13
+ }
14
+
15
+ /**
16
+ * Two skeleton bars standing in for the redacted text of a paid text-only
17
+ * message — the real content is never sent to the client until purchase, so
18
+ * there's nothing to render yet.
19
+ *
20
+ * Uses explicit black/white (not `bg-current`) because Tailwind doesn't
21
+ * support opacity modifiers on the `current` color — `bg-current/30` silently
22
+ * produces no CSS rule at all.
23
+ */
24
+ const SkeletonTextLines: React.FC<SkeletonTextLinesProps> = ({
25
+ variant = 'dark',
26
+ animate = true,
27
+ }) => {
28
+ const pulse = animate ? 'animate-pulse motion-reduce:animate-none' : ''
29
+ const colour = variant === 'dark' ? 'bg-white/25' : 'bg-black/15'
30
+
31
+ return (
32
+ <div className="flex flex-col gap-2 str-chat__message-text my-1" aria-hidden>
33
+ <div className={classNames('h-3 w-40 rounded-full', pulse, colour)} />
34
+ <div className={classNames('h-3 w-24 rounded-full', pulse, colour)} />
35
+ </div>
36
+ )
37
+ }
38
+
39
+ export default SkeletonTextLines
@@ -0,0 +1,19 @@
1
+ import classNames from 'classnames'
2
+ import React from 'react'
3
+
4
+ const TextShell: React.FC<React.PropsWithChildren<{ className?: string }>> = ({
5
+ children,
6
+ className,
7
+ }) => (
8
+ <div
9
+ className={classNames(
10
+ 'str-chat__message-bubble message-locked-attachment-text',
11
+ 'px-4 py-3 flex flex-col gap-1',
12
+ className
13
+ )}
14
+ >
15
+ {children}
16
+ </div>
17
+ )
18
+
19
+ export default TextShell
@@ -0,0 +1,67 @@
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
+ })
@@ -0,0 +1,89 @@
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
+ }
@@ -0,0 +1,83 @@
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
+ }
@@ -1,12 +1,12 @@
1
1
  import React, { Suspense } from 'react'
2
2
 
3
3
  import type { ComposerCardProps } from './components/Composer/Card'
4
- import type { ReceivedCardProps } from './components/Received/Card'
5
- import type { SentCardProps } from './components/Sent/Card'
4
+ import type { MediaCardProps } from './components/Media/Card'
5
+ import type { TextCardProps } from './components/Text/Card'
6
6
 
7
7
  const ComposerCardLazy = React.lazy(() => import('./components/Composer/Card'))
8
- const SentCardLazy = React.lazy(() => import('./components/Sent/Card'))
9
- const ReceivedCardLazy = React.lazy(() => import('./components/Received/Card'))
8
+ const TextCardLazy = React.lazy(() => import('./components/Text/Card'))
9
+ const MediaCardLazy = React.lazy(() => import('./components/Media/Card'))
10
10
 
11
11
  const LockedAttachmentFallback = () => (
12
12
  <div
@@ -21,42 +21,31 @@ const Composer = (props: ComposerCardProps) => (
21
21
  </Suspense>
22
22
  )
23
23
 
24
- const Sent = (props: SentCardProps) => (
24
+ const Text = (props: TextCardProps) => (
25
25
  <Suspense fallback={<LockedAttachmentFallback />}>
26
- <SentCardLazy {...props} />
26
+ <TextCardLazy {...props} />
27
27
  </Suspense>
28
28
  )
29
29
 
30
- const Received = (props: ReceivedCardProps) => (
30
+ const Media = (props: MediaCardProps) => (
31
31
  <Suspense fallback={<LockedAttachmentFallback />}>
32
- <ReceivedCardLazy {...props} />
32
+ <MediaCardLazy {...props} />
33
33
  </Suspense>
34
34
  )
35
35
 
36
- /**
37
- * @deprecated Renamed to `LockedAttachment.Sent` (post-send sender card) or
38
- * `LockedAttachment.Composer` (drafting state). This alias maps to `Sent`
39
- * because that matches the previous in-thread usage; pre-send / drafting
40
- * usages (e.g. with `onDismiss`) should migrate to `LockedAttachment.Composer`.
41
- */
42
- const Creator = Sent
43
-
44
- /** @deprecated Renamed to `LockedAttachment.Received`. */
45
- const Visitor = Received
46
-
47
- const LockedAttachment = { Composer, Sent, Received, Creator, Visitor }
36
+ const LockedAttachment = {
37
+ Composer,
38
+ Text,
39
+ Media,
40
+ }
48
41
 
49
42
  export default LockedAttachment
50
- export type { ComposerCardProps, SentCardProps, ReceivedCardProps }
51
- /**
52
- * @deprecated Renamed to `SentCardProps`. Drafting usages (with `onDismiss`)
53
- * should migrate to `ComposerCardProps`.
54
- */
55
- export type CreatorCardProps = SentCardProps
56
- /** @deprecated Renamed to `ReceivedCardProps`. */
57
- export type VisitorCardProps = ReceivedCardProps
43
+ export { default as PurchaseStatusBadge } from './components/_shared/PurchaseStatusBadge'
44
+ export type { PurchaseStatusBadgeProps } from './components/_shared/PurchaseStatusBadge'
45
+ export type { ComposerCardProps, TextCardProps, MediaCardProps }
58
46
  export type {
59
47
  PaymentStatus,
48
+ FetchSourceHandler,
60
49
  LockedAttachmentSource,
61
50
  LockedAttachmentGalleryItem,
62
51
  LockedAttachmentContextValue,
@@ -4,19 +4,28 @@ import type { PaymentStatus } from '../../stream-custom-data'
4
4
 
5
5
  export interface LockedAttachmentBaseProps {
6
6
  title?: string
7
+ /** Dimmed fallback shown in the title slot when `title` is unset (no attachment metadata yet — still drafting). */
8
+ placeholderTitle?: string
7
9
  mimeType?: string
8
10
  thumbnailUrl?: string
9
11
  detail?: string
10
12
  amountText?: string
11
- paymentStatus?: PaymentStatus
12
13
  /**
13
14
  * When provided with 2+ items, the card renders as a mixed-media carousel
14
15
  * (e.g. a couple of photos + a video) instead of a single thumbnail. Each
15
16
  * item brings its own thumbnail and optional source so that
16
- * `LockedAttachment.Composer` / `.Sent` / `.Received` can all share the
17
- * same carousel chrome.
17
+ * `LockedAttachment.Composer` / `.Media` can share the same carousel
18
+ * chrome.
18
19
  */
19
20
  gallery?: LockedAttachmentGalleryItem[]
21
+ /**
22
+ * "text" renders the card as a locked/plain chat bubble showing the
23
+ * message's own text (skeleton-blurred until purchased) instead of a media
24
+ * thumbnail card — for a paid text-only message. Media-only props
25
+ * (`mimeType`, `thumbnailUrl`, `title`, `detail`, `gallery`) are ignored in
26
+ * this mode. Defaults to `"media"`.
27
+ */
28
+ contentType?: 'text' | 'media'
20
29
  }
21
30
 
22
31
  export interface LockedAttachmentGalleryItem {
@@ -39,11 +48,26 @@ export interface LockedAttachmentSource {
39
48
 
40
49
  export type { PaymentStatus }
41
50
 
51
+ /**
52
+ * Lazily fetches the underlying source (URL/thumbnail) for a locked
53
+ * attachment. Returns `void` to signal "no source available" — the card
54
+ * stays locked. Shared by `ComposerCardProps`/`MediaCardProps.onFetchSource`.
55
+ */
56
+ export type FetchSourceHandler = () => Promise<LockedAttachmentSource | void>
57
+
42
58
  export interface LockedAttachmentContextValue {
43
59
  isUnlocking: (id: string) => boolean
44
60
  onUnlockClick?: (message: LocalMessage, channel: Channel) => void
45
61
  onDownloadClick?: (message: LocalMessage, channel: Channel) => void
46
- onFetchSource?: (message: LocalMessage, channel: Channel) => Promise<LockedAttachmentSource | void>
62
+ /**
63
+ * Reveal the locked media's underlying source (URL/thumbnail). Not used
64
+ * for a paid text message — the backend writes the real text directly onto
65
+ * the message on purchase and `message.text` alone drives that reveal.
66
+ */
67
+ onFetchSource?: (
68
+ message: LocalMessage,
69
+ channel: Channel
70
+ ) => Promise<LockedAttachmentSource | void>
47
71
  }
48
72
 
49
73
  export const defaultLockedAttachmentContextValue: LockedAttachmentContextValue = {
package/src/index.ts CHANGED
@@ -70,13 +70,10 @@ export type { AvatarProps } from './components/Avatar'
70
70
  export type { ActionButtonProps } from './components/ActionButton'
71
71
  export type {
72
72
  ComposerCardProps,
73
- SentCardProps,
74
- ReceivedCardProps,
73
+ TextCardProps as LockedAttachmentTextCardProps,
74
+ MediaCardProps as LockedAttachmentMediaCardProps,
75
75
  LockedAttachmentGalleryItem,
76
76
  LockedAttachmentContextValue,
77
- // Deprecated aliases preserved for backwards compatibility:
78
- CreatorCardProps,
79
- VisitorCardProps,
80
77
  } from './components/LockedAttachment'
81
78
  export type {
82
79
  ComposerCardProps as LinkAttachmentComposerCardProps,
@@ -41,6 +41,7 @@ export interface MessageMetadata {
41
41
  amount_text?: string
42
42
  payment_status?: PaymentStatus
43
43
  attachment_title?: string
44
+ attachment_type?: 'text' | 'media'
44
45
  attachment_mime_type?: string
45
46
  attachment_thumbnail?: string
46
47
  attachment_detail?: string
package/src/styles.css CHANGED
@@ -831,3 +831,8 @@
831
831
  color: #016e1a;
832
832
  background-color: #dbf0e0;
833
833
  }
834
+
835
+ /** Remove padding from the text inside the locked attachment text shell */
836
+ .message-locked-attachment-text .str-chat__message-text {
837
+ padding: 0rem !important;
838
+ }
@@ -1,132 +0,0 @@
1
- import { jsxs as y, jsx as t } from "react/jsx-runtime";
2
- import { ImagesIcon as A, PencilSimpleIcon as C, XIcon as D } from "@phosphor-icons/react";
3
- import V, { useState as h, useRef as E, useCallback as K } from "react";
4
- import { L as P, C as S, G as X, a as q } from "./LockedThumbnail-EsjaAqLR.js";
5
- const Q = ({
6
- title: l,
7
- mimeType: b = "application/octet-stream",
8
- thumbnailUrl: N,
9
- detail: I,
10
- amountText: g,
11
- placeholderTitle: L = "Attachment title",
12
- placeholderAmountText: z,
13
- gallery: r,
14
- onDismiss: p,
15
- onPreviewClick: a,
16
- onFetchSource: n,
17
- onEditClick: x
18
- }) => {
19
- const [c, B] = h(), [u, o] = h(!1), [R, w] = h(!1), d = E(!1), i = ((r == null ? void 0 : r.length) ?? 0) >= 2, j = K(async () => {
20
- if (a == null || a(), u) {
21
- o(!1);
22
- return;
23
- }
24
- if (i) {
25
- o(!0);
26
- return;
27
- }
28
- if (c) {
29
- o(!0);
30
- return;
31
- }
32
- if (n && !d.current) {
33
- d.current = !0, w(!0);
34
- try {
35
- const e = await n();
36
- e && (B(e), o(!0));
37
- } finally {
38
- d.current = !1, w(!1);
39
- }
40
- }
41
- }, [u, i, c, a, n]), f = i || n || a ? j : void 0, m = !u, s = R, G = /* @__PURE__ */ y(V.Fragment, { children: [
42
- /* @__PURE__ */ t("span", { className: "text-xs font-medium text-white/55", children: "•" }),
43
- /* @__PURE__ */ t(
44
- "span",
45
- {
46
- className: g ? "text-xs font-medium text-white/55" : "text-xs font-medium text-white/30",
47
- children: g || z
48
- }
49
- )
50
- ] }), v = p ? /* @__PURE__ */ t(
51
- "button",
52
- {
53
- type: "button",
54
- onClick: (e) => {
55
- e.stopPropagation(), p();
56
- },
57
- className: "flex size-6 items-center justify-center rounded-full bg-[#121110] text-white",
58
- "aria-label": "Dismiss attachment",
59
- children: /* @__PURE__ */ t(D, { className: "size-3", weight: "bold" })
60
- }
61
- ) : void 0, T = x ? /* @__PURE__ */ t(
62
- "button",
63
- {
64
- type: "button",
65
- onClick: x,
66
- "aria-label": "Edit attachment",
67
- className: "flex size-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/15",
68
- children: /* @__PURE__ */ t(C, { className: "size-5", weight: "regular" })
69
- }
70
- ) : void 0, k = i ? /* @__PURE__ */ t(
71
- X,
72
- {
73
- variant: "dark",
74
- gallery: r,
75
- title: l,
76
- showLocked: m,
77
- topRight: v
78
- }
79
- ) : /* @__PURE__ */ t(
80
- q,
81
- {
82
- variant: "dark",
83
- mimeType: b,
84
- thumbnailUrl: N,
85
- title: l,
86
- source: c,
87
- showLocked: m,
88
- topRight: v
89
- }
90
- );
91
- return /* @__PURE__ */ y(P, { variant: "dark", children: [
92
- f ? (
93
- // Uses a `<div role="button">` rather than a native `<button>` so the
94
- // dismiss control rendered inside `thumbnail` (also a `<button>`)
95
- // doesn't produce invalid nested interactive elements.
96
- /* @__PURE__ */ t(
97
- "div",
98
- {
99
- role: "button",
100
- tabIndex: s ? -1 : 0,
101
- "aria-label": "Toggle preview",
102
- "aria-busy": s,
103
- "aria-pressed": !m,
104
- "aria-disabled": s || void 0,
105
- onClick: s ? void 0 : f,
106
- onKeyDown: (e) => {
107
- s || e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), f());
108
- },
109
- className: s ? "block w-full text-left" : "block w-full cursor-pointer text-left",
110
- children: k
111
- }
112
- )
113
- ) : k,
114
- /* @__PURE__ */ t(
115
- S,
116
- {
117
- variant: "dark",
118
- title: l,
119
- placeholderTitle: L,
120
- mimeType: b,
121
- detail: I,
122
- statusBadge: G,
123
- icon: i ? /* @__PURE__ */ t(A, { className: "size-5 shrink-0 text-white/55" }) : void 0,
124
- trailingAction: T
125
- }
126
- )
127
- ] });
128
- };
129
- export {
130
- Q as default
131
- };
132
- //# sourceMappingURL=Card-BRvcFjsq.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Card-BRvcFjsq.js","sources":["../src/components/LockedAttachment/components/Composer/Card.tsx"],"sourcesContent":["import { ImagesIcon, PencilSimpleIcon, XIcon } from '@phosphor-icons/react'\nimport React, { useCallback, useRef, useState } from 'react'\n\nimport type {\n LockedAttachmentBaseProps,\n LockedAttachmentSource,\n} from '../../types'\nimport CardBody from '../_shared/CardBody'\nimport GalleryThumbnail from '../_shared/GalleryThumbnail'\nimport LockedCardShell from '../_shared/LockedCardShell'\nimport LockedThumbnail from '../_shared/LockedThumbnail'\n\nexport interface ComposerCardProps extends LockedAttachmentBaseProps {\n /** Placeholder shown in the title slot before the composer types one. */\n placeholderTitle?: string\n /** Placeholder shown in the amount slot before one is configured. */\n placeholderAmountText?: string\n /**\n * When provided, renders a dismiss X in the thumbnail corner. Called when\n * the composer clicks it to remove the attachment.\n */\n onDismiss?: () => void\n /** Fired the first time the composer taps the thumbnail to preview. */\n onPreviewClick?: () => void\n /**\n * Lazily loads the underlying source so the composer can preview the\n * attachment they're about to send. Called the first time the thumbnail is\n * tapped; the returned source is cached and reused on subsequent toggles.\n */\n onFetchSource?: () => Promise<LockedAttachmentSource | void>\n /**\n * When provided, renders a pencil button in the body bottom-right that the\n * composer can use to edit the attachment metadata (e.g. open the price /\n * gallery editor). Matches the Composer \"Button\" instance in Figma.\n */\n onEditClick?: () => void\n}\n\n/**\n * The card the composer sees while drafting a paid attachment.\n * Matches the Composer column of the messaging design system in Figma.\n */\nconst ComposerCard: React.FC<ComposerCardProps> = ({\n title,\n mimeType = 'application/octet-stream',\n thumbnailUrl,\n detail,\n amountText,\n placeholderTitle = 'Attachment title',\n placeholderAmountText,\n gallery,\n onDismiss,\n onPreviewClick,\n onFetchSource,\n onEditClick,\n}) => {\n const [source, setSource] = useState<LockedAttachmentSource | undefined>()\n const [isPreviewVisible, setIsPreviewVisible] = useState(false)\n const [isLoadingPreview, setIsLoadingPreview] = useState(false)\n const fetchingRef = useRef(false)\n\n const isGallery = (gallery?.length ?? 0) >= 2\n\n const handleToggle = useCallback(async () => {\n onPreviewClick?.()\n if (isPreviewVisible) {\n setIsPreviewVisible(false)\n return\n }\n // Gallery items carry their own per-item sources on the `gallery` prop, so\n // we just flip visibility — no async source fetch is needed.\n if (isGallery) {\n setIsPreviewVisible(true)\n return\n }\n if (source) {\n setIsPreviewVisible(true)\n return\n }\n if (!onFetchSource) return\n if (fetchingRef.current) return\n fetchingRef.current = true\n setIsLoadingPreview(true)\n try {\n const result = await onFetchSource()\n if (result) {\n setSource(result)\n setIsPreviewVisible(true)\n }\n } finally {\n fetchingRef.current = false\n setIsLoadingPreview(false)\n }\n }, [isPreviewVisible, isGallery, source, onPreviewClick, onFetchSource])\n\n // Gallery is always previewable in the composer because each item's source\n // is already provided up-front via `gallery[*].sourceUrl`.\n const togglePreview =\n isGallery || onFetchSource || onPreviewClick ? handleToggle : undefined\n const showLocked = !isPreviewVisible\n const isBusy = isLoadingPreview\n\n const statusBadge = (\n <React.Fragment>\n <span className=\"text-xs font-medium text-white/55\">&bull;</span>\n <span\n className={\n amountText\n ? 'text-xs font-medium text-white/55'\n : 'text-xs font-medium text-white/30'\n }\n >\n {amountText || placeholderAmountText}\n </span>\n </React.Fragment>\n )\n\n const dismissButton = onDismiss ? (\n <button\n type=\"button\"\n onClick={(e) => {\n // Stop the click from bubbling up into the outer preview-toggle\n // wrapper when both are wired up at the same time.\n e.stopPropagation()\n onDismiss()\n }}\n className=\"flex size-6 items-center justify-center rounded-full bg-[#121110] text-white\"\n aria-label=\"Dismiss attachment\"\n >\n <XIcon className=\"size-3\" weight=\"bold\" />\n </button>\n ) : undefined\n\n const editButton = onEditClick ? (\n <button\n type=\"button\"\n onClick={onEditClick}\n aria-label=\"Edit attachment\"\n className=\"flex size-10 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/15\"\n >\n <PencilSimpleIcon className=\"size-5\" weight=\"regular\" />\n </button>\n ) : undefined\n\n const thumbnail = isGallery ? (\n <GalleryThumbnail\n variant=\"dark\"\n gallery={gallery!}\n title={title}\n showLocked={showLocked}\n topRight={dismissButton}\n />\n ) : (\n <LockedThumbnail\n variant=\"dark\"\n mimeType={mimeType}\n thumbnailUrl={thumbnailUrl}\n title={title}\n source={source}\n showLocked={showLocked}\n topRight={dismissButton}\n />\n )\n\n return (\n <LockedCardShell variant=\"dark\">\n {togglePreview ? (\n // Uses a `<div role=\"button\">` rather than a native `<button>` so the\n // dismiss control rendered inside `thumbnail` (also a `<button>`)\n // doesn't produce invalid nested interactive elements.\n <div\n role=\"button\"\n tabIndex={isBusy ? -1 : 0}\n aria-label=\"Toggle preview\"\n aria-busy={isBusy}\n aria-pressed={!showLocked}\n aria-disabled={isBusy || undefined}\n onClick={isBusy ? undefined : togglePreview}\n onKeyDown={(e) => {\n if (isBusy) return\n // Only handle keys that originate on the wrapper itself, not on\n // inner interactive elements (dismiss X, carousel arrows). Without\n // this guard, pressing Enter/Space on a focused child would bubble\n // up and toggle the preview in addition to activating the child.\n if (e.target !== e.currentTarget) return\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n void togglePreview()\n }\n }}\n className={\n !isBusy\n ? 'block w-full cursor-pointer text-left'\n : 'block w-full text-left'\n }\n >\n {thumbnail}\n </div>\n ) : (\n thumbnail\n )}\n\n <CardBody\n variant=\"dark\"\n title={title}\n placeholderTitle={placeholderTitle}\n mimeType={mimeType}\n detail={detail}\n statusBadge={statusBadge}\n icon={\n isGallery ? (\n <ImagesIcon className=\"size-5 shrink-0 text-white/55\" />\n ) : undefined\n }\n trailingAction={editButton}\n />\n </LockedCardShell>\n )\n}\n\nexport default ComposerCard\n"],"names":["ComposerCard","title","mimeType","thumbnailUrl","detail","amountText","placeholderTitle","placeholderAmountText","gallery","onDismiss","onPreviewClick","onFetchSource","onEditClick","source","setSource","useState","isPreviewVisible","setIsPreviewVisible","isLoadingPreview","setIsLoadingPreview","fetchingRef","useRef","isGallery","handleToggle","useCallback","result","togglePreview","showLocked","isBusy","statusBadge","jsxs","React","jsx","dismissButton","XIcon","editButton","PencilSimpleIcon","thumbnail","GalleryThumbnail","LockedThumbnail","LockedCardShell","CardBody","ImagesIcon"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),d=require("@phosphor-icons/react"),t=require("react");require("classnames");const T=require("./index-DRurVQ0-.cjs"),j=require("./LockedThumbnail-uAauUd5c.cjs"),p="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none text-white hover:bg-[#2a2928] disabled:opacity-70",U=({isLocked:i,sourceUrl:u,redeemUrl:g,onUnlockClicked:l,onDownloadClicked:o,isUnlocking:r=!1})=>{if(i&&l!=null)return e.jsx("button",{type:"button",onClick:l,disabled:r,className:p,children:r?e.jsx("span",{className:"size-4 animate-spin rounded-full border-2 border-white/30 border-t-white"}):e.jsxs(t.Fragment,{children:[e.jsx(d.LockSimpleIcon,{className:"size-4",weight:"fill"}),"Unlock"]})});if(!i&&o!=null){const c=g??u;return c!=null?e.jsxs("a",{href:c,target:"_blank",rel:"noopener noreferrer",onClick:o,className:`${p} !text-white`,children:[e.jsx(d.DownloadSimpleIcon,{className:"size-4",weight:"bold"}),"Download"]}):e.jsxs("button",{type:"button",onClick:o,className:p,children:[e.jsx(d.DownloadSimpleIcon,{className:"size-4",weight:"bold"}),"Download"]})}return null},B=({title:i,amountText:u,thumbnailUrl:g,mimeType:l="application/octet-stream",detail:o,gallery:r,onUnlockClick:c,onFetchSource:N,onDownloadClick:I,paymentStatus:n,isUnlocking:C=!1})=>{const f=((r==null?void 0:r.length)??0)>=2,[s,q]=t.useState(),v=t.useRef(null),w=t.useRef(!1),x=t.useRef(N);x.current=N;const k=s==null?void 0:s.sourceUrl,z=s==null?void 0:s.redeemUrl,h=t.useCallback(async()=>{var a;if(!w.current){w.current=!0;try{const b=await((a=x.current)==null?void 0:a.call(x));b&&q(b)}finally{w.current=!1}}},[]),L=t.useCallback(()=>{n==="paid"?h():c==null||c()},[n,h,c]);t.useEffect(()=>{if(!v.current||n!=="paid"||s!==void 0)return;const a=new IntersectionObserver(([b])=>{b.isIntersecting&&(h(),a.disconnect())},{threshold:1});return a.observe(v.current),()=>a.disconnect()},[n,s,h]);const m=f?n!=="paid":k===void 0,R=T.getSourceType(l),D=n==="paid"?e.jsxs(t.Fragment,{children:[e.jsx("span",{className:"text-xs font-medium text-black/55",children:"•"}),e.jsx("span",{className:"text-xs font-medium text-[#008236]",children:"Purchased"}),e.jsx(d.CheckCircleIcon,{className:"size-4 text-[#008236]",weight:"bold"})]}):u!=null?e.jsxs(t.Fragment,{children:[e.jsx("span",{className:"text-xs font-medium text-black/55",children:"•"}),e.jsx("span",{className:"text-xs font-medium text-black/55",children:u})]}):null,S=f?void 0:k;return e.jsxs(j.LockedCardShell,{variant:"light",rootRef:v,"data-testid":"locked-attachment",children:[f?e.jsx(j.GalleryThumbnail,{variant:"light",gallery:r,title:i,showLocked:m,paymentStatus:n}):e.jsx(j.LockedThumbnail,{variant:"light",mimeType:l,thumbnailUrl:g,title:i,source:s,showLocked:m,paymentStatus:n,containedImage:!m&&(R==="image"||R==="document")}),e.jsx(j.CardBody,{variant:"light",title:i,mimeType:l,detail:o,statusBadge:D,icon:f?e.jsx(d.ImagesIcon,{className:"size-5 shrink-0 text-black/55"}):void 0,action:e.jsx(U,{isLocked:m,isUnlocking:C,sourceUrl:S,redeemUrl:z,onUnlockClicked:L,onDownloadClicked:I})})]})};exports.default=B;
2
- //# sourceMappingURL=Card-BuXkok0A.cjs.map