@linktr.ee/messaging-react 1.31.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -39,7 +39,8 @@ const mockUser = {
|
|
|
39
39
|
const createMockChannel = async (
|
|
40
40
|
client: StreamChat,
|
|
41
41
|
hasMessages = true,
|
|
42
|
-
followerStatus?: string | boolean
|
|
42
|
+
followerStatus?: string | boolean,
|
|
43
|
+
isFrozen = false
|
|
43
44
|
) => {
|
|
44
45
|
const participant = mockParticipants[0]
|
|
45
46
|
|
|
@@ -105,6 +106,7 @@ const createMockChannel = async (
|
|
|
105
106
|
// Prepare channel data with optional follower status
|
|
106
107
|
const channelData: Record<string, unknown> = {
|
|
107
108
|
members: [mockUser.id, participant.id],
|
|
109
|
+
frozen: isFrozen,
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
// Add follower status if provided
|
|
@@ -155,10 +157,13 @@ const createMockChannel = async (
|
|
|
155
157
|
return channel
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
type TemplateProps = ComponentProps & {
|
|
160
|
+
type TemplateProps = ComponentProps & {
|
|
161
|
+
followerStatus?: string | boolean
|
|
162
|
+
isFrozen?: boolean
|
|
163
|
+
}
|
|
159
164
|
|
|
160
165
|
const Template: StoryFn<TemplateProps> = (args) => {
|
|
161
|
-
const { followerStatus, ...channelViewProps } = args
|
|
166
|
+
const { followerStatus, isFrozen = false, ...channelViewProps } = args
|
|
162
167
|
const [client] = React.useState(() => {
|
|
163
168
|
const client = new StreamChat('mock-api-key', {
|
|
164
169
|
allowServerSideConnect: true,
|
|
@@ -171,10 +176,12 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
171
176
|
const [channel, setChannel] = React.useState<ChannelType | null>(null)
|
|
172
177
|
|
|
173
178
|
useEffect(() => {
|
|
174
|
-
createMockChannel(client, true, followerStatus).then(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
createMockChannel(client, true, followerStatus, isFrozen).then(
|
|
180
|
+
(mockChannel) => {
|
|
181
|
+
setChannel(mockChannel)
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
}, [client, followerStatus, isFrozen])
|
|
178
185
|
|
|
179
186
|
if (!channel) {
|
|
180
187
|
return <div>Loading...</div>
|
|
@@ -524,3 +531,27 @@ NoFollowerStatus.parameters = {
|
|
|
524
531
|
},
|
|
525
532
|
},
|
|
526
533
|
}
|
|
534
|
+
|
|
535
|
+
export const FrozenChannel: StoryFn<TemplateProps> = Template.bind({})
|
|
536
|
+
FrozenChannel.args = {
|
|
537
|
+
showBackButton: false,
|
|
538
|
+
isFrozen: true,
|
|
539
|
+
renderMessageInputActions: (channel) => (
|
|
540
|
+
<button
|
|
541
|
+
onClick={() => console.log('Custom action clicked', channel.id)}
|
|
542
|
+
className="p-2 hover:bg-sand rounded-lg"
|
|
543
|
+
aria-label="Attach file"
|
|
544
|
+
type="button"
|
|
545
|
+
>
|
|
546
|
+
📎
|
|
547
|
+
</button>
|
|
548
|
+
),
|
|
549
|
+
}
|
|
550
|
+
FrozenChannel.parameters = {
|
|
551
|
+
docs: {
|
|
552
|
+
description: {
|
|
553
|
+
story:
|
|
554
|
+
'Channel view for a frozen conversation. The message composer renders in its disabled frozen state while the rest of the conversation remains readable.',
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { renderWithProviders, screen } from '../../test/utils'
|
|
5
|
+
|
|
6
|
+
import { CustomMessageInput } from '.'
|
|
7
|
+
|
|
8
|
+
let mockChannelData: Record<string, unknown> = {}
|
|
9
|
+
|
|
10
|
+
vi.mock('stream-chat-react', () => ({
|
|
11
|
+
MessageInput: ({
|
|
12
|
+
Input,
|
|
13
|
+
}: {
|
|
14
|
+
Input: React.ComponentType
|
|
15
|
+
}) => (
|
|
16
|
+
<div data-testid="stream-message-input">
|
|
17
|
+
<Input />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
SimpleAttachmentSelector: () => (
|
|
21
|
+
<div data-testid="simple-attachment-selector" />
|
|
22
|
+
),
|
|
23
|
+
TextareaComposer: ({
|
|
24
|
+
maxRows: _maxRows,
|
|
25
|
+
...props
|
|
26
|
+
}: React.TextareaHTMLAttributes<HTMLTextAreaElement> & { maxRows?: number }) => (
|
|
27
|
+
<textarea data-testid="textarea-composer" {...props} />
|
|
28
|
+
),
|
|
29
|
+
AttachmentPreviewList: () => <div data-testid="attachment-preview-list" />,
|
|
30
|
+
QuotedMessagePreview: () => <div data-testid="quoted-message-preview" />,
|
|
31
|
+
useChannelStateContext: () => ({
|
|
32
|
+
channel: { data: mockChannelData },
|
|
33
|
+
}),
|
|
34
|
+
useMessageInputContext: () => ({
|
|
35
|
+
handleSubmit: vi.fn(),
|
|
36
|
+
}),
|
|
37
|
+
useMessageComposerHasSendableData: () => false,
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
vi.mock('../CustomLinkPreviewList', () => ({
|
|
41
|
+
CustomLinkPreviewList: () => <div data-testid="custom-link-preview-list" />,
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
describe('CustomMessageInput', () => {
|
|
45
|
+
it('renders the interactive message input when channel is not frozen', () => {
|
|
46
|
+
mockChannelData = {}
|
|
47
|
+
|
|
48
|
+
const { container } = renderWithProviders(<CustomMessageInput />)
|
|
49
|
+
|
|
50
|
+
const messageInput = container.firstElementChild
|
|
51
|
+
expect(messageInput).not.toHaveAttribute('aria-disabled')
|
|
52
|
+
expect(messageInput).not.toHaveAttribute('inert')
|
|
53
|
+
expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('renders the frozen message input when channel is frozen', () => {
|
|
57
|
+
mockChannelData = { frozen: true }
|
|
58
|
+
|
|
59
|
+
const { container } = renderWithProviders(<CustomMessageInput />)
|
|
60
|
+
|
|
61
|
+
const messageInput = container.firstElementChild
|
|
62
|
+
expect(messageInput).toHaveAttribute('aria-disabled', 'true')
|
|
63
|
+
expect(messageInput).toHaveAttribute('inert')
|
|
64
|
+
expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('makes the textarea read-only and disables send when channel is frozen', () => {
|
|
68
|
+
mockChannelData = { frozen: true }
|
|
69
|
+
|
|
70
|
+
renderWithProviders(<CustomMessageInput />)
|
|
71
|
+
|
|
72
|
+
const textarea = screen.getByTestId('textarea-composer')
|
|
73
|
+
expect(textarea).toHaveAttribute('aria-disabled', 'true')
|
|
74
|
+
expect(textarea).toHaveAttribute('readonly')
|
|
75
|
+
expect(textarea).toHaveAttribute('tabindex', '-1')
|
|
76
|
+
expect(textarea).not.toHaveAttribute('autofocus')
|
|
77
|
+
|
|
78
|
+
const sendButton = screen.getByRole('button', { name: /send/i })
|
|
79
|
+
expect(sendButton).toBeDisabled()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('renders the existing attachment selector when channel is frozen', () => {
|
|
83
|
+
mockChannelData = { frozen: true }
|
|
84
|
+
|
|
85
|
+
renderWithProviders(<CustomMessageInput />)
|
|
86
|
+
|
|
87
|
+
expect(screen.getByTestId('simple-attachment-selector')).toBeInTheDocument()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('renders adjacent actions inside the disabled container when frozen', () => {
|
|
91
|
+
mockChannelData = { frozen: true }
|
|
92
|
+
|
|
93
|
+
const { container } = renderWithProviders(
|
|
94
|
+
<CustomMessageInput
|
|
95
|
+
renderActions={() => (
|
|
96
|
+
<button data-testid="custom-action" type="button">
|
|
97
|
+
Action
|
|
98
|
+
</button>
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const messageInput = container.firstElementChild
|
|
104
|
+
const action = screen.getByTestId('custom-action')
|
|
105
|
+
|
|
106
|
+
expect(messageInput).toHaveAttribute('aria-disabled', 'true')
|
|
107
|
+
expect(messageInput?.contains(action)).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('renders adjacent actions in the interactive container when not frozen', () => {
|
|
111
|
+
mockChannelData = {}
|
|
112
|
+
|
|
113
|
+
const { container } = renderWithProviders(
|
|
114
|
+
<CustomMessageInput
|
|
115
|
+
renderActions={() => (
|
|
116
|
+
<button data-testid="custom-action" type="button">
|
|
117
|
+
Action
|
|
118
|
+
</button>
|
|
119
|
+
)}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const messageInput = container.firstElementChild
|
|
124
|
+
expect(messageInput).not.toHaveAttribute('aria-disabled')
|
|
125
|
+
expect(screen.getByTestId('custom-action')).toBeInTheDocument()
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
QuotedMessagePreview,
|
|
7
7
|
SimpleAttachmentSelector,
|
|
8
8
|
TextareaComposer,
|
|
9
|
+
useChannelStateContext,
|
|
9
10
|
useMessageComposerHasSendableData,
|
|
10
11
|
useMessageInputContext,
|
|
11
12
|
} from 'stream-chat-react'
|
|
@@ -13,8 +14,11 @@ import {
|
|
|
13
14
|
import { CustomLinkPreviewList } from '../CustomLinkPreviewList'
|
|
14
15
|
|
|
15
16
|
const CustomMessageInputInner: React.FC = () => {
|
|
17
|
+
const { channel } = useChannelStateContext()
|
|
18
|
+
const isFrozen = channel?.data?.frozen === true
|
|
16
19
|
const { handleSubmit } = useMessageInputContext()
|
|
17
20
|
const hasSendableData = useMessageComposerHasSendableData()
|
|
21
|
+
const isSendDisabled = isFrozen || !hasSendableData
|
|
18
22
|
|
|
19
23
|
return (
|
|
20
24
|
<>
|
|
@@ -28,20 +32,23 @@ const CustomMessageInputInner: React.FC = () => {
|
|
|
28
32
|
<div className="flex">
|
|
29
33
|
<div className="w-full ml-2 mr-4 self-center leading-[0]">
|
|
30
34
|
<TextareaComposer
|
|
35
|
+
aria-disabled={isFrozen || undefined}
|
|
31
36
|
className="w-full resize-none outline-none leading-6"
|
|
32
37
|
// While this might usually be considered an anti-pattern, in most
|
|
33
38
|
// cases, when a message thread is rendered, we want the input to
|
|
34
39
|
// gain focus automatically.
|
|
35
40
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
36
|
-
autoFocus
|
|
41
|
+
autoFocus={!isFrozen}
|
|
37
42
|
maxRows={4}
|
|
43
|
+
readOnly={isFrozen}
|
|
44
|
+
tabIndex={isFrozen ? -1 : undefined}
|
|
38
45
|
/>
|
|
39
46
|
</div>
|
|
40
47
|
<button
|
|
41
48
|
aria-label="Send"
|
|
42
49
|
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"
|
|
43
50
|
data-testid="send-button"
|
|
44
|
-
disabled={
|
|
51
|
+
disabled={isSendDisabled}
|
|
45
52
|
onClick={handleSubmit}
|
|
46
53
|
type="button"
|
|
47
54
|
>
|
|
@@ -59,9 +66,19 @@ export interface CustomMessageInputProps {
|
|
|
59
66
|
|
|
60
67
|
export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
|
|
61
68
|
renderActions,
|
|
62
|
-
}) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
}) => {
|
|
70
|
+
const { channel } = useChannelStateContext()
|
|
71
|
+
const isFrozen = channel?.data?.frozen === true
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
// @ts-expect-error Only React 19 onwards has `inert` in its types.
|
|
76
|
+
inert={isFrozen ? '' : undefined}
|
|
77
|
+
aria-disabled={isFrozen || undefined}
|
|
78
|
+
className="message-input flex items-center gap-2 p-4 aria-disabled:opacity-40"
|
|
79
|
+
>
|
|
80
|
+
{renderActions?.()}
|
|
81
|
+
<MessageInput Input={CustomMessageInputInner} />
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|