@linktr.ee/messaging-react 1.24.2 → 1.24.3-rc-1773289717
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/index.d.ts +2 -1
- package/dist/index.js +906 -900
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelView.stories.tsx +27 -0
- package/src/components/ChannelView.test.tsx +147 -0
- package/src/components/ChannelView.tsx +6 -0
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -254,6 +254,33 @@ WithMessageActions.parameters = {
|
|
|
254
254
|
},
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
export const WithConversationFooter: StoryFn<TemplateProps> = Template.bind({})
|
|
258
|
+
WithConversationFooter.args = {
|
|
259
|
+
showBackButton: false,
|
|
260
|
+
renderConversationFooter: (channel) => (
|
|
261
|
+
<div className="mx-4 mb-2 rounded-xl border border-black/10 bg-[#F9F7F4] px-4 py-3">
|
|
262
|
+
<p className="text-sm text-charcoal">
|
|
263
|
+
You will get notified when a new reply arrives in this conversation.
|
|
264
|
+
</p>
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
className="mt-2 rounded-full bg-charcoal px-3 py-1.5 text-xs font-medium text-white"
|
|
268
|
+
onClick={() => console.log('Footer CTA clicked', channel.id)}
|
|
269
|
+
>
|
|
270
|
+
Open Notification Settings
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
),
|
|
274
|
+
}
|
|
275
|
+
WithConversationFooter.parameters = {
|
|
276
|
+
docs: {
|
|
277
|
+
description: {
|
|
278
|
+
story:
|
|
279
|
+
'Channel view with a custom footer rendered between the message list and message input via renderConversationFooter(channel).',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
|
|
257
284
|
export const WithMessageDecoration: StoryFn<TemplateProps> = Template.bind({})
|
|
258
285
|
WithMessageDecoration.args = {
|
|
259
286
|
showBackButton: false,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { Channel } from 'stream-chat'
|
|
3
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { renderWithProviders, screen } from '../test/utils'
|
|
6
|
+
|
|
7
|
+
import { ChannelView } from './ChannelView'
|
|
8
|
+
|
|
9
|
+
let activeChannel: unknown
|
|
10
|
+
|
|
11
|
+
vi.mock('stream-chat-react', () => ({
|
|
12
|
+
Channel: ({
|
|
13
|
+
channel,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
channel: unknown
|
|
17
|
+
children: React.ReactNode
|
|
18
|
+
}) => {
|
|
19
|
+
activeChannel = channel
|
|
20
|
+
return <div data-testid="channel">{children}</div>
|
|
21
|
+
},
|
|
22
|
+
Window: ({ children }: { children: React.ReactNode }) => (
|
|
23
|
+
<div data-testid="window">{children}</div>
|
|
24
|
+
),
|
|
25
|
+
MessageList: () => <div data-testid="message-list" />,
|
|
26
|
+
WithComponents: ({ children }: { children: React.ReactNode }) => (
|
|
27
|
+
<>{children}</>
|
|
28
|
+
),
|
|
29
|
+
useMessageContext: () => ({ message: { id: 'message-1', text: 'hello' } }),
|
|
30
|
+
useChannelStateContext: () => ({ channel: activeChannel }),
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
vi.mock('../providers/MessagingProvider', () => ({
|
|
34
|
+
useMessagingContext: () => ({ service: null, debug: false }),
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
vi.mock('./CustomMessageInput', () => ({
|
|
38
|
+
CustomMessageInput: ({
|
|
39
|
+
renderActions,
|
|
40
|
+
}: {
|
|
41
|
+
renderActions?: () => React.ReactNode
|
|
42
|
+
}) => <div data-testid="message-input">{renderActions?.()}</div>,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
vi.mock('./CustomMessage', () => ({
|
|
46
|
+
CustomMessage: () => <div data-testid="custom-message" />,
|
|
47
|
+
}))
|
|
48
|
+
|
|
49
|
+
vi.mock('./CustomSystemMessage', () => ({
|
|
50
|
+
CustomSystemMessage: () => <div data-testid="custom-system-message" />,
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
vi.mock('./CustomDateSeparator', () => ({
|
|
54
|
+
CustomDateSeparator: () => <div data-testid="custom-date-separator" />,
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
vi.mock('./Avatar', () => ({
|
|
58
|
+
Avatar: () => <div data-testid="avatar" />,
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const createChannel = () =>
|
|
62
|
+
({
|
|
63
|
+
id: 'channel-1',
|
|
64
|
+
cid: 'messaging:channel-1',
|
|
65
|
+
data: {},
|
|
66
|
+
_client: { userID: 'visitor-1' },
|
|
67
|
+
state: {
|
|
68
|
+
members: {
|
|
69
|
+
visitor: { user: { id: 'visitor-1', name: 'Visitor' }, role: 'owner' },
|
|
70
|
+
linker: { user: { id: 'linker-1', name: 'Linker' }, role: 'member' },
|
|
71
|
+
},
|
|
72
|
+
membership: {},
|
|
73
|
+
messages: [],
|
|
74
|
+
},
|
|
75
|
+
on: vi.fn(),
|
|
76
|
+
off: vi.fn(),
|
|
77
|
+
sendMessage: vi.fn(),
|
|
78
|
+
hide: vi.fn(),
|
|
79
|
+
pin: vi.fn(),
|
|
80
|
+
unpin: vi.fn(),
|
|
81
|
+
}) as unknown as Channel
|
|
82
|
+
|
|
83
|
+
describe('ChannelView', () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
activeChannel = undefined
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('renders conversation footer between message list and message input', () => {
|
|
89
|
+
const channel = createChannel()
|
|
90
|
+
const renderConversationFooter = vi.fn((currentChannel: Channel) => (
|
|
91
|
+
<div data-testid="conversation-footer">
|
|
92
|
+
footer-{currentChannel.id}
|
|
93
|
+
</div>
|
|
94
|
+
))
|
|
95
|
+
|
|
96
|
+
renderWithProviders(
|
|
97
|
+
<ChannelView
|
|
98
|
+
channel={channel}
|
|
99
|
+
renderConversationFooter={renderConversationFooter}
|
|
100
|
+
/>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const messageList = screen.getByTestId('message-list')
|
|
104
|
+
const conversationFooter = screen.getByTestId('conversation-footer')
|
|
105
|
+
const messageInput = screen.getByTestId('message-input')
|
|
106
|
+
|
|
107
|
+
expect(renderConversationFooter).toHaveBeenCalledWith(channel)
|
|
108
|
+
expect(conversationFooter).toHaveTextContent('footer-channel-1')
|
|
109
|
+
expect(
|
|
110
|
+
messageList.compareDocumentPosition(conversationFooter) &
|
|
111
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
112
|
+
).toBeTruthy()
|
|
113
|
+
expect(
|
|
114
|
+
conversationFooter.compareDocumentPosition(messageInput) &
|
|
115
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
116
|
+
).toBeTruthy()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('does not render conversation footer when no renderer is provided', () => {
|
|
120
|
+
renderWithProviders(<ChannelView channel={createChannel()} />)
|
|
121
|
+
|
|
122
|
+
expect(screen.queryByTestId('conversation-footer')).not.toBeInTheDocument()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('keeps channel banner and input action renderers working', () => {
|
|
126
|
+
const channel = createChannel()
|
|
127
|
+
const renderMessageInputActions = vi.fn((currentChannel: Channel) => (
|
|
128
|
+
<button data-testid="message-input-action">{currentChannel.id}</button>
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
renderWithProviders(
|
|
132
|
+
<ChannelView
|
|
133
|
+
channel={channel}
|
|
134
|
+
renderChannelBanner={() => (
|
|
135
|
+
<div data-testid="channel-banner">channel-banner</div>
|
|
136
|
+
)}
|
|
137
|
+
renderMessageInputActions={renderMessageInputActions}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
expect(screen.getByTestId('channel-banner')).toBeInTheDocument()
|
|
142
|
+
expect(screen.getByTestId('message-input-action')).toHaveTextContent(
|
|
143
|
+
'channel-1'
|
|
144
|
+
)
|
|
145
|
+
expect(renderMessageInputActions).toHaveBeenCalledWith(channel)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -524,6 +524,7 @@ const ChannelViewInner: React.FC<{
|
|
|
524
524
|
onBack?: () => void
|
|
525
525
|
showBackButton: boolean
|
|
526
526
|
renderMessageInputActions?: (channel: ChannelType) => React.ReactNode
|
|
527
|
+
renderConversationFooter?: (channel: ChannelType) => React.ReactNode
|
|
527
528
|
onLeaveConversation?: (channel: ChannelType) => void
|
|
528
529
|
onBlockParticipant?: (participantId?: string) => void
|
|
529
530
|
CustomChannelEmptyState?: React.ComponentType
|
|
@@ -543,6 +544,7 @@ const ChannelViewInner: React.FC<{
|
|
|
543
544
|
onBack,
|
|
544
545
|
showBackButton,
|
|
545
546
|
renderMessageInputActions,
|
|
547
|
+
renderConversationFooter,
|
|
546
548
|
onLeaveConversation,
|
|
547
549
|
onBlockParticipant,
|
|
548
550
|
showDeleteConversation = true,
|
|
@@ -640,6 +642,8 @@ const ChannelViewInner: React.FC<{
|
|
|
640
642
|
/>
|
|
641
643
|
</div>
|
|
642
644
|
|
|
645
|
+
{renderConversationFooter?.(channel)}
|
|
646
|
+
|
|
643
647
|
{/* Message Input */}
|
|
644
648
|
<CustomMessageInput
|
|
645
649
|
renderActions={() => renderMessageInputActions?.(channel)}
|
|
@@ -675,6 +679,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
675
679
|
onBack,
|
|
676
680
|
showBackButton = false,
|
|
677
681
|
renderMessageInputActions,
|
|
682
|
+
renderConversationFooter,
|
|
678
683
|
onLeaveConversation,
|
|
679
684
|
onBlockParticipant,
|
|
680
685
|
className,
|
|
@@ -755,6 +760,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
755
760
|
onBack={onBack}
|
|
756
761
|
showBackButton={showBackButton}
|
|
757
762
|
renderMessageInputActions={renderMessageInputActions}
|
|
763
|
+
renderConversationFooter={renderConversationFooter}
|
|
758
764
|
onLeaveConversation={onLeaveConversation}
|
|
759
765
|
onBlockParticipant={onBlockParticipant}
|
|
760
766
|
CustomChannelEmptyState={CustomChannelEmptyState}
|
|
@@ -19,6 +19,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
19
19
|
capabilities = {},
|
|
20
20
|
className,
|
|
21
21
|
renderMessageInputActions,
|
|
22
|
+
renderConversationFooter,
|
|
22
23
|
onChannelSelect,
|
|
23
24
|
onParticipantSelect,
|
|
24
25
|
initialParticipantFilter,
|
|
@@ -484,6 +485,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
484
485
|
onBack={handleBackToChannelList}
|
|
485
486
|
showBackButton={!directConversationMode}
|
|
486
487
|
renderMessageInputActions={renderMessageInputActions}
|
|
488
|
+
renderConversationFooter={renderConversationFooter}
|
|
487
489
|
renderChannelBanner={renderChannelBanner}
|
|
488
490
|
onLeaveConversation={handleLeaveConversation}
|
|
489
491
|
onBlockParticipant={handleBlockParticipant}
|
package/src/types.ts
CHANGED
|
@@ -88,6 +88,7 @@ export interface ChannelViewProps {
|
|
|
88
88
|
onBack?: () => void
|
|
89
89
|
showBackButton?: boolean
|
|
90
90
|
renderMessageInputActions?: (channel: Channel) => React.ReactNode
|
|
91
|
+
renderConversationFooter?: (channel: Channel) => React.ReactNode
|
|
91
92
|
onLeaveConversation?: (channel: Channel) => void
|
|
92
93
|
onBlockParticipant?: (participantId?: string) => void
|
|
93
94
|
className?: string
|
|
@@ -192,6 +193,7 @@ export interface ChannelViewProps {
|
|
|
192
193
|
export type ChannelViewPassthroughProps = Pick<
|
|
193
194
|
ChannelViewProps,
|
|
194
195
|
| 'renderMessageInputActions'
|
|
196
|
+
| 'renderConversationFooter'
|
|
195
197
|
| 'CustomChannelEmptyState'
|
|
196
198
|
| 'onDeleteConversationClick'
|
|
197
199
|
| 'onBlockParticipantClick'
|