@linktr.ee/messaging-react 1.25.1 → 1.26.1

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.25.1",
3
+ "version": "1.26.1",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
  />
@@ -54,8 +54,21 @@ vi.mock('./CustomDateSeparator', () => ({
54
54
  CustomDateSeparator: () => <div data-testid="custom-date-separator" />,
55
55
  }))
56
56
 
57
+ vi.mock('./ChannelInfoDialog', () => ({
58
+ ChannelInfoDialog: () => <div data-testid="channel-info-dialog" />,
59
+ }))
60
+
61
+ const avatarRenderCalls: Array<Record<string, unknown>> = []
57
62
  vi.mock('./Avatar', () => ({
58
- Avatar: () => <div data-testid="avatar" />,
63
+ Avatar: (props: Record<string, unknown>) => {
64
+ avatarRenderCalls.push({ ...props })
65
+ return <div data-testid="avatar" />
66
+ },
67
+ }))
68
+
69
+ let mockIsStarred = false
70
+ vi.mock('../hooks/useChannelStar', () => ({
71
+ useChannelStar: () => mockIsStarred,
59
72
  }))
60
73
 
61
74
  const createChannel = () =>
@@ -83,6 +96,8 @@ const createChannel = () =>
83
96
  describe('ChannelView', () => {
84
97
  beforeEach(() => {
85
98
  activeChannel = undefined
99
+ avatarRenderCalls.length = 0
100
+ mockIsStarred = false
86
101
  })
87
102
 
88
103
  it('renders conversation footer between message list and message input', () => {
@@ -122,6 +137,40 @@ describe('ChannelView', () => {
122
137
  expect(screen.queryByTestId('conversation-footer')).not.toBeInTheDocument()
123
138
  })
124
139
 
140
+ it('does not pass starred to avatar when showStarButton is false', () => {
141
+ mockIsStarred = true
142
+ const channel = createChannel()
143
+
144
+ renderWithProviders(
145
+ <ChannelView channel={channel} showStarButton={false} />
146
+ )
147
+
148
+ expect(avatarRenderCalls.length).toBeGreaterThan(0)
149
+ expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
150
+ })
151
+
152
+ it('passes starred to avatar when showStarButton is true and channel is starred', () => {
153
+ mockIsStarred = true
154
+ const channel = createChannel()
155
+
156
+ renderWithProviders(
157
+ <ChannelView channel={channel} showStarButton={true} />
158
+ )
159
+
160
+ expect(avatarRenderCalls.length).toBeGreaterThan(0)
161
+ expect(avatarRenderCalls.every((call) => call.starred === true)).toBe(true)
162
+ })
163
+
164
+ it('does not pass starred to avatar by default (showStarButton defaults to false)', () => {
165
+ mockIsStarred = true
166
+ const channel = createChannel()
167
+
168
+ renderWithProviders(<ChannelView channel={channel} />)
169
+
170
+ expect(avatarRenderCalls.length).toBeGreaterThan(0)
171
+ expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
172
+ })
173
+
125
174
  it('keeps channel banner and input action renderers working', () => {
126
175
  const channel = createChannel()
127
176
  const renderMessageInputActions = vi.fn((currentChannel: Channel) => (
@@ -97,7 +97,7 @@ const CustomChannelHeader: React.FC<{
97
97
  id={participant?.user?.id || channel.id || 'unknown'}
98
98
  name={participantName}
99
99
  image={participantImage}
100
- starred={isStarred}
100
+ starred={showStarButton && isStarred}
101
101
  size={40}
102
102
  />
103
103
  <h1 className="text-xs font-medium text-black/90">
@@ -150,7 +150,7 @@ const CustomChannelHeader: React.FC<{
150
150
  id={participant?.user?.id || channel.id || 'unknown'}
151
151
  name={participantName}
152
152
  image={participantImage}
153
- starred={isStarred}
153
+ starred={showStarButton && isStarred}
154
154
  size={40}
155
155
  />
156
156
  <div className="min-w-0">
@@ -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
  />
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.