@linktr.ee/messaging-react 1.40.2 → 2.0.1-rc-1778656305
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-CAC3fPjy.js +107 -0
- package/dist/Card-CAC3fPjy.js.map +1 -0
- package/dist/Card-DLUBUg_w.js +132 -0
- package/dist/Card-DLUBUg_w.js.map +1 -0
- package/dist/Card-_StSlnYh.js +163 -0
- package/dist/Card-_StSlnYh.js.map +1 -0
- package/dist/LockedThumbnail-p5RsFOug.js +220 -0
- package/dist/LockedThumbnail-p5RsFOug.js.map +1 -0
- package/dist/assets/index.css +1 -1
- package/dist/index-B1h46F9x.js +3092 -0
- package/dist/index-B1h46F9x.js.map +1 -0
- package/dist/index.d.ts +109 -30
- package/dist/index.js +14 -12
- package/package.json +2 -2
- package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +2 -14
- package/src/components/ChannelInfoDialog/index.tsx +4 -8
- package/src/components/ChannelList/ChannelListContext.tsx +2 -0
- package/src/components/ChannelList/CustomChannelPreview.tsx +14 -3
- package/src/components/ChannelList/index.tsx +9 -1
- package/src/components/ChannelView.test.tsx +11 -0
- package/src/components/ChannelView.tsx +44 -33
- package/src/components/CustomMessage/index.tsx +24 -7
- package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +57 -17
- package/src/components/CustomTypingIndicator/CustomTypingIndicator.test.tsx +187 -0
- package/src/components/CustomTypingIndicator/DmAgentContext.ts +3 -0
- package/src/components/CustomTypingIndicator/index.tsx +101 -37
- package/src/components/LockedAttachment/LockedAttachment.stories.tsx +230 -89
- 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/LockedAttachment/components/Received/CardActions.tsx +91 -0
- 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/LockedAttachment/components/_shared/CardBody.tsx +94 -0
- package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +178 -0
- package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
- package/src/components/LockedAttachment/components/_shared/LockedCardShell.tsx +36 -0
- package/src/components/LockedAttachment/components/_shared/LockedThumbnail.tsx +128 -0
- package/src/components/LockedAttachment/index.tsx +43 -12
- package/src/components/LockedAttachment/types.ts +17 -0
- package/src/components/MediaMessage/index.tsx +2 -2
- package/src/components/MessagingShell/index.tsx +4 -4
- package/src/index.ts +18 -2
- package/src/stories/mocks.tsx +2 -9
- package/src/styles.css +7 -0
- package/src/types.ts +11 -1
- package/src/utils/getMessageDisplayText.test.ts +44 -0
- package/src/utils/getMessageDisplayText.ts +27 -0
- package/dist/Card-A0lkei-S.js +0 -138
- package/dist/Card-A0lkei-S.js.map +0 -1
- package/dist/Card-DXoAKkv0.js +0 -127
- package/dist/Card-DXoAKkv0.js.map +0 -1
- package/dist/index-B_PLgcDi.js +0 -2994
- package/dist/index-B_PLgcDi.js.map +0 -1
- package/src/components/LockedAttachment/components/Creator/Card.tsx +0 -210
- package/src/components/LockedAttachment/components/Creator/index.tsx +0 -2
- package/src/components/LockedAttachment/components/Visitor/Card.tsx +0 -155
- package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +0 -62
- package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +0 -12
- package/src/components/LockedAttachment/components/Visitor/index.ts +0 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CaretLeftIcon,
|
|
3
|
+
CaretRightIcon,
|
|
4
|
+
LockOpenIcon,
|
|
5
|
+
LockSimpleIcon,
|
|
6
|
+
PlayIcon,
|
|
7
|
+
} from '@phosphor-icons/react'
|
|
8
|
+
import classNames from 'classnames'
|
|
9
|
+
import React, { useState } from 'react'
|
|
10
|
+
|
|
11
|
+
import { AttachmentThumbnail, getSourceType } from '../../../AttachmentCard'
|
|
12
|
+
import type {
|
|
13
|
+
LockedAttachmentGalleryItem,
|
|
14
|
+
PaymentStatus,
|
|
15
|
+
} from '../../types'
|
|
16
|
+
|
|
17
|
+
import type { LockedCardVariant } from './LockedCardShell'
|
|
18
|
+
|
|
19
|
+
export interface GalleryThumbnailProps {
|
|
20
|
+
variant: LockedCardVariant
|
|
21
|
+
gallery: LockedAttachmentGalleryItem[]
|
|
22
|
+
title?: string
|
|
23
|
+
/** When true, all items are shown blurred + the locked/play badge is overlaid. */
|
|
24
|
+
showLocked: boolean
|
|
25
|
+
paymentStatus?: PaymentStatus
|
|
26
|
+
/** Top-right slot (e.g. dismiss X on Composer). Rendered inside the thumbnail. */
|
|
27
|
+
topRight?: React.ReactNode
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isPlayableItem(item: LockedAttachmentGalleryItem): boolean {
|
|
31
|
+
const sourceType = getSourceType(item.mimeType)
|
|
32
|
+
return sourceType === 'video' || sourceType === 'audio'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mixed-media carousel used by every LockedAttachment card when its `gallery`
|
|
37
|
+
* prop has 2+ items. Matches the "Gallery" frames in the Figma design system:
|
|
38
|
+
* 337/386 aspect ratio, prev/next caret buttons, a counter pill, and a center
|
|
39
|
+
* lock / play indicator that swaps based on the current item.
|
|
40
|
+
*/
|
|
41
|
+
const GalleryThumbnail: React.FC<GalleryThumbnailProps> = ({
|
|
42
|
+
variant,
|
|
43
|
+
gallery,
|
|
44
|
+
title,
|
|
45
|
+
showLocked,
|
|
46
|
+
paymentStatus,
|
|
47
|
+
topRight,
|
|
48
|
+
}) => {
|
|
49
|
+
const [index, setIndex] = useState(0)
|
|
50
|
+
const total = gallery.length
|
|
51
|
+
const safeIndex = Math.min(index, total - 1)
|
|
52
|
+
const current = gallery[safeIndex]
|
|
53
|
+
const isVideoItem = isPlayableItem(current)
|
|
54
|
+
|
|
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
|
+
// Stop propagation so arrow taps don't bubble up to the outer preview-toggle
|
|
72
|
+
// wrapper on ComposerCard / SentCard.
|
|
73
|
+
const goPrev = (e: React.MouseEvent) => {
|
|
74
|
+
e.stopPropagation()
|
|
75
|
+
setIndex((i) => Math.max(0, i - 1))
|
|
76
|
+
}
|
|
77
|
+
const goNext = (e: React.MouseEvent) => {
|
|
78
|
+
e.stopPropagation()
|
|
79
|
+
setIndex((i) => Math.min(total - 1, i + 1))
|
|
80
|
+
}
|
|
81
|
+
const atStart = safeIndex === 0
|
|
82
|
+
const atEnd = safeIndex === total - 1
|
|
83
|
+
|
|
84
|
+
const isDark = variant === 'dark'
|
|
85
|
+
|
|
86
|
+
// Locked + unlocked render entirely separate trees so the locked state
|
|
87
|
+
// doesn't end up stacking a blurred `<img>` on top of an underlying
|
|
88
|
+
// `AttachmentThumbnail` (whose `bg-white/10` poster shell otherwise
|
|
89
|
+
// shows through as a visible horizontal seam at the 16:9 boundary
|
|
90
|
+
// inside the taller 337/386 gallery aspect).
|
|
91
|
+
//
|
|
92
|
+
// The locked branch uses `filter: blur` on a single full-size `<img>`
|
|
93
|
+
// — not `backdrop-filter` — so the rounded corners are clipped
|
|
94
|
+
// normally without any clip-path workarounds (see LockedThumbnail).
|
|
95
|
+
return (
|
|
96
|
+
<div className="relative aspect-[337/386] overflow-hidden rounded-t-2xl">
|
|
97
|
+
{showLocked ? (
|
|
98
|
+
<div
|
|
99
|
+
className={classNames(
|
|
100
|
+
'absolute inset-0',
|
|
101
|
+
isDark ? 'bg-white/10' : 'bg-black/5'
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
{current.thumbnailUrl && (
|
|
105
|
+
<img
|
|
106
|
+
src={current.thumbnailUrl}
|
|
107
|
+
alt=""
|
|
108
|
+
aria-hidden
|
|
109
|
+
draggable={false}
|
|
110
|
+
// `scale-110` hides the fuzzy edge that `filter: blur` leaves
|
|
111
|
+
// around the image bounds.
|
|
112
|
+
className="absolute inset-0 h-full w-full scale-110 object-cover blur-[40px]"
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
) : (
|
|
117
|
+
<AttachmentThumbnail
|
|
118
|
+
mimeType={current.mimeType}
|
|
119
|
+
sourceUrl={current.sourceUrl}
|
|
120
|
+
thumbnailUrl={current.thumbnailUrl}
|
|
121
|
+
title={title}
|
|
122
|
+
variant={variant}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{centerBadge && (
|
|
127
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
128
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-black/25 text-white">
|
|
129
|
+
{centerBadge}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{total > 1 && (
|
|
135
|
+
<React.Fragment>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={goPrev}
|
|
139
|
+
disabled={atStart}
|
|
140
|
+
aria-label="Previous item"
|
|
141
|
+
className={classNames(
|
|
142
|
+
'absolute left-3 top-1/2 -translate-y-1/2 flex size-7 items-center justify-center rounded-full bg-black/30 text-white transition-opacity',
|
|
143
|
+
atStart ? 'opacity-40' : 'hover:bg-black/40'
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<CaretLeftIcon className="size-5" weight="bold" />
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={goNext}
|
|
151
|
+
disabled={atEnd}
|
|
152
|
+
aria-label="Next item"
|
|
153
|
+
className={classNames(
|
|
154
|
+
'absolute right-3 top-1/2 -translate-y-1/2 flex size-7 items-center justify-center rounded-full bg-black/30 text-white transition-opacity',
|
|
155
|
+
atEnd ? 'opacity-40' : 'hover:bg-black/40'
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
<CaretRightIcon className="size-5" weight="bold" />
|
|
159
|
+
</button>
|
|
160
|
+
|
|
161
|
+
<div className="pointer-events-none absolute bottom-5 left-1/2 -translate-x-1/2 rounded-lg bg-black/30 px-2 py-1">
|
|
162
|
+
<span className="text-xs font-medium leading-4 text-white">
|
|
163
|
+
{safeIndex + 1}/{total}
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
</React.Fragment>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{topRight && (
|
|
170
|
+
<div className="pointer-events-auto absolute right-3 top-3 z-10">
|
|
171
|
+
{topRight}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default GalleryThumbnail
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LockOpenIcon,
|
|
3
|
+
LockSimpleIcon,
|
|
4
|
+
PlayIcon,
|
|
5
|
+
} from '@phosphor-icons/react'
|
|
6
|
+
import React from 'react'
|
|
7
|
+
|
|
8
|
+
import { getSourceType } from '../../../AttachmentCard'
|
|
9
|
+
import type { PaymentStatus } from '../../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Centered 48x48 lock badge that overlays the thumbnail of any locked attachment.
|
|
13
|
+
* Uses `bg-black/25` from the design system's `alpha-black-25` token.
|
|
14
|
+
*
|
|
15
|
+
* For playable media (video/audio), the badge shows a play icon instead of a
|
|
16
|
+
* lock — matching the convention used by `GalleryThumbnail`.
|
|
17
|
+
*/
|
|
18
|
+
export const LockBadge: React.FC<{
|
|
19
|
+
paymentStatus?: PaymentStatus
|
|
20
|
+
mimeType?: string
|
|
21
|
+
}> = ({ paymentStatus, mimeType }) => {
|
|
22
|
+
const sourceType = mimeType ? getSourceType(mimeType) : undefined
|
|
23
|
+
const isPlayable = sourceType === 'video' || sourceType === 'audio'
|
|
24
|
+
|
|
25
|
+
const Icon = isPlayable
|
|
26
|
+
? PlayIcon
|
|
27
|
+
: paymentStatus === 'paid'
|
|
28
|
+
? LockOpenIcon
|
|
29
|
+
: LockSimpleIcon
|
|
30
|
+
return (
|
|
31
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
32
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-black/25 text-white">
|
|
33
|
+
<Icon className="size-6" weight="fill" />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default LockBadge
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export type LockedCardVariant = 'dark' | 'light'
|
|
5
|
+
|
|
6
|
+
export interface LockedCardShellProps {
|
|
7
|
+
variant: LockedCardVariant
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
rootRef?: React.Ref<HTMLDivElement>
|
|
10
|
+
'data-testid'?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Outer chrome for every LockedAttachment card.
|
|
15
|
+
* Mirrors the `shadow-400` + 16px radius treatment from the Figma design system.
|
|
16
|
+
*/
|
|
17
|
+
const LockedCardShell: React.FC<LockedCardShellProps> = ({
|
|
18
|
+
variant,
|
|
19
|
+
children,
|
|
20
|
+
rootRef,
|
|
21
|
+
'data-testid': dataTestId,
|
|
22
|
+
}) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={rootRef}
|
|
25
|
+
data-testid={dataTestId}
|
|
26
|
+
className={classNames(
|
|
27
|
+
'relative w-[280px] select-none overflow-hidden rounded-2xl',
|
|
28
|
+
'shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.04),0_8px_32px_rgba(0,0,0,0.1)]',
|
|
29
|
+
variant === 'dark' ? 'bg-[#121110]' : 'bg-white'
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export default LockedCardShell
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AttachmentThumbnail,
|
|
6
|
+
renderTypeIcon,
|
|
7
|
+
} from '../../../AttachmentCard'
|
|
8
|
+
import type { LockedAttachmentSource, PaymentStatus } from '../../types'
|
|
9
|
+
|
|
10
|
+
import { LockBadge } from './LockBadge'
|
|
11
|
+
import type { LockedCardVariant } from './LockedCardShell'
|
|
12
|
+
|
|
13
|
+
export interface LockedThumbnailProps {
|
|
14
|
+
variant: LockedCardVariant
|
|
15
|
+
mimeType: string
|
|
16
|
+
thumbnailUrl?: string
|
|
17
|
+
title?: string
|
|
18
|
+
/**
|
|
19
|
+
* When provided, the underlying source is shown unblurred (preview / unlocked state).
|
|
20
|
+
* When undefined, the thumbnail is treated as locked: blurred with a central lock badge.
|
|
21
|
+
*/
|
|
22
|
+
source?: LockedAttachmentSource
|
|
23
|
+
/**
|
|
24
|
+
* Forces the locked appearance even when a source is provided. Used for the
|
|
25
|
+
* Composer card where the locked treatment is the default and the user toggles
|
|
26
|
+
* a preview in place.
|
|
27
|
+
*/
|
|
28
|
+
showLocked: boolean
|
|
29
|
+
paymentStatus?: PaymentStatus
|
|
30
|
+
/** Top-right overlay (e.g. dismiss X on Composer). Rendered inside the thumbnail. */
|
|
31
|
+
topRight?: React.ReactNode
|
|
32
|
+
/** When true, renders the unlocked image with aspect-video + object-contain. */
|
|
33
|
+
containedImage?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Renders the 180px-tall media area for a LockedAttachment card.
|
|
38
|
+
* Handles three visual states:
|
|
39
|
+
* - locked: blurred thumbnail + centered 48px lock badge
|
|
40
|
+
* - preview/paid: clear thumbnail (the underlying source)
|
|
41
|
+
* - empty: type-icon placeholder
|
|
42
|
+
*/
|
|
43
|
+
const LockedThumbnail: React.FC<LockedThumbnailProps> = ({
|
|
44
|
+
variant,
|
|
45
|
+
mimeType,
|
|
46
|
+
thumbnailUrl,
|
|
47
|
+
title,
|
|
48
|
+
source,
|
|
49
|
+
showLocked,
|
|
50
|
+
paymentStatus,
|
|
51
|
+
topRight,
|
|
52
|
+
containedImage = false,
|
|
53
|
+
}) => {
|
|
54
|
+
const effectiveSourceUrl = showLocked ? undefined : source?.sourceUrl
|
|
55
|
+
const effectiveThumbnailUrl = showLocked
|
|
56
|
+
? thumbnailUrl
|
|
57
|
+
: (source?.thumbnailUrl ?? thumbnailUrl)
|
|
58
|
+
|
|
59
|
+
const isDark = variant === 'dark'
|
|
60
|
+
|
|
61
|
+
// Locked + unlocked render entirely separate trees so the locked state
|
|
62
|
+
// doesn't end up stacking a blurred `<img>` on top of an underlying
|
|
63
|
+
// `AttachmentThumbnail` (whose `bg-white/10` poster shell otherwise
|
|
64
|
+
// shows through as a visible horizontal seam at the 16:9 boundary).
|
|
65
|
+
//
|
|
66
|
+
// The locked branch uses `filter: blur` on a single full-size `<img>`
|
|
67
|
+
// — not `backdrop-filter` — because `backdrop-filter` paints in its
|
|
68
|
+
// own compositing layer that escapes ancestor `overflow: hidden` on
|
|
69
|
+
// Chrome/Safari and flattens the card's top corners. `filter` is
|
|
70
|
+
// clipped normally, so the rounded corners stay round without any
|
|
71
|
+
// clip-path workarounds.
|
|
72
|
+
return (
|
|
73
|
+
<div className="relative overflow-hidden rounded-t-2xl">
|
|
74
|
+
{showLocked ? (
|
|
75
|
+
<div
|
|
76
|
+
className={classNames(
|
|
77
|
+
'relative aspect-video overflow-hidden',
|
|
78
|
+
isDark ? 'bg-white/10' : 'bg-black/5'
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{effectiveThumbnailUrl ? (
|
|
82
|
+
<img
|
|
83
|
+
src={effectiveThumbnailUrl}
|
|
84
|
+
alt=""
|
|
85
|
+
aria-hidden
|
|
86
|
+
draggable={false}
|
|
87
|
+
// `scale-110` hides the fuzzy semi-transparent edge that
|
|
88
|
+
// `filter: blur` leaves around the image bounds.
|
|
89
|
+
className="absolute inset-0 h-full w-full scale-110 object-cover blur-[40px]"
|
|
90
|
+
/>
|
|
91
|
+
) : (
|
|
92
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
93
|
+
{renderTypeIcon(mimeType, {
|
|
94
|
+
className: isDark
|
|
95
|
+
? 'size-12 text-white/20'
|
|
96
|
+
: 'size-12 text-black/20',
|
|
97
|
+
weight: 'regular',
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
<LockBadge paymentStatus={paymentStatus} mimeType={mimeType} />
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<AttachmentThumbnail
|
|
105
|
+
mimeType={mimeType}
|
|
106
|
+
sourceUrl={effectiveSourceUrl}
|
|
107
|
+
thumbnailUrl={effectiveThumbnailUrl}
|
|
108
|
+
title={title}
|
|
109
|
+
variant={variant}
|
|
110
|
+
containedImage={containedImage}
|
|
111
|
+
mediaPlayerProps={
|
|
112
|
+
effectiveSourceUrl
|
|
113
|
+
? { autoPlay: true, loop: true, controls: true, muted: false }
|
|
114
|
+
: undefined
|
|
115
|
+
}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{topRight && (
|
|
120
|
+
<div className="pointer-events-auto absolute right-3 top-3 z-10">
|
|
121
|
+
{topRight}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default LockedThumbnail
|
|
@@ -1,32 +1,63 @@
|
|
|
1
1
|
import React, { Suspense } from 'react'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
3
|
+
import type { ComposerCardProps } from './components/Composer/Card'
|
|
4
|
+
import type { ReceivedCardProps } from './components/Received/Card'
|
|
5
|
+
import type { SentCardProps } from './components/Sent/Card'
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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
10
|
|
|
9
11
|
const LockedAttachmentFallback = () => (
|
|
10
12
|
<div
|
|
11
|
-
className="w-[280px] min-h-[200px] animate-pulse rounded-
|
|
13
|
+
className="w-[280px] min-h-[200px] animate-pulse rounded-2xl bg-black/[0.06] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.04),0_8px_32px_rgba(0,0,0,0.1)]"
|
|
12
14
|
aria-hidden
|
|
13
15
|
/>
|
|
14
16
|
)
|
|
15
17
|
|
|
16
|
-
const
|
|
18
|
+
const Composer = (props: ComposerCardProps) => (
|
|
17
19
|
<Suspense fallback={<LockedAttachmentFallback />}>
|
|
18
|
-
<
|
|
20
|
+
<ComposerCardLazy {...props} />
|
|
19
21
|
</Suspense>
|
|
20
22
|
)
|
|
21
23
|
|
|
22
|
-
const
|
|
24
|
+
const Sent = (props: SentCardProps) => (
|
|
23
25
|
<Suspense fallback={<LockedAttachmentFallback />}>
|
|
24
|
-
<
|
|
26
|
+
<SentCardLazy {...props} />
|
|
25
27
|
</Suspense>
|
|
26
28
|
)
|
|
27
29
|
|
|
28
|
-
const
|
|
30
|
+
const Received = (props: ReceivedCardProps) => (
|
|
31
|
+
<Suspense fallback={<LockedAttachmentFallback />}>
|
|
32
|
+
<ReceivedCardLazy {...props} />
|
|
33
|
+
</Suspense>
|
|
34
|
+
)
|
|
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 }
|
|
29
48
|
|
|
30
49
|
export default LockedAttachment
|
|
31
|
-
export type {
|
|
32
|
-
|
|
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
|
|
58
|
+
export type {
|
|
59
|
+
PaymentStatus,
|
|
60
|
+
LockedAttachmentSource,
|
|
61
|
+
LockedAttachmentGalleryItem,
|
|
62
|
+
LockedAttachmentContextValue,
|
|
63
|
+
} from './types'
|
|
@@ -9,6 +9,23 @@ export interface LockedAttachmentBaseProps {
|
|
|
9
9
|
detail?: string
|
|
10
10
|
amountText?: string
|
|
11
11
|
paymentStatus?: PaymentStatus
|
|
12
|
+
/**
|
|
13
|
+
* When provided with 2+ items, the card renders as a mixed-media carousel
|
|
14
|
+
* (e.g. a couple of photos + a video) instead of a single thumbnail. Each
|
|
15
|
+
* item brings its own thumbnail and optional source so that
|
|
16
|
+
* `LockedAttachment.Composer` / `.Sent` / `.Received` can all share the
|
|
17
|
+
* same carousel chrome.
|
|
18
|
+
*/
|
|
19
|
+
gallery?: LockedAttachmentGalleryItem[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LockedAttachmentGalleryItem {
|
|
23
|
+
/** MIME type of this carousel item — drives the per-item play / lock affordance. */
|
|
24
|
+
mimeType: string
|
|
25
|
+
/** Poster image used for both the locked (blurred) and unlocked preview state. */
|
|
26
|
+
thumbnailUrl?: string
|
|
27
|
+
/** Underlying source (image or video URL) — shown only when unlocked. */
|
|
28
|
+
sourceUrl?: string
|
|
12
29
|
}
|
|
13
30
|
|
|
14
31
|
export interface LockedAttachmentSource {
|
|
@@ -216,7 +216,7 @@ export function resolveMediaFromMessage(
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
/** Dark card (sent / own message) — matches LockedAttachment.
|
|
219
|
+
/** Dark card (sent / own message) — matches LockedAttachment.Sent preview without toggle. */
|
|
220
220
|
const Creator: React.FC<MediaMessageResolved> = ({
|
|
221
221
|
resolvedUrl,
|
|
222
222
|
resolvedType,
|
|
@@ -246,7 +246,7 @@ const Creator: React.FC<MediaMessageResolved> = ({
|
|
|
246
246
|
)
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
/** Light card (received) — matches LockedAttachment.
|
|
249
|
+
/** Light card (received) — matches LockedAttachment.Received unlocked without Purchased copy. */
|
|
250
250
|
const Visitor: React.FC<MediaMessageResolved> = ({
|
|
251
251
|
resolvedUrl,
|
|
252
252
|
resolvedType,
|
|
@@ -35,6 +35,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
35
35
|
onMessageSent,
|
|
36
36
|
showStarButton = false,
|
|
37
37
|
chatbotVotingEnabled = false,
|
|
38
|
+
viewerLanguage,
|
|
38
39
|
renderMessagePreview,
|
|
39
40
|
renderChannelBanner,
|
|
40
41
|
customProfileContent,
|
|
@@ -60,9 +61,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
60
61
|
string | null
|
|
61
62
|
>(null)
|
|
62
63
|
|
|
63
|
-
const {
|
|
64
|
-
showDeleteConversation = true,
|
|
65
|
-
} = capabilities
|
|
64
|
+
const { showDeleteConversation = true } = capabilities
|
|
66
65
|
|
|
67
66
|
// Create default filters and merge with provided filters
|
|
68
67
|
const channelFilters = React.useMemo(() => {
|
|
@@ -194,7 +193,6 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
194
193
|
const channel = await service.startChannelWithParticipant({
|
|
195
194
|
id: initialParticipantData.id,
|
|
196
195
|
name: initialParticipantData.name,
|
|
197
|
-
email: initialParticipantData.email,
|
|
198
196
|
phone: initialParticipantData.phone,
|
|
199
197
|
})
|
|
200
198
|
|
|
@@ -375,6 +373,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
375
373
|
channelRenderFilterFn={channelRenderFilterFn}
|
|
376
374
|
customEmptyStateIndicator={channelListCustomEmptyStateIndicator}
|
|
377
375
|
renderMessagePreview={renderMessagePreview}
|
|
376
|
+
viewerLanguage={viewerLanguage}
|
|
378
377
|
/>
|
|
379
378
|
</div>
|
|
380
379
|
|
|
@@ -418,6 +417,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
418
417
|
onMessageSent={onMessageSent}
|
|
419
418
|
showStarButton={showStarButton}
|
|
420
419
|
chatbotVotingEnabled={chatbotVotingEnabled}
|
|
420
|
+
viewerLanguage={viewerLanguage}
|
|
421
421
|
customProfileContent={customProfileContent}
|
|
422
422
|
customChannelActions={customChannelActions}
|
|
423
423
|
renderMessage={renderMessage}
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,10 @@ export {
|
|
|
18
18
|
resolveLinkAttachment,
|
|
19
19
|
resolveMediaFromMessage,
|
|
20
20
|
} from './components/MediaMessage'
|
|
21
|
-
export type {
|
|
21
|
+
export type {
|
|
22
|
+
MediaMessageProps,
|
|
23
|
+
MediaMessageResolved,
|
|
24
|
+
} from './components/MediaMessage'
|
|
22
25
|
|
|
23
26
|
// Providers
|
|
24
27
|
export { MessagingProvider } from './providers/MessagingProvider'
|
|
@@ -31,6 +34,10 @@ export { useCustomMessage } from './components/CustomMessage/context'
|
|
|
31
34
|
|
|
32
35
|
// Utils
|
|
33
36
|
export { formatRelativeTime } from './utils/formatRelativeTime'
|
|
37
|
+
export {
|
|
38
|
+
getMessageDisplayText,
|
|
39
|
+
normalizeLanguageCode,
|
|
40
|
+
} from './utils/getMessageDisplayText'
|
|
34
41
|
|
|
35
42
|
// Types
|
|
36
43
|
export type {
|
|
@@ -45,7 +52,16 @@ export type {
|
|
|
45
52
|
export type { MessageMetadata } from './stream-custom-data'
|
|
46
53
|
export type { AvatarProps } from './components/Avatar'
|
|
47
54
|
export type { ActionButtonProps } from './components/ActionButton'
|
|
48
|
-
export type {
|
|
55
|
+
export type {
|
|
56
|
+
ComposerCardProps,
|
|
57
|
+
SentCardProps,
|
|
58
|
+
ReceivedCardProps,
|
|
59
|
+
LockedAttachmentGalleryItem,
|
|
60
|
+
LockedAttachmentContextValue,
|
|
61
|
+
// Deprecated aliases preserved for backwards compatibility:
|
|
62
|
+
CreatorCardProps,
|
|
63
|
+
VisitorCardProps,
|
|
64
|
+
} from './components/LockedAttachment'
|
|
49
65
|
export type { CustomMessageRegistry } from './components/CustomMessage/context'
|
|
50
66
|
export type { AttachmentSourceType } from './components/AttachmentCard/utils/mimeType'
|
|
51
67
|
export type { Faq, FaqListProps } from './components/FaqList'
|
package/src/stories/mocks.tsx
CHANGED
|
@@ -110,31 +110,26 @@ export const mockParticipants = [
|
|
|
110
110
|
{
|
|
111
111
|
id: 'participant-1',
|
|
112
112
|
name: 'Alice Johnson',
|
|
113
|
-
email: 'alice@example.com',
|
|
114
113
|
image: 'https://i.pravatar.cc/150?img=2',
|
|
115
114
|
},
|
|
116
115
|
{
|
|
117
116
|
id: 'participant-2',
|
|
118
117
|
name: 'Bob Smith',
|
|
119
|
-
email: 'bob@example.com',
|
|
120
118
|
image: 'https://i.pravatar.cc/150?img=3',
|
|
121
119
|
},
|
|
122
120
|
{
|
|
123
121
|
id: 'participant-3',
|
|
124
122
|
name: 'Carol Williams',
|
|
125
|
-
email: 'carol@example.com',
|
|
126
123
|
image: 'https://i.pravatar.cc/150?img=4',
|
|
127
124
|
},
|
|
128
125
|
{
|
|
129
126
|
id: 'participant-4',
|
|
130
127
|
name: 'David Brown',
|
|
131
|
-
email: 'david@example.com',
|
|
132
128
|
image: 'https://i.pravatar.cc/150?img=5',
|
|
133
129
|
},
|
|
134
130
|
{
|
|
135
131
|
id: 'participant-5',
|
|
136
132
|
name: 'Emma Davis',
|
|
137
|
-
email: 'emma@example.com',
|
|
138
133
|
image: 'https://i.pravatar.cc/150?img=6',
|
|
139
134
|
},
|
|
140
135
|
]
|
|
@@ -143,10 +138,8 @@ export const mockParticipants = [
|
|
|
143
138
|
export const mockParticipantSource = {
|
|
144
139
|
loadParticipants: async (options?: { search?: string; limit?: number }) => {
|
|
145
140
|
const searchTerm = options?.search || ''
|
|
146
|
-
const filtered = mockParticipants.filter(
|
|
147
|
-
(
|
|
148
|
-
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
149
|
-
p.email.toLowerCase().includes(searchTerm.toLowerCase())
|
|
141
|
+
const filtered = mockParticipants.filter((p) =>
|
|
142
|
+
p.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
150
143
|
)
|
|
151
144
|
return {
|
|
152
145
|
participants: filtered,
|
package/src/styles.css
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/* Stream Chat base styles */
|
|
2
2
|
@import 'stream-chat-react/dist/css/v2/index.css';
|
|
3
3
|
|
|
4
|
+
/* Inherit the host's font instead of stream-chat-react's hardcoded system stack.
|
|
5
|
+
In admin (federation host) this resolves to Link Sans; on linktr.ee profile
|
|
6
|
+
pages it resolves to the creator's profile font. */
|
|
7
|
+
.str-chat {
|
|
8
|
+
--str-chat__font-family: inherit;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
/* Dialog component styles - used by messaging components */
|
|
5
12
|
/* Note: Dialogs get moved to the top layer when opened with .showModal() */
|
|
6
13
|
.mes-dialog {
|
package/src/types.ts
CHANGED
|
@@ -25,7 +25,6 @@ export type { LockedAttachmentSource } from './components/LockedAttachment'
|
|
|
25
25
|
export interface Participant {
|
|
26
26
|
id: string
|
|
27
27
|
name: string
|
|
28
|
-
email?: string
|
|
29
28
|
image?: string
|
|
30
29
|
username?: string
|
|
31
30
|
phone?: string
|
|
@@ -95,6 +94,11 @@ export interface ChannelListProps {
|
|
|
95
94
|
message: LocalMessage | undefined,
|
|
96
95
|
defaultPreview?: string
|
|
97
96
|
) => React.ReactNode
|
|
97
|
+
/**
|
|
98
|
+
* Language code used to pick translated message text from Stream Chat i18n.
|
|
99
|
+
* Falls back to message.text when no matching translation exists.
|
|
100
|
+
*/
|
|
101
|
+
viewerLanguage?: string
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
/**
|
|
@@ -173,6 +177,11 @@ export interface ChannelViewProps {
|
|
|
173
177
|
* Defaults to false.
|
|
174
178
|
*/
|
|
175
179
|
chatbotVotingEnabled?: boolean
|
|
180
|
+
/**
|
|
181
|
+
* Language code used to pick translated message text from Stream Chat i18n.
|
|
182
|
+
* Falls back to message.text when no matching translation exists.
|
|
183
|
+
*/
|
|
184
|
+
viewerLanguage?: string
|
|
176
185
|
|
|
177
186
|
/**
|
|
178
187
|
* Custom render function for a banner/card component that renders
|
|
@@ -244,6 +253,7 @@ export type ChannelViewPassthroughProps = Pick<
|
|
|
244
253
|
| 'onMessageSent'
|
|
245
254
|
| 'showStarButton'
|
|
246
255
|
| 'chatbotVotingEnabled'
|
|
256
|
+
| 'viewerLanguage'
|
|
247
257
|
| 'renderChannelBanner'
|
|
248
258
|
| 'customProfileContent'
|
|
249
259
|
| 'customChannelActions'
|