@linktr.ee/messaging-react 2.0.1 → 2.1.0-rc-1778695491

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 (64) hide show
  1. package/dist/Card-CFFNq49v.js +163 -0
  2. package/dist/Card-CFFNq49v.js.map +1 -0
  3. package/dist/Card-CsJvUF_b.js +107 -0
  4. package/dist/Card-CsJvUF_b.js.map +1 -0
  5. package/dist/Card-D32U6KfZ.js +85 -0
  6. package/dist/Card-D32U6KfZ.js.map +1 -0
  7. package/dist/Card-DlMSDSdm.js +132 -0
  8. package/dist/Card-DlMSDSdm.js.map +1 -0
  9. package/dist/Card-DlSSJPip.js +60 -0
  10. package/dist/Card-DlSSJPip.js.map +1 -0
  11. package/dist/Card-zGbhRBwv.js +48 -0
  12. package/dist/Card-zGbhRBwv.js.map +1 -0
  13. package/dist/CardThumbnail-DTBuRQHF.js +239 -0
  14. package/dist/CardThumbnail-DTBuRQHF.js.map +1 -0
  15. package/dist/LockedThumbnail-DpJx169C.js +220 -0
  16. package/dist/LockedThumbnail-DpJx169C.js.map +1 -0
  17. package/dist/{index-Bex7eg3v.js → index-DfcRe-Hj.js} +618 -607
  18. package/dist/index-DfcRe-Hj.js.map +1 -0
  19. package/dist/index.d.ts +217 -28
  20. package/dist/index.js +16 -15
  21. package/package.json +1 -1
  22. package/src/components/CustomMessage/index.tsx +2 -3
  23. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +307 -0
  24. package/src/components/LinkAttachment/components/Composer/Card.tsx +117 -0
  25. package/src/components/LinkAttachment/components/Composer/index.ts +2 -0
  26. package/src/components/LinkAttachment/components/Received/Card.tsx +132 -0
  27. package/src/components/LinkAttachment/components/Received/index.ts +2 -0
  28. package/src/components/LinkAttachment/components/Sent/Card.tsx +57 -0
  29. package/src/components/LinkAttachment/components/Sent/index.ts +2 -0
  30. package/src/components/LinkAttachment/components/_shared/CardBody.tsx +117 -0
  31. package/src/components/LinkAttachment/components/_shared/CardCta.tsx +69 -0
  32. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +120 -0
  33. package/src/components/LinkAttachment/components/_shared/CardThumbnail.tsx +156 -0
  34. package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.ts +56 -0
  35. package/src/components/LinkAttachment/index.tsx +68 -0
  36. package/src/components/LinkAttachment/types.ts +69 -0
  37. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +230 -89
  38. package/src/components/LockedAttachment/components/Composer/Card.tsx +221 -0
  39. package/src/components/LockedAttachment/components/Composer/index.ts +2 -0
  40. package/src/components/LockedAttachment/components/Received/Card.tsx +191 -0
  41. package/src/components/LockedAttachment/components/Received/CardActions.tsx +91 -0
  42. package/src/components/LockedAttachment/components/Received/index.ts +2 -0
  43. package/src/components/LockedAttachment/components/Sent/Card.tsx +177 -0
  44. package/src/components/LockedAttachment/components/Sent/index.ts +2 -0
  45. package/src/components/LockedAttachment/components/_shared/CardBody.tsx +94 -0
  46. package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +178 -0
  47. package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
  48. package/src/components/LockedAttachment/components/_shared/LockedCardShell.tsx +36 -0
  49. package/src/components/LockedAttachment/components/_shared/LockedThumbnail.tsx +128 -0
  50. package/src/components/LockedAttachment/index.tsx +43 -12
  51. package/src/components/LockedAttachment/types.ts +17 -0
  52. package/src/components/MediaMessage/index.tsx +8 -2
  53. package/src/index.ts +15 -1
  54. package/dist/Card-BKP9ml9O.js +0 -138
  55. package/dist/Card-BKP9ml9O.js.map +0 -1
  56. package/dist/Card-Bk_4lVzP.js +0 -127
  57. package/dist/Card-Bk_4lVzP.js.map +0 -1
  58. package/dist/index-Bex7eg3v.js.map +0 -1
  59. package/src/components/LockedAttachment/components/Creator/Card.tsx +0 -210
  60. package/src/components/LockedAttachment/components/Creator/index.tsx +0 -2
  61. package/src/components/LockedAttachment/components/Visitor/Card.tsx +0 -155
  62. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +0 -62
  63. package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +0 -12
  64. package/src/components/LockedAttachment/components/Visitor/index.ts +0 -2
@@ -1,210 +0,0 @@
1
- import {
2
- CheckCircleIcon,
3
- EyeIcon,
4
- EyeSlashIcon,
5
- LockIcon,
6
- LockOpenIcon,
7
- XIcon,
8
- } from '@phosphor-icons/react'
9
- import classNames from 'classnames'
10
- import React, { useCallback, useRef, useState } from 'react'
11
-
12
- import AttachmentCard, { AttachmentThumbnail } from '../../../AttachmentCard'
13
- import type {
14
- LockedAttachmentBaseProps,
15
- LockedAttachmentSource,
16
- PaymentStatus,
17
- } from '../../types'
18
-
19
- export interface CreatorCardProps extends LockedAttachmentBaseProps {
20
- placeholderTitle?: string
21
- placeholderAmountText?: string
22
- isUnlocking?: boolean
23
- onDismiss?: () => void
24
- onPreviewClick?: () => void
25
- onFetchSource?: () => Promise<LockedAttachmentSource | void>
26
- }
27
-
28
- function headerSlots(props: {
29
- onDismiss?: () => void
30
- onToggle?: () => void
31
- isExpanded?: boolean
32
- paymentStatus?: PaymentStatus
33
- isLoading?: boolean
34
- }): { topLeft?: React.ReactNode; topRight?: React.ReactNode } {
35
- const { onDismiss, onToggle, isExpanded, paymentStatus, isLoading } = props
36
-
37
- if (onDismiss) {
38
- return {
39
- topLeft: undefined,
40
- topRight: (
41
- <button
42
- type="button"
43
- onClick={onDismiss}
44
- className="flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
45
- aria-label="Dismiss attachment"
46
- >
47
- <XIcon className="size-4" weight="bold" />
48
- </button>
49
- ),
50
- }
51
- }
52
-
53
- if (onToggle) {
54
- const Icon = isExpanded ? EyeIcon : EyeSlashIcon
55
- return {
56
- topLeft: (
57
- <button
58
- type="button"
59
- onClick={onToggle}
60
- className="flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
61
- aria-label={isExpanded ? 'Hide preview' : 'Show preview'}
62
- aria-pressed={isExpanded}
63
- >
64
- <Icon className="size-4" weight="fill" />
65
- </button>
66
- ),
67
- topRight: undefined,
68
- }
69
- }
70
-
71
- const Icon = paymentStatus === 'paid' ? LockOpenIcon : LockIcon
72
-
73
- return {
74
- topLeft: (
75
- <div className="flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
76
- {isLoading ? (
77
- <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
78
- ) : (
79
- <Icon className="size-4" weight="fill" />
80
- )}
81
- </div>
82
- ),
83
- topRight: undefined,
84
- }
85
- }
86
-
87
- const CreatorCard: React.FC<CreatorCardProps> = ({
88
- title,
89
- mimeType = 'application/octet-stream',
90
- thumbnailUrl,
91
- detail,
92
- amountText,
93
- placeholderTitle = 'Attachment title',
94
- placeholderAmountText,
95
- paymentStatus,
96
- isUnlocking,
97
- onDismiss,
98
- onPreviewClick,
99
- onFetchSource,
100
- }) => {
101
- const [source, setSource] = useState<LockedAttachmentSource | undefined>()
102
- const [isPreviewVisible, setIsPreviewVisible] = useState(false)
103
- const [isLoadingPreview, setLoadingPreview] = useState(false)
104
- const fetchingRef = useRef(false)
105
-
106
- const effectiveSourceUrl = isPreviewVisible ? source?.sourceUrl : undefined
107
- const effectiveThumbnailUrl = isPreviewVisible ? (source?.thumbnailUrl ?? thumbnailUrl) : thumbnailUrl
108
- const isBusy = isLoadingPreview || isUnlocking
109
-
110
- const handleToggle = useCallback(async () => {
111
- onPreviewClick?.()
112
- if (isPreviewVisible) {
113
- setIsPreviewVisible(false)
114
- return
115
- }
116
- if (source) {
117
- setIsPreviewVisible(true)
118
- return
119
- }
120
- if (!onFetchSource) return
121
- if (fetchingRef.current) return
122
- fetchingRef.current = true
123
- setLoadingPreview(true)
124
- try {
125
- const result = await onFetchSource()
126
- if (result) {
127
- setSource(result)
128
- setIsPreviewVisible(true)
129
- }
130
- } finally {
131
- fetchingRef.current = false
132
- setLoadingPreview(false)
133
- }
134
- }, [isPreviewVisible, source, onPreviewClick, onFetchSource])
135
-
136
- const toggleHandler = onFetchSource || onPreviewClick ? handleToggle : undefined
137
-
138
- const statusBadge =
139
- paymentStatus === 'paid' ? (
140
- <React.Fragment>
141
- <span className="text-xs font-medium text-white/55">&bull;</span>
142
- <span className="text-xs font-medium text-[#34c759]">Sold</span>
143
- <CheckCircleIcon className="size-4 text-[#34c759]" weight="bold" />
144
- </React.Fragment>
145
- ) : (
146
- <React.Fragment>
147
- <span className="text-xs font-medium text-white/55">&bull;</span>
148
- <span
149
- className={classNames('text-xs font-medium', {
150
- 'text-white/30': !amountText,
151
- 'text-white/55': !!amountText,
152
- })}
153
- >
154
- {amountText || placeholderAmountText}
155
- </span>
156
- </React.Fragment>
157
- )
158
-
159
- const { topLeft, topRight } = headerSlots({
160
- onDismiss,
161
- onToggle: isBusy ? undefined : toggleHandler,
162
- isExpanded: isPreviewVisible,
163
- paymentStatus,
164
- isLoading: isBusy,
165
- })
166
-
167
- return (
168
- <AttachmentCard
169
- variant="dark"
170
- title={title}
171
- placeholderTitle={placeholderTitle}
172
- mimeType={mimeType}
173
- detail={detail}
174
- statusBadge={statusBadge}
175
- topLeft={topLeft}
176
- topRight={topRight}
177
- thumbnail={
178
- <button
179
- type="button"
180
- disabled={!toggleHandler || isBusy}
181
- className={classNames(
182
- 'relative block w-full overflow-hidden border-0 bg-white/10 p-0 text-left appearance-none',
183
- { 'cursor-pointer': !!toggleHandler && !isBusy, 'cursor-default': !toggleHandler || isBusy }
184
- )}
185
- onClick={isBusy ? undefined : toggleHandler}
186
- aria-label={toggleHandler ? 'Toggle preview' : undefined}
187
- aria-busy={isBusy}
188
- >
189
- <AttachmentThumbnail
190
- mimeType={mimeType}
191
- sourceUrl={effectiveSourceUrl}
192
- thumbnailUrl={effectiveThumbnailUrl}
193
- title={title}
194
- variant="dark"
195
- mediaPlayerProps={
196
- effectiveSourceUrl
197
- ? { autoPlay: true, loop: true, controls: true, muted: false }
198
- : undefined
199
- }
200
- />
201
- {!isPreviewVisible && (
202
- <div className="pointer-events-none absolute inset-0 bg-black/30" />
203
- )}
204
- </button>
205
- }
206
- />
207
- )
208
- }
209
-
210
- export default CreatorCard
@@ -1,2 +0,0 @@
1
- export { default as CreatorCard } from './Card'
2
- export type { CreatorCardProps } from './Card'
@@ -1,155 +0,0 @@
1
- import { CheckCircleIcon } from '@phosphor-icons/react'
2
- import React, { useCallback, useEffect, useRef, useState } from 'react'
3
-
4
- import AttachmentCard, { AttachmentThumbnail } from '../../../AttachmentCard'
5
- import type {
6
- LockedAttachmentBaseProps,
7
- LockedAttachmentSource,
8
- } from '../../types'
9
-
10
- import CardActions from './CardActions'
11
- import { LockBadge } from './LockBadge'
12
-
13
- export interface VisitorCardProps extends LockedAttachmentBaseProps {
14
- /**
15
- * Called when the visitor clicks Unlock on an unpaid attachment.
16
- * Use this to open a checkout flow. Omit to hide the Unlock button.
17
- */
18
- onUnlockClick?: () => void
19
- /**
20
- * Called to fetch the attachment source — fired automatically when
21
- * paymentStatus transitions to 'paid', or immediately on click when
22
- * paymentStatus is already 'paid'. Return a LockedAttachmentSource to
23
- * unlock the card.
24
- */
25
- onFetchSource?: () => Promise<LockedAttachmentSource | void>
26
- /**
27
- * Called when the visitor clicks Download on an unlocked card.
28
- * Omit to hide the Download button.
29
- */
30
- onDownloadClick?: () => void
31
- /**
32
- * When true, shows loading dots on the Unlock button.
33
- * Driven by the LockedAttachmentContext (e.g. checkout in progress, payment processing).
34
- */
35
- isUnlocking?: boolean
36
- }
37
-
38
- const VisitorCard: React.FC<VisitorCardProps> = ({
39
- title,
40
- amountText,
41
- thumbnailUrl,
42
- mimeType = 'application/octet-stream',
43
- detail,
44
- onUnlockClick,
45
- onFetchSource,
46
- onDownloadClick,
47
- paymentStatus,
48
- isUnlocking = false,
49
- }) => {
50
- const [source, setSource] = useState<LockedAttachmentSource | undefined>()
51
-
52
- const cardRef = useRef<HTMLDivElement>(null)
53
- const fetchingRef = useRef(false)
54
-
55
- const onFetchSourceRef = useRef(onFetchSource)
56
- onFetchSourceRef.current = onFetchSource
57
-
58
- const effectiveSourceUrl = source?.sourceUrl
59
- const effectiveThumbnail = source?.thumbnailUrl ?? thumbnailUrl
60
- const effectiveRedeemUrl = source?.redeemUrl
61
-
62
- const fetchSource = useCallback(async (): Promise<void> => {
63
- if (fetchingRef.current) return
64
- fetchingRef.current = true
65
- try {
66
- const result = await onFetchSourceRef.current?.()
67
- if (result) setSource(result)
68
- } finally {
69
- fetchingRef.current = false
70
- }
71
- }, [])
72
-
73
- const handleUnlockClick = useCallback(() => {
74
- if (paymentStatus === 'paid') {
75
- void fetchSource()
76
- } else {
77
- onUnlockClick?.()
78
- }
79
- }, [paymentStatus, fetchSource, onUnlockClick])
80
-
81
- // Fetch source when card is in viewport
82
- useEffect(() => {
83
- if (!cardRef.current) return
84
- if (paymentStatus !== 'paid' || source !== undefined) return
85
-
86
- const observer = new IntersectionObserver(
87
- ([entry]) => {
88
- if (entry.isIntersecting) {
89
- void fetchSource()
90
- observer.disconnect()
91
- }
92
- },
93
- { threshold: 1.0 }
94
- )
95
-
96
- observer.observe(cardRef.current)
97
- return () => observer.disconnect()
98
- }, [paymentStatus, source, fetchSource])
99
-
100
- const isLocked = effectiveSourceUrl === undefined
101
-
102
- const statusBadge =
103
- paymentStatus === 'paid' ? (
104
- <React.Fragment>
105
- <span className="text-xs font-medium text-black/55">&bull;</span>
106
- <span className="text-xs font-medium text-[#008236]">Purchased</span>
107
- <CheckCircleIcon className="size-4 text-[#008236]" weight="bold" />
108
- </React.Fragment>
109
- ) : amountText != null ? (
110
- <React.Fragment>
111
- <span className="text-xs font-medium text-black/55">&bull;</span>
112
- <span className="text-xs font-medium text-black/55">{amountText}</span>
113
- </React.Fragment>
114
- ) : null
115
-
116
- return (
117
- <AttachmentCard
118
- variant="light"
119
- rootRef={cardRef}
120
- data-testid="locked-attachment"
121
- title={title}
122
- mimeType={mimeType}
123
- detail={detail}
124
- statusBadge={statusBadge}
125
- thumbnail={
126
- <div className="relative">
127
- <AttachmentThumbnail
128
- mimeType={mimeType}
129
- sourceUrl={effectiveSourceUrl}
130
- thumbnailUrl={effectiveThumbnail}
131
- title={title}
132
- variant="light"
133
- containedImage={!isLocked}
134
- />
135
- {isLocked && (
136
- <div className="pointer-events-none absolute inset-0 bg-black/30">
137
- <LockBadge paymentStatus={paymentStatus} />
138
- </div>
139
- )}
140
- </div>
141
- }
142
- action={
143
- <CardActions
144
- isUnlocking={isUnlocking}
145
- sourceUrl={effectiveSourceUrl}
146
- redeemUrl={effectiveRedeemUrl}
147
- onUnlockClicked={handleUnlockClick}
148
- onDownloadClicked={onDownloadClick}
149
- />
150
- }
151
- />
152
- )
153
- }
154
-
155
- export default VisitorCard
@@ -1,62 +0,0 @@
1
- import { DownloadSimpleIcon, LockSimpleIcon } from '@phosphor-icons/react'
2
- import React from 'react'
3
-
4
- interface CardActionsProps {
5
- sourceUrl?: string
6
- redeemUrl?: string
7
- onUnlockClicked?: () => void
8
- onDownloadClicked?: () => void
9
- isUnlocking?: boolean
10
- }
11
-
12
- const CardActions: React.FC<CardActionsProps> = (props) => {
13
- const {
14
- isUnlocking = false,
15
- sourceUrl,
16
- redeemUrl,
17
- onUnlockClicked,
18
- onDownloadClicked,
19
- } = props
20
-
21
- const isLocked = sourceUrl === undefined
22
-
23
- if (isLocked && onUnlockClicked != null) {
24
- return (
25
- <button
26
- type="button"
27
- onClick={onUnlockClicked}
28
- disabled={isUnlocking}
29
- className="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"
30
- >
31
- {isUnlocking ? (
32
- <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
33
- ) : (
34
- <React.Fragment>
35
- <LockSimpleIcon className="size-4" weight="fill" />
36
- Unlock
37
- </React.Fragment>
38
- )}
39
- </button>
40
- )
41
- }
42
-
43
- if (!isLocked && onDownloadClicked != null && sourceUrl != null) {
44
- return (
45
- <a
46
- href={redeemUrl ?? sourceUrl}
47
- target="_blank"
48
- rel="noopener noreferrer"
49
- onClick={onDownloadClicked}
50
- className="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]"
51
- >
52
- <DownloadSimpleIcon className="size-4" weight="bold" />
53
- Download
54
- </a>
55
- )
56
- }
57
-
58
- return null
59
- }
60
-
61
-
62
- export default CardActions
@@ -1,12 +0,0 @@
1
- import { LockOpenIcon, LockSimpleIcon } from '@phosphor-icons/react'
2
- import React from 'react'
3
-
4
- import type { PaymentStatus } from '../../types'
5
-
6
- export const LockBadge: React.FC<{ paymentStatus?: PaymentStatus }> = ({
7
- paymentStatus,
8
- }) => (
9
- <div className="absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
10
- {paymentStatus === 'paid' ? <LockOpenIcon /> : <LockSimpleIcon />}
11
- </div>
12
- )
@@ -1,2 +0,0 @@
1
- export { default as VisitorCard } from './Card'
2
- export type { VisitorCardProps } from './Card'