@linktr.ee/messaging-react 1.25.0 → 1.26.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/dist/assets/index.css +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +891 -824
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +31 -0
- package/src/components/ChannelList/index.test.tsx +18 -0
- package/src/components/ChannelList/index.tsx +2 -0
- package/src/components/CustomSystemMessage/CustomSystemMessage.stories.tsx +8 -0
- package/src/components/CustomSystemMessage/CustomSystemMessage.test.tsx +48 -2
- package/src/components/CustomSystemMessage/index.tsx +119 -13
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/stream-custom-data.ts +3 -0
- package/src/styles.css +0 -30
- package/src/types.ts +26 -0
package/package.json
CHANGED
|
@@ -129,6 +129,37 @@ describe('CustomChannelPreview', () => {
|
|
|
129
129
|
).not.toBeInTheDocument()
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
+
it('ignores age safety system messages when resolving the channel preview text', () => {
|
|
133
|
+
const channel = createMockChannel([
|
|
134
|
+
{
|
|
135
|
+
id: 'msg-1',
|
|
136
|
+
text: 'Still available to chat',
|
|
137
|
+
type: 'regular',
|
|
138
|
+
created_at: new Date('2026-01-01T00:00:00.000Z'),
|
|
139
|
+
user: { id: 'participant-1', name: 'Alice' },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'msg-2',
|
|
143
|
+
type: 'system',
|
|
144
|
+
created_at: new Date('2026-01-01T00:01:00.000Z'),
|
|
145
|
+
metadata: {
|
|
146
|
+
custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
renderWithProviders(
|
|
152
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
expect(screen.getByText('Still available to chat')).toBeInTheDocument()
|
|
156
|
+
expect(
|
|
157
|
+
screen.queryByText(
|
|
158
|
+
'This user isn’t able to reply because they don’t meet our age safety guidelines.'
|
|
159
|
+
)
|
|
160
|
+
).not.toBeInTheDocument()
|
|
161
|
+
})
|
|
162
|
+
|
|
132
163
|
it('shows fallback text when all messages are system messages', () => {
|
|
133
164
|
const channel = createMockChannel([
|
|
134
165
|
{
|
|
@@ -76,4 +76,22 @@ describe('ChannelList', () => {
|
|
|
76
76
|
expect(streamProps.onMessageNew).toBe(onMessageNew)
|
|
77
77
|
expect(streamProps.onAddedToChannel).toBe(onAddedToChannel)
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
it('passes channelRenderFilterFn through to Stream ChannelList', () => {
|
|
81
|
+
const filterFn = vi.fn()
|
|
82
|
+
|
|
83
|
+
renderWithProviders(
|
|
84
|
+
React.createElement(ChannelList, {
|
|
85
|
+
...defaultProps,
|
|
86
|
+
channelRenderFilterFn: filterFn,
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
expect(streamChannelListMock).toHaveBeenCalledOnce()
|
|
91
|
+
const streamProps = streamChannelListMock.mock.calls[0][0] as {
|
|
92
|
+
channelRenderFilterFn?: unknown
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expect(streamProps.channelRenderFilterFn).toBe(filterFn)
|
|
96
|
+
})
|
|
79
97
|
})
|
|
@@ -21,6 +21,7 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
21
21
|
allowNewMessagesFromUnfilteredChannels = false,
|
|
22
22
|
onMessageNew,
|
|
23
23
|
onAddedToChannel,
|
|
24
|
+
channelRenderFilterFn,
|
|
24
25
|
sort = DEFAULT_SORT,
|
|
25
26
|
className,
|
|
26
27
|
customEmptyStateIndicator,
|
|
@@ -71,6 +72,7 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
71
72
|
}
|
|
72
73
|
onMessageNew={onMessageNew}
|
|
73
74
|
onAddedToChannel={onAddedToChannel}
|
|
75
|
+
channelRenderFilterFn={channelRenderFilterFn}
|
|
74
76
|
Preview={CustomChannelPreview}
|
|
75
77
|
EmptyStateIndicator={customEmptyStateIndicator}
|
|
76
78
|
/>
|
|
@@ -47,6 +47,14 @@ DmAgentResumed.args = createStoryProps({
|
|
|
47
47
|
},
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
export const AgeSafetyBlocked: StoryFn<EventComponentProps> = Template.bind({})
|
|
51
|
+
AgeSafetyBlocked.args = createStoryProps({
|
|
52
|
+
text: ' ',
|
|
53
|
+
metadata: {
|
|
54
|
+
custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
50
58
|
export const GenericFallback: StoryFn<EventComponentProps> = Template.bind({})
|
|
51
59
|
GenericFallback.args = createStoryProps({
|
|
52
60
|
text: 'Message activity event',
|
|
@@ -60,6 +60,51 @@ describe('CustomSystemMessage', () => {
|
|
|
60
60
|
expect(dmAgentSystemMessage).toHaveTextContent('DM Agent resumed')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
it('renders the age safety blocked subtype with fallback copy and warning treatment', () => {
|
|
64
|
+
const { container } = renderWithProviders(
|
|
65
|
+
<CustomSystemMessage
|
|
66
|
+
{...createProps({
|
|
67
|
+
text: ' ',
|
|
68
|
+
metadata: {
|
|
69
|
+
custom_type: 'SYSTEM_AGE_SAFETY_BLOCKED',
|
|
70
|
+
},
|
|
71
|
+
})}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const ageSafetySystemMessage = screen.getByTestId(
|
|
76
|
+
'age-safety-system-message'
|
|
77
|
+
)
|
|
78
|
+
expect(ageSafetySystemMessage).toHaveAttribute(
|
|
79
|
+
'data-age-safety-system-type',
|
|
80
|
+
'SYSTEM_AGE_SAFETY_BLOCKED'
|
|
81
|
+
)
|
|
82
|
+
expect(ageSafetySystemMessage).toHaveTextContent(
|
|
83
|
+
'This user isn’t able to reply because they don’t meet our age safety guidelines.'
|
|
84
|
+
)
|
|
85
|
+
const emphasisLink = screen.getByRole('link', {
|
|
86
|
+
name: 'age safety guidelines.',
|
|
87
|
+
})
|
|
88
|
+
expect(emphasisLink).toHaveClass('mes-age-safety-system-message__emphasis')
|
|
89
|
+
expect(emphasisLink).toHaveAttribute(
|
|
90
|
+
'href',
|
|
91
|
+
'https://linktr.ee/s/about/contact'
|
|
92
|
+
)
|
|
93
|
+
expect(emphasisLink).toHaveAttribute('target', '_blank')
|
|
94
|
+
expect(emphasisLink).toHaveAttribute('rel', 'noopener noreferrer')
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByTestId('age-safety-system-message-icon')
|
|
97
|
+
).toBeInTheDocument()
|
|
98
|
+
expect(
|
|
99
|
+
container.querySelector('.mes-age-safety-system-message__icon')
|
|
100
|
+
).toBeInTheDocument()
|
|
101
|
+
expect(
|
|
102
|
+
container.querySelector(
|
|
103
|
+
'[data-testid="age-safety-system-message-icon"] path[opacity="0.2"]'
|
|
104
|
+
)
|
|
105
|
+
).toBeInTheDocument()
|
|
106
|
+
})
|
|
107
|
+
|
|
63
108
|
it('falls back to legacy generic rendering when subtype is absent or unknown', () => {
|
|
64
109
|
const { container } = renderWithProviders(
|
|
65
110
|
<CustomSystemMessage
|
|
@@ -71,11 +116,12 @@ describe('CustomSystemMessage', () => {
|
|
|
71
116
|
/>
|
|
72
117
|
)
|
|
73
118
|
|
|
74
|
-
expect(
|
|
119
|
+
expect(
|
|
120
|
+
screen.queryByTestId('dm-agent-system-message')
|
|
121
|
+
).not.toBeInTheDocument()
|
|
75
122
|
expect(screen.getByText('Legacy system message')).toBeInTheDocument()
|
|
76
123
|
expect(
|
|
77
124
|
container.querySelector('.str-chat__message--system__line')
|
|
78
125
|
).toBeInTheDocument()
|
|
79
126
|
})
|
|
80
|
-
|
|
81
127
|
})
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { SparkleIcon } from '@phosphor-icons/react'
|
|
1
|
+
import { ProhibitIcon, SparkleIcon } from '@phosphor-icons/react'
|
|
2
2
|
import { MessageTimestamp, type EventComponentProps } from 'stream-chat-react'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgeSafetySystemType,
|
|
6
|
+
DmAgentSystemType,
|
|
7
|
+
} from '../../stream-custom-data'
|
|
5
8
|
|
|
6
9
|
const DM_AGENT_SYSTEM_TYPES: readonly DmAgentSystemType[] = [
|
|
7
10
|
'SYSTEM_DM_AGENT_PAUSED',
|
|
@@ -14,51 +17,154 @@ const DM_AGENT_SYSTEM_MESSAGE_FALLBACK_TEXT: Record<DmAgentSystemType, string> =
|
|
|
14
17
|
SYSTEM_DM_AGENT_RESUMED: 'DM Agent has rejoined the conversation',
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
const AGE_SAFETY_SYSTEM_TYPES: readonly AgeSafetySystemType[] = [
|
|
21
|
+
'SYSTEM_AGE_SAFETY_BLOCKED',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const AGE_SAFETY_SYSTEM_MESSAGE_FALLBACK_TEXT: Record<
|
|
25
|
+
AgeSafetySystemType,
|
|
26
|
+
string
|
|
27
|
+
> = {
|
|
28
|
+
SYSTEM_AGE_SAFETY_BLOCKED:
|
|
29
|
+
'This user isn’t able to reply because they don’t meet our age safety guidelines.',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS = 'age safety guidelines.'
|
|
33
|
+
const AGE_SAFETY_SYSTEM_MESSAGE_URL = 'https://linktr.ee/s/about/contact'
|
|
34
|
+
|
|
17
35
|
const isDmAgentSystemType = (
|
|
18
36
|
value: string | undefined
|
|
19
37
|
): value is DmAgentSystemType => {
|
|
20
38
|
return DM_AGENT_SYSTEM_TYPES.includes(value as DmAgentSystemType)
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
const
|
|
41
|
+
const isAgeSafetySystemType = (
|
|
42
|
+
value: string | undefined
|
|
43
|
+
): value is AgeSafetySystemType => {
|
|
44
|
+
return AGE_SAFETY_SYSTEM_TYPES.includes(value as AgeSafetySystemType)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type CustomSystemMessageVariant =
|
|
48
|
+
| {
|
|
49
|
+
kind: 'dm-agent'
|
|
50
|
+
type: DmAgentSystemType
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
kind: 'age-safety'
|
|
54
|
+
type: AgeSafetySystemType
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const getCustomSystemMessageVariant = (
|
|
24
58
|
message: EventComponentProps['message']
|
|
25
|
-
):
|
|
59
|
+
): CustomSystemMessageVariant | undefined => {
|
|
26
60
|
const metadataCustomType = message.metadata?.custom_type
|
|
27
61
|
if (isDmAgentSystemType(metadataCustomType)) {
|
|
28
|
-
return
|
|
62
|
+
return {
|
|
63
|
+
kind: 'dm-agent',
|
|
64
|
+
type: metadataCustomType,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isAgeSafetySystemType(metadataCustomType)) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'age-safety',
|
|
71
|
+
type: metadataCustomType,
|
|
72
|
+
}
|
|
29
73
|
}
|
|
30
74
|
|
|
31
75
|
const fallbackType = message.dm_agent_system_type
|
|
32
76
|
if (isDmAgentSystemType(fallbackType)) {
|
|
33
|
-
return
|
|
77
|
+
return {
|
|
78
|
+
kind: 'dm-agent',
|
|
79
|
+
type: fallbackType,
|
|
80
|
+
}
|
|
34
81
|
}
|
|
35
82
|
|
|
36
83
|
return undefined
|
|
37
84
|
}
|
|
38
85
|
|
|
86
|
+
const renderAgeSafetyMessageText = (messageText: string): React.ReactNode => {
|
|
87
|
+
const emphasisIndex = messageText.indexOf(AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS)
|
|
88
|
+
if (emphasisIndex === -1) {
|
|
89
|
+
return messageText
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const emphasisEndIndex =
|
|
93
|
+
emphasisIndex + AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS.length
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
{messageText.slice(0, emphasisIndex)}
|
|
98
|
+
<a
|
|
99
|
+
href={AGE_SAFETY_SYSTEM_MESSAGE_URL}
|
|
100
|
+
target="_blank"
|
|
101
|
+
rel="noopener noreferrer"
|
|
102
|
+
className="mes-age-safety-system-message__emphasis font-medium text-inherit underline"
|
|
103
|
+
>
|
|
104
|
+
{AGE_SAFETY_SYSTEM_MESSAGE_EMPHASIS}
|
|
105
|
+
</a>
|
|
106
|
+
{messageText.slice(emphasisEndIndex)}
|
|
107
|
+
</>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
39
111
|
export const CustomSystemMessage: React.FC<EventComponentProps> = (props) => {
|
|
40
112
|
const isDateHidden = props.message.hide_date === true
|
|
41
|
-
const
|
|
113
|
+
const customSystemMessageVariant = getCustomSystemMessageVariant(
|
|
114
|
+
props.message
|
|
115
|
+
)
|
|
42
116
|
|
|
43
|
-
if (
|
|
117
|
+
if (customSystemMessageVariant?.kind === 'dm-agent') {
|
|
44
118
|
const messageText =
|
|
45
119
|
props.message.text?.trim() ||
|
|
46
|
-
DM_AGENT_SYSTEM_MESSAGE_FALLBACK_TEXT[
|
|
120
|
+
DM_AGENT_SYSTEM_MESSAGE_FALLBACK_TEXT[customSystemMessageVariant.type]
|
|
47
121
|
|
|
48
122
|
return (
|
|
49
123
|
<div className="str-chat__message--system" data-testid="message-system">
|
|
50
124
|
<div
|
|
51
|
-
className="mes-dm-agent-system-message"
|
|
125
|
+
className="mes-dm-agent-system-message mx-auto mb-2 inline-flex w-fit max-w-[min(100%,480px)] items-center justify-center gap-[10px] rounded-[12px] border border-[rgba(0,0,0,0.08)] p-3 text-[rgba(0,0,0,0.55)]"
|
|
52
126
|
data-testid="dm-agent-system-message"
|
|
53
|
-
data-dm-agent-system-type={
|
|
127
|
+
data-dm-agent-system-type={customSystemMessageVariant.type}
|
|
54
128
|
>
|
|
55
129
|
<SparkleIcon
|
|
56
130
|
size={16}
|
|
57
131
|
weight="regular"
|
|
58
132
|
aria-hidden
|
|
59
|
-
className="mes-dm-agent-system-message__sparkle"
|
|
133
|
+
className="mes-dm-agent-system-message__sparkle shrink-0"
|
|
134
|
+
/>
|
|
135
|
+
<p className="mes-dm-agent-system-message__text m-0 text-center text-[14px] font-normal leading-5 tracking-[0.21px]">
|
|
136
|
+
{messageText}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
{!isDateHidden && <MessageTimestamp message={props.message} />}
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (customSystemMessageVariant?.kind === 'age-safety') {
|
|
145
|
+
const messageText =
|
|
146
|
+
props.message.text?.trim() ||
|
|
147
|
+
AGE_SAFETY_SYSTEM_MESSAGE_FALLBACK_TEXT[customSystemMessageVariant.type]
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="str-chat__message--system" data-testid="message-system">
|
|
151
|
+
<div
|
|
152
|
+
className="mes-age-safety-system-message box-border mx-auto mb-2 flex w-full max-w-[329px] items-start justify-center gap-3 rounded-[12px] border border-[var(--border-secondary,rgba(0,0,0,0.08))] bg-[var(--bg-warning-subtle,#fef3c6)] px-2 py-4 pl-5 text-[color:var(--text-warning-on-warning,#894b00)]"
|
|
153
|
+
data-testid="age-safety-system-message"
|
|
154
|
+
data-age-safety-system-type={customSystemMessageVariant.type}
|
|
155
|
+
>
|
|
156
|
+
<ProhibitIcon
|
|
157
|
+
size={24}
|
|
158
|
+
weight="duotone"
|
|
159
|
+
aria-hidden
|
|
160
|
+
className="mes-age-safety-system-message__icon shrink-0 text-[color:var(--text-warning-on-warning,#894b00)]"
|
|
161
|
+
data-testid="age-safety-system-message-icon"
|
|
60
162
|
/>
|
|
61
|
-
<
|
|
163
|
+
<div className="mes-age-safety-system-message__content min-w-0 flex-[1_0_0]">
|
|
164
|
+
<p className="m-0 text-balance text-left text-[12px] font-normal leading-4 tracking-[0.21px] text-[color:var(--text-warning-on-warning,#894b00)]">
|
|
165
|
+
{renderAgeSafetyMessageText(messageText)}
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
62
168
|
</div>
|
|
63
169
|
{!isDateHidden && <MessageTimestamp message={props.message} />}
|
|
64
170
|
</div>
|
|
@@ -27,6 +27,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
27
27
|
CustomChannelEmptyState,
|
|
28
28
|
showChannelList = true,
|
|
29
29
|
filters,
|
|
30
|
+
channelRenderFilterFn,
|
|
30
31
|
channelListCustomEmptyStateIndicator,
|
|
31
32
|
onDeleteConversationClick,
|
|
32
33
|
onBlockParticipantClick,
|
|
@@ -455,6 +456,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
455
456
|
onChannelSelect={handleChannelSelect}
|
|
456
457
|
selectedChannel={selectedChannel || undefined}
|
|
457
458
|
filters={channelFilters}
|
|
459
|
+
channelRenderFilterFn={channelRenderFilterFn}
|
|
458
460
|
customEmptyStateIndicator={channelListCustomEmptyStateIndicator}
|
|
459
461
|
renderMessagePreview={renderMessagePreview}
|
|
460
462
|
/>
|
|
@@ -18,10 +18,13 @@ export type DmAgentSystemType =
|
|
|
18
18
|
| 'SYSTEM_DM_AGENT_PAUSED'
|
|
19
19
|
| 'SYSTEM_DM_AGENT_RESUMED'
|
|
20
20
|
|
|
21
|
+
export type AgeSafetySystemType = 'SYSTEM_AGE_SAFETY_BLOCKED'
|
|
22
|
+
|
|
21
23
|
export type MessageCustomType =
|
|
22
24
|
| 'MESSAGE_TIP'
|
|
23
25
|
| 'MESSAGE_PAID'
|
|
24
26
|
| 'MESSAGE_CHATBOT'
|
|
27
|
+
| AgeSafetySystemType
|
|
25
28
|
| DmAgentSystemType
|
|
26
29
|
|
|
27
30
|
/**
|
package/src/styles.css
CHANGED
|
@@ -231,33 +231,3 @@
|
|
|
231
231
|
color: #016e1a;
|
|
232
232
|
background-color: #dbf0e0;
|
|
233
233
|
}
|
|
234
|
-
|
|
235
|
-
/* DM Agent pause/resume system message variants */
|
|
236
|
-
.mes-dm-agent-system-message {
|
|
237
|
-
display: inline-flex;
|
|
238
|
-
align-items: center;
|
|
239
|
-
justify-content: center;
|
|
240
|
-
gap: 10px;
|
|
241
|
-
margin-inline: auto;
|
|
242
|
-
margin-bottom: 8px;
|
|
243
|
-
width: fit-content;
|
|
244
|
-
max-width: min(100%, 480px);
|
|
245
|
-
padding: 12px;
|
|
246
|
-
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
247
|
-
border-radius: 12px;
|
|
248
|
-
color: rgba(0, 0, 0, 0.55);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.mes-dm-agent-system-message__sparkle {
|
|
252
|
-
flex-shrink: 0;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
.mes-dm-agent-system-message__text {
|
|
256
|
-
margin: 0;
|
|
257
|
-
font-size: 14px;
|
|
258
|
-
font-weight: 400;
|
|
259
|
-
line-height: 20px;
|
|
260
|
-
letter-spacing: 0.21px;
|
|
261
|
-
color: rgba(0, 0, 0, 0.55);
|
|
262
|
-
text-align: center;
|
|
263
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -91,6 +91,20 @@ export interface ChannelListProps {
|
|
|
91
91
|
* channel belongs in the current list before inserting it.
|
|
92
92
|
*/
|
|
93
93
|
onAddedToChannel?: StreamChannelListProps['onAddedToChannel']
|
|
94
|
+
/**
|
|
95
|
+
* Client-side filter applied before rendering the channel list.
|
|
96
|
+
* Websocket events can add channels to the list that bypass server-side
|
|
97
|
+
* query filters. Use this to enforce visibility rules that can't be
|
|
98
|
+
* automatically derived from the filters prop (e.g. $or conditions).
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Hide channels where the visitor hasn't sent a message yet,
|
|
102
|
+
* // but keep legacy channels that predate the has_visitor_message field
|
|
103
|
+
* channelRenderFilterFn={(channels) =>
|
|
104
|
+
* channels.filter(ch => ch.data?.has_visitor_message !== false)
|
|
105
|
+
* }
|
|
106
|
+
*/
|
|
107
|
+
channelRenderFilterFn?: (channels: Channel[]) => Channel[]
|
|
94
108
|
/**
|
|
95
109
|
* Sort order for the channel list query.
|
|
96
110
|
* Defaults to `{ last_message_at: -1 }` (most recent first).
|
|
@@ -282,6 +296,18 @@ export interface MessagingShellProps extends ChannelViewPassthroughProps {
|
|
|
282
296
|
*/
|
|
283
297
|
filters?: ChannelFilters
|
|
284
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Client-side filter applied before rendering the channel list.
|
|
301
|
+
* Websocket events can add channels to the list that bypass server-side
|
|
302
|
+
* query filters. Use this to enforce visibility rules client-side.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* channelRenderFilterFn={(channels) =>
|
|
306
|
+
* channels.filter(ch => ch.data?.has_visitor_message !== false)
|
|
307
|
+
* }
|
|
308
|
+
*/
|
|
309
|
+
channelRenderFilterFn?: (channels: Channel[]) => Channel[]
|
|
310
|
+
|
|
285
311
|
/**
|
|
286
312
|
* Custom empty state indicator component to render when the channel list is empty.
|
|
287
313
|
* Useful for showing a custom empty state indicator when the channel list is empty.
|