@linktr.ee/messaging-react 1.32.1 → 1.33.0-rc-1777504230
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-ClE_iExA.js → Card-BsqYzZt1.js} +55 -55
- package/dist/Card-BsqYzZt1.js.map +1 -0
- package/dist/{Card-1CQEn-OT.js → Card-Cnn9V-W7.js} +44 -44
- package/dist/Card-Cnn9V-W7.js.map +1 -0
- package/dist/assets/index.css +1 -1
- package/dist/index-BMfupE8K.js +3130 -0
- package/dist/index-BMfupE8K.js.map +1 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.js +20 -2477
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelInfoDialog/index.tsx +3 -1
- package/src/components/ChannelView.stories.tsx +38 -0
- package/src/components/ChannelView.test.tsx +25 -6
- package/src/components/ChannelView.tsx +26 -6
- package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +180 -0
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +63 -1
- package/src/components/CustomMessageInput/index.tsx +24 -5
- package/src/components/LockedAttachment/components/Creator/Card.tsx +11 -11
- package/src/components/LockedAttachment/components/MediaPlayer.tsx +10 -1
- package/src/components/LockedAttachment/components/Visitor/Card.tsx +9 -9
- package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +2 -2
- package/src/components/MediaMessage/MediaMessage.stories.tsx +233 -0
- package/src/components/MediaMessage/MediaMessage.test.tsx +520 -0
- package/src/components/MediaMessage/index.tsx +476 -0
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +2 -0
- package/src/styles.css +49 -0
- package/src/types.ts +13 -0
- package/dist/Card-1CQEn-OT.js.map +0 -1
- package/dist/Card-ClE_iExA.js.map +0 -1
- package/dist/MediaPlayer-B9Ws2NeE.js +0 -292
- package/dist/MediaPlayer-B9Ws2NeE.js.map +0 -1
|
@@ -7,17 +7,17 @@ import { renderWithProviders, screen } from '../test/utils'
|
|
|
7
7
|
import { ChannelView } from './ChannelView'
|
|
8
8
|
|
|
9
9
|
let activeChannel: unknown
|
|
10
|
+
let activeChannelProps: Record<string, unknown> = {}
|
|
10
11
|
|
|
11
12
|
vi.mock('stream-chat-react', () => ({
|
|
12
|
-
Channel: ({
|
|
13
|
-
channel,
|
|
14
|
-
children,
|
|
15
|
-
}: {
|
|
13
|
+
Channel: (props: {
|
|
16
14
|
channel: unknown
|
|
17
15
|
children: React.ReactNode
|
|
16
|
+
[key: string]: unknown
|
|
18
17
|
}) => {
|
|
19
|
-
activeChannel = channel
|
|
20
|
-
|
|
18
|
+
activeChannel = props.channel
|
|
19
|
+
activeChannelProps = props
|
|
20
|
+
return <div data-testid="channel">{props.children}</div>
|
|
21
21
|
},
|
|
22
22
|
Window: ({ children }: { children: React.ReactNode }) => (
|
|
23
23
|
<div data-testid="window">{children}</div>
|
|
@@ -96,6 +96,7 @@ const createChannel = () =>
|
|
|
96
96
|
describe('ChannelView', () => {
|
|
97
97
|
beforeEach(() => {
|
|
98
98
|
activeChannel = undefined
|
|
99
|
+
activeChannelProps = {}
|
|
99
100
|
avatarRenderCalls.length = 0
|
|
100
101
|
mockIsStarred = false
|
|
101
102
|
})
|
|
@@ -193,4 +194,22 @@ describe('ChannelView', () => {
|
|
|
193
194
|
)
|
|
194
195
|
expect(renderMessageInputActions).toHaveBeenCalledWith(channel)
|
|
195
196
|
})
|
|
197
|
+
|
|
198
|
+
it('passes sendButton to Channel as SendButton when provided', () => {
|
|
199
|
+
const CustomSendButton = () => (
|
|
200
|
+
<button type="button" aria-label="Custom Send" />
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
renderWithProviders(
|
|
204
|
+
<ChannelView channel={createChannel()} sendButton={CustomSendButton} />
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(activeChannelProps.SendButton).toBe(CustomSendButton)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('does not pass SendButton to Channel when sendButton is not provided', () => {
|
|
211
|
+
renderWithProviders(<ChannelView channel={createChannel()} />)
|
|
212
|
+
|
|
213
|
+
expect(activeChannelProps.SendButton).toBeUndefined()
|
|
214
|
+
})
|
|
196
215
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArrowLeftIcon, DotsThreeIcon, StarIcon } from '@phosphor-icons/react'
|
|
1
|
+
import { ArrowLeftIcon, CaretRightIcon, DotsThreeIcon, StarIcon } from '@phosphor-icons/react'
|
|
2
2
|
import classNames from 'classnames'
|
|
3
3
|
import React, { useCallback, useRef } from 'react'
|
|
4
4
|
import { Channel as ChannelType } from 'stream-chat'
|
|
@@ -96,9 +96,15 @@ const CustomChannelHeader: React.FC<{
|
|
|
96
96
|
starred={showStarButton && isStarred}
|
|
97
97
|
size={40}
|
|
98
98
|
/>
|
|
99
|
-
<
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={onShowInfo}
|
|
102
|
+
className="flex items-center gap-0.5 rounded-full bg-black/[0.05] px-3 py-1 text-xs font-medium text-black/90 hover:bg-black/[0.08] transition-colors"
|
|
103
|
+
aria-label={`View info for ${participantName}`}
|
|
104
|
+
>
|
|
100
105
|
{participantName}
|
|
101
|
-
|
|
106
|
+
<CaretRightIcon className="size-3 shrink-0" />
|
|
107
|
+
</button>
|
|
102
108
|
</div>
|
|
103
109
|
<div className="flex justify-end items-center gap-2">
|
|
104
110
|
{showStarButton && (
|
|
@@ -150,9 +156,21 @@ const CustomChannelHeader: React.FC<{
|
|
|
150
156
|
size={40}
|
|
151
157
|
/>
|
|
152
158
|
<div className="min-w-0">
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
{canShowInfo ? (
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={onShowInfo}
|
|
163
|
+
className="flex items-center gap-1 font-medium text-black/90 truncate hover:text-black/70 transition-colors"
|
|
164
|
+
aria-label={`View info for ${participantName}`}
|
|
165
|
+
>
|
|
166
|
+
<span className="truncate">{participantName}</span>
|
|
167
|
+
<CaretRightIcon className="size-4 shrink-0" />
|
|
168
|
+
</button>
|
|
169
|
+
) : (
|
|
170
|
+
<h1 className="font-medium text-black/90 truncate">
|
|
171
|
+
{participantName}
|
|
172
|
+
</h1>
|
|
173
|
+
)}
|
|
156
174
|
</div>
|
|
157
175
|
</div>
|
|
158
176
|
<div className="flex items-center gap-2">
|
|
@@ -376,6 +394,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
376
394
|
customProfileContent,
|
|
377
395
|
customChannelActions,
|
|
378
396
|
renderMessage,
|
|
397
|
+
sendButton,
|
|
379
398
|
}) => {
|
|
380
399
|
// Custom send message handler that:
|
|
381
400
|
// 1. Applies messageMetadata if provided
|
|
@@ -435,6 +454,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
435
454
|
LoadingIndicator={LoadingState}
|
|
436
455
|
DateSeparator={CustomDateSeparator}
|
|
437
456
|
doSendMessageRequest={doSendMessageRequest}
|
|
457
|
+
{...(sendButton ? { SendButton: sendButton } : {})}
|
|
438
458
|
>
|
|
439
459
|
<ChannelViewInner
|
|
440
460
|
onBack={onBack}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import { QueryChannelAPIResponse, StreamChat } from 'stream-chat'
|
|
4
|
+
import { Channel, Chat } from 'stream-chat-react'
|
|
5
|
+
|
|
6
|
+
import { CustomMessageInput } from '.'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Setup helpers (mirrors ChannelView.stories.tsx)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const mockUser = {
|
|
13
|
+
id: 'storybook-user',
|
|
14
|
+
name: 'Storybook User',
|
|
15
|
+
image: 'https://i.pravatar.cc/150?img=1',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const mockParticipant = {
|
|
19
|
+
id: 'participant-1',
|
|
20
|
+
name: 'Jane Creator',
|
|
21
|
+
image: 'https://i.pravatar.cc/150?img=5',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const createMockChannel = async (
|
|
25
|
+
client: StreamChat,
|
|
26
|
+
opts: { frozen?: boolean } = {}
|
|
27
|
+
) => {
|
|
28
|
+
const channelData = { members: [mockUser.id, mockParticipant.id], frozen: opts.frozen ?? false }
|
|
29
|
+
const ch = client.channel('messaging', 'storybook-cmi-channel', channelData)
|
|
30
|
+
|
|
31
|
+
ch.watch = async () => {
|
|
32
|
+
ch.state.members = {
|
|
33
|
+
[mockUser.id]: { user: mockUser, user_id: mockUser.id },
|
|
34
|
+
[mockParticipant.id]: { user: mockParticipant, user_id: mockParticipant.id },
|
|
35
|
+
}
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
;(ch as any)._data = channelData
|
|
38
|
+
return { channel: channelData, members: [], messages: [], watchers: [], pinned_messages: [], duration: '0ms' } as unknown as QueryChannelAPIResponse
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try { await ch.watch() } catch (_) { /* mock */ }
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
;(ch as any)._data = channelData
|
|
44
|
+
return ch
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Story wrapper — mounts the input inside a real Stream Channel context
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
type WrapperProps = {
|
|
52
|
+
frozen?: boolean
|
|
53
|
+
sendButton?: React.ComponentType<{
|
|
54
|
+
sendMessage: (...args: unknown[]) => unknown
|
|
55
|
+
disabled?: boolean
|
|
56
|
+
[key: string]: unknown
|
|
57
|
+
}>
|
|
58
|
+
renderActions?: () => React.ReactNode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const Wrapper: React.FC<WrapperProps> = ({ frozen, sendButton, renderActions }) => {
|
|
62
|
+
const [client] = React.useState(() => {
|
|
63
|
+
const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
|
|
64
|
+
c.userID = mockUser.id
|
|
65
|
+
c.user = mockUser
|
|
66
|
+
return c
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const [channel, setChannel] = React.useState<ReturnType<StreamChat['channel']> | null>(null)
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
createMockChannel(client, { frozen }).then(setChannel)
|
|
73
|
+
}, [client, frozen])
|
|
74
|
+
|
|
75
|
+
if (!channel) return <div className="p-4 text-sm text-gray-400">Loading…</div>
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Chat client={client}>
|
|
79
|
+
<Channel
|
|
80
|
+
channel={channel}
|
|
81
|
+
{...(sendButton ? { SendButton: sendButton } : {})}
|
|
82
|
+
>
|
|
83
|
+
<div className="bg-white" style={{ minWidth: 360 }}>
|
|
84
|
+
<CustomMessageInput renderActions={renderActions} />
|
|
85
|
+
</div>
|
|
86
|
+
</Channel>
|
|
87
|
+
</Chat>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Meta
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
const meta: Meta<WrapperProps> = {
|
|
96
|
+
title: 'CustomMessageInput',
|
|
97
|
+
component: CustomMessageInput,
|
|
98
|
+
parameters: {
|
|
99
|
+
layout: 'centered',
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
export default meta
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Stories
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export const Default: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
109
|
+
Default.args = {}
|
|
110
|
+
Default.parameters = {
|
|
111
|
+
docs: {
|
|
112
|
+
description: {
|
|
113
|
+
story:
|
|
114
|
+
'Default message input. The send button (arrow) is disabled until the user types text — `useMessageComposerHasSendableData` controls this.',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const Frozen: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
120
|
+
Frozen.args = { frozen: true }
|
|
121
|
+
Frozen.parameters = {
|
|
122
|
+
docs: {
|
|
123
|
+
description: {
|
|
124
|
+
story:
|
|
125
|
+
'Frozen channel state: the entire input area fades out (`aria-disabled`, `inert`), the textarea becomes read-only, and the send button is disabled.',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const WithCustomActionButton: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
131
|
+
WithCustomActionButton.args = {
|
|
132
|
+
renderActions: () => (
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
aria-label="Attach media"
|
|
136
|
+
className="flex items-center justify-center size-10 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1] shrink-0"
|
|
137
|
+
onClick={() => console.log('media picker')}
|
|
138
|
+
>
|
|
139
|
+
📷
|
|
140
|
+
</button>
|
|
141
|
+
),
|
|
142
|
+
}
|
|
143
|
+
WithCustomActionButton.parameters = {
|
|
144
|
+
docs: {
|
|
145
|
+
description: {
|
|
146
|
+
story:
|
|
147
|
+
'Shows how `renderActions` injects extra controls (e.g. a media picker button) to the left of the message bubble.',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A custom send button that turns purple to signal a media attachment is staged. */
|
|
153
|
+
const PurpleMediaSendButton: React.FC<{
|
|
154
|
+
sendMessage: (...args: unknown[]) => unknown
|
|
155
|
+
disabled?: boolean
|
|
156
|
+
[key: string]: unknown
|
|
157
|
+
}> = ({ sendMessage, disabled, ...rest }) => (
|
|
158
|
+
<button
|
|
159
|
+
{...rest}
|
|
160
|
+
type="button"
|
|
161
|
+
aria-label="Send"
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
onClick={() => sendMessage()}
|
|
164
|
+
className="str-chat__send-button mt-auto flex items-center justify-center shrink-0 rounded-full size-8 bg-[#6D28D9] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white"
|
|
165
|
+
style={{ fontSize: 15 }}
|
|
166
|
+
>
|
|
167
|
+
🚀
|
|
168
|
+
</button>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
export const WithCustomSendButton: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
172
|
+
WithCustomSendButton.args = { sendButton: PurpleMediaSendButton }
|
|
173
|
+
WithCustomSendButton.parameters = {
|
|
174
|
+
docs: {
|
|
175
|
+
description: {
|
|
176
|
+
story:
|
|
177
|
+
'Passes a custom `SendButton` via `Channel`\'s `SendButton` prop (which is how `ChannelView`\'s `sendButton` prop surfaces it). `CustomMessageInput` reads the button from `useComponentContext` and falls back to the default when none is provided.',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { renderWithProviders, screen } from '../../test/utils'
|
|
5
5
|
|
|
@@ -7,6 +7,21 @@ import { CustomMessageInput } from '.'
|
|
|
7
7
|
|
|
8
8
|
let mockChannelData: Record<string, unknown> = {}
|
|
9
9
|
|
|
10
|
+
let mockContextSendButton:
|
|
11
|
+
| React.ComponentType<{
|
|
12
|
+
sendMessage: (...args: unknown[]) => unknown
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
}>
|
|
15
|
+
| undefined = ({ sendMessage, disabled }) => (
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
data-testid="send-button"
|
|
19
|
+
aria-label="Send"
|
|
20
|
+
disabled={disabled}
|
|
21
|
+
onClick={() => sendMessage()}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
|
|
10
25
|
vi.mock('stream-chat-react', () => ({
|
|
11
26
|
MessageInput: ({
|
|
12
27
|
Input,
|
|
@@ -35,6 +50,9 @@ vi.mock('stream-chat-react', () => ({
|
|
|
35
50
|
handleSubmit: vi.fn(),
|
|
36
51
|
}),
|
|
37
52
|
useMessageComposerHasSendableData: () => false,
|
|
53
|
+
useComponentContext: () => ({
|
|
54
|
+
SendButton: mockContextSendButton,
|
|
55
|
+
}),
|
|
38
56
|
}))
|
|
39
57
|
|
|
40
58
|
vi.mock('../CustomLinkPreviewList', () => ({
|
|
@@ -42,6 +60,18 @@ vi.mock('../CustomLinkPreviewList', () => ({
|
|
|
42
60
|
}))
|
|
43
61
|
|
|
44
62
|
describe('CustomMessageInput', () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
mockContextSendButton = ({ sendMessage, disabled }) => (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
data-testid="send-button"
|
|
68
|
+
aria-label="Send"
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
onClick={() => sendMessage()}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
45
75
|
it('renders the interactive message input when channel is not frozen', () => {
|
|
46
76
|
mockChannelData = {}
|
|
47
77
|
|
|
@@ -124,4 +154,36 @@ describe('CustomMessageInput', () => {
|
|
|
124
154
|
expect(messageInput).not.toHaveAttribute('aria-disabled')
|
|
125
155
|
expect(screen.getByTestId('custom-action')).toBeInTheDocument()
|
|
126
156
|
})
|
|
157
|
+
|
|
158
|
+
it('renders the custom SendButton from component context', () => {
|
|
159
|
+
mockChannelData = {}
|
|
160
|
+
mockContextSendButton = () => (
|
|
161
|
+
<button type="button" data-testid="custom-context-send-button" aria-label="Custom Send" />
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
renderWithProviders(<CustomMessageInput />)
|
|
165
|
+
|
|
166
|
+
expect(screen.getByTestId('custom-context-send-button')).toBeInTheDocument()
|
|
167
|
+
expect(screen.queryByTestId('default-send-button')).not.toBeInTheDocument()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('falls back to the arrow send button when component context provides none', () => {
|
|
171
|
+
mockChannelData = {}
|
|
172
|
+
mockContextSendButton = undefined
|
|
173
|
+
|
|
174
|
+
renderWithProviders(<CustomMessageInput />)
|
|
175
|
+
|
|
176
|
+
expect(screen.getByTestId('send-button')).toBeInTheDocument()
|
|
177
|
+
expect(screen.queryByTestId('custom-context-send-button')).not.toBeInTheDocument()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('preserves disabled state on the send button when there is no sendable data', () => {
|
|
181
|
+
mockChannelData = {}
|
|
182
|
+
|
|
183
|
+
renderWithProviders(<CustomMessageInput />)
|
|
184
|
+
|
|
185
|
+
const sendButton = screen.getByRole('button', { name: /send/i })
|
|
186
|
+
// useMessageComposerHasSendableData is mocked to return false
|
|
187
|
+
expect(sendButton).toBeDisabled()
|
|
188
|
+
})
|
|
127
189
|
})
|
|
@@ -7,16 +7,37 @@ import {
|
|
|
7
7
|
SimpleAttachmentSelector,
|
|
8
8
|
TextareaComposer,
|
|
9
9
|
useChannelStateContext,
|
|
10
|
+
useComponentContext,
|
|
10
11
|
useMessageComposerHasSendableData,
|
|
11
12
|
useMessageInputContext,
|
|
12
13
|
} from 'stream-chat-react'
|
|
13
14
|
|
|
14
15
|
import { CustomLinkPreviewList } from '../CustomLinkPreviewList'
|
|
15
16
|
|
|
17
|
+
const DefaultSendButton: React.FC<{
|
|
18
|
+
sendMessage: () => void
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
[key: string]: unknown
|
|
21
|
+
}> = ({ sendMessage, disabled, ...rest }) => (
|
|
22
|
+
<button
|
|
23
|
+
{...rest}
|
|
24
|
+
type="button"
|
|
25
|
+
aria-label="Send"
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
onClick={sendMessage}
|
|
28
|
+
>
|
|
29
|
+
<ArrowUpIcon weight="bold" className="size-4" />
|
|
30
|
+
</button>
|
|
31
|
+
)
|
|
32
|
+
|
|
16
33
|
const CustomMessageInputInner: React.FC = () => {
|
|
17
34
|
const { channel } = useChannelStateContext()
|
|
18
35
|
const isFrozen = channel?.data?.frozen === true
|
|
19
36
|
const { handleSubmit } = useMessageInputContext()
|
|
37
|
+
const { SendButton: SendButtonFromContext } = useComponentContext(
|
|
38
|
+
'CustomMessageInput',
|
|
39
|
+
)
|
|
40
|
+
const SendButton = SendButtonFromContext ?? DefaultSendButton
|
|
20
41
|
const hasSendableData = useMessageComposerHasSendableData()
|
|
21
42
|
const isSendDisabled = isFrozen || !hasSendableData
|
|
22
43
|
|
|
@@ -44,16 +65,14 @@ const CustomMessageInputInner: React.FC = () => {
|
|
|
44
65
|
tabIndex={isFrozen ? -1 : undefined}
|
|
45
66
|
/>
|
|
46
67
|
</div>
|
|
47
|
-
<
|
|
68
|
+
<SendButton
|
|
69
|
+
sendMessage={handleSubmit}
|
|
48
70
|
aria-label="Send"
|
|
49
71
|
className="str-chat__send-button mt-auto flex justify-center items-center flex-shrink-0 rounded-full size-8 bg-[#121110] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white focus-ring"
|
|
50
72
|
data-testid="send-button"
|
|
51
73
|
disabled={isSendDisabled}
|
|
52
|
-
onClick={handleSubmit}
|
|
53
74
|
type="button"
|
|
54
|
-
|
|
55
|
-
<ArrowUpIcon className="size-4" />
|
|
56
|
-
</button>
|
|
75
|
+
/>
|
|
57
76
|
</div>
|
|
58
77
|
</div>
|
|
59
78
|
</>
|
|
@@ -67,11 +67,11 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
|
|
|
67
67
|
onToggle={onPreviewClick ? handleToggle : undefined}
|
|
68
68
|
/>
|
|
69
69
|
|
|
70
|
-
<div className="px-4 pb-3 pt-3">
|
|
70
|
+
<div className="px-4 pb-3 pt-3 bg-black">
|
|
71
71
|
<p
|
|
72
72
|
className={classNames('mb-1.5 truncate text-base font-medium', {
|
|
73
|
-
'text-
|
|
74
|
-
'text-
|
|
73
|
+
'text-white/30': !title,
|
|
74
|
+
'text-white': !!title,
|
|
75
75
|
})}
|
|
76
76
|
>
|
|
77
77
|
{title || placeholderTitle}
|
|
@@ -79,30 +79,30 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
|
|
|
79
79
|
|
|
80
80
|
<div className="flex items-center gap-1">
|
|
81
81
|
{renderTypeIcon(mimeType, {
|
|
82
|
-
className: 'size-5 shrink-0 text-
|
|
82
|
+
className: 'size-5 shrink-0 text-white/55',
|
|
83
83
|
weight: 'regular',
|
|
84
84
|
})}
|
|
85
85
|
|
|
86
86
|
{detail && (
|
|
87
|
-
<span className="text-xs font-medium text-
|
|
87
|
+
<span className="text-xs font-medium text-white/55">{detail}</span>
|
|
88
88
|
)}
|
|
89
89
|
|
|
90
90
|
{paymentStatus === 'paid' ? (
|
|
91
91
|
<React.Fragment>
|
|
92
|
-
<span className="text-xs font-medium text-
|
|
93
|
-
<span className="text-xs font-medium text-[#
|
|
92
|
+
<span className="text-xs font-medium text-white/55">•</span>
|
|
93
|
+
<span className="text-xs font-medium text-[#4ade80]">Sold</span>
|
|
94
94
|
<CheckCircleIcon
|
|
95
|
-
className="size-4 text-[#
|
|
95
|
+
className="size-4 text-[#4ade80]"
|
|
96
96
|
weight="bold"
|
|
97
97
|
/>
|
|
98
98
|
</React.Fragment>
|
|
99
99
|
) : (
|
|
100
100
|
<React.Fragment>
|
|
101
|
-
<span className="text-xs font-medium text-
|
|
101
|
+
<span className="text-xs font-medium text-white/55">•</span>
|
|
102
102
|
<span
|
|
103
103
|
className={classNames('text-xs font-medium', {
|
|
104
|
-
'text-
|
|
105
|
-
'text-
|
|
104
|
+
'text-white/30': !amountText,
|
|
105
|
+
'text-white/55': !!amountText,
|
|
106
106
|
})}
|
|
107
107
|
>
|
|
108
108
|
{amountText || placeholderAmountText}
|
|
@@ -31,6 +31,12 @@ export interface MediaPlayerProps {
|
|
|
31
31
|
showProgress?: boolean
|
|
32
32
|
/** When true, requests muted playback (helps autoplay policies on video). */
|
|
33
33
|
muted?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* When provided, overrides the default click-to-play-toggle behaviour on the
|
|
36
|
+
* player container. The play/pause button (which calls stopPropagation) is
|
|
37
|
+
* unaffected, so inline playback still works.
|
|
38
|
+
*/
|
|
39
|
+
onContainerClick?: (e: React.MouseEvent) => void
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
@@ -43,6 +49,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
43
49
|
controls = true,
|
|
44
50
|
showProgress = false,
|
|
45
51
|
muted = false,
|
|
52
|
+
onContainerClick,
|
|
46
53
|
}) => {
|
|
47
54
|
// --- Derived ---
|
|
48
55
|
const sourceType = getSourceType(mimeType)
|
|
@@ -185,13 +192,15 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
185
192
|
tabIndex={0}
|
|
186
193
|
className={`relative cursor-pointer overflow-hidden bg-black ${aspectClass}`}
|
|
187
194
|
style={aspectStyle}
|
|
188
|
-
onClick={() => {
|
|
195
|
+
onClick={(e) => {
|
|
196
|
+
if (onContainerClick) { onContainerClick(e); return }
|
|
189
197
|
if (manualPlayRequired) return
|
|
190
198
|
if (controls) setPlaying((p) => !p)
|
|
191
199
|
}}
|
|
192
200
|
onKeyDown={(e) => {
|
|
193
201
|
if (e.key !== 'Enter' && e.key !== ' ') return
|
|
194
202
|
e.preventDefault()
|
|
203
|
+
if (onContainerClick) { onContainerClick(e as unknown as React.MouseEvent); return }
|
|
195
204
|
if (manualPlayRequired) return
|
|
196
205
|
if (controls) setPlaying((p) => !p)
|
|
197
206
|
}}
|
|
@@ -111,35 +111,35 @@ const VisitorCard: React.FC<VisitorCardProps> = ({
|
|
|
111
111
|
paymentStatus={paymentStatus}
|
|
112
112
|
/>
|
|
113
113
|
|
|
114
|
-
<div className="px-4 pb-3 pt-3">
|
|
115
|
-
<p className="mb-1.5 truncate text-base font-medium text-
|
|
114
|
+
<div className="px-4 pb-3 pt-3 bg-black">
|
|
115
|
+
<p className="mb-1.5 truncate text-base font-medium text-white">
|
|
116
116
|
{title}
|
|
117
117
|
</p>
|
|
118
118
|
<div className="flex items-center gap-1">
|
|
119
119
|
{renderTypeIcon(mimeType, {
|
|
120
|
-
className: 'size-5 shrink-0 text-
|
|
120
|
+
className: 'size-5 shrink-0 text-white/55',
|
|
121
121
|
weight: 'regular',
|
|
122
122
|
})}
|
|
123
123
|
|
|
124
124
|
{detail && (
|
|
125
|
-
<span className="text-xs font-medium text-
|
|
125
|
+
<span className="text-xs font-medium text-white/55">{detail}</span>
|
|
126
126
|
)}
|
|
127
127
|
|
|
128
128
|
{paymentStatus === 'paid' ? (
|
|
129
129
|
<React.Fragment>
|
|
130
|
-
<span className="text-xs font-medium text-
|
|
131
|
-
<span className="text-xs font-medium text-[#
|
|
130
|
+
<span className="text-xs font-medium text-white/55">•</span>
|
|
131
|
+
<span className="text-xs font-medium text-[#4ade80]">
|
|
132
132
|
Purchased
|
|
133
133
|
</span>
|
|
134
134
|
<CheckCircleIcon
|
|
135
|
-
className="size-4 text-[#
|
|
135
|
+
className="size-4 text-[#4ade80]"
|
|
136
136
|
weight="bold"
|
|
137
137
|
/>
|
|
138
138
|
</React.Fragment>
|
|
139
139
|
) : amountText != null ? (
|
|
140
140
|
<React.Fragment>
|
|
141
|
-
<span className="text-xs font-medium text-
|
|
142
|
-
<span className="text-xs font-medium text-
|
|
141
|
+
<span className="text-xs font-medium text-white/55">•</span>
|
|
142
|
+
<span className="text-xs font-medium text-white/55">
|
|
143
143
|
{amountText}
|
|
144
144
|
</span>
|
|
145
145
|
</React.Fragment>
|
|
@@ -26,7 +26,7 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
|
|
|
26
26
|
type="button"
|
|
27
27
|
onClick={onUnlockClicked}
|
|
28
28
|
disabled={isUnlocking}
|
|
29
|
-
className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-
|
|
29
|
+
className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-medium leading-none text-[#121110] hover:bg-white/90 disabled:opacity-70"
|
|
30
30
|
>
|
|
31
31
|
{isUnlocking ? (
|
|
32
32
|
<LoadingDots />
|
|
@@ -47,7 +47,7 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
|
|
|
47
47
|
target="_blank"
|
|
48
48
|
rel="noopener noreferrer"
|
|
49
49
|
onClick={onDownloadClicked}
|
|
50
|
-
className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-
|
|
50
|
+
className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-medium leading-none !text-[#121110] hover:bg-white/90"
|
|
51
51
|
>
|
|
52
52
|
<DownloadSimpleIcon className="size-4" weight="bold" />
|
|
53
53
|
Download
|