@linktr.ee/messaging-react 1.28.1 → 1.29.0-rc-1776325810

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.28.1",
3
+ "version": "1.29.0-rc-1776325810",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,4 +1,5 @@
1
1
  import React from 'react'
2
+ import type { Channel } from 'stream-chat'
2
3
  import { describe, expect, it, vi, beforeEach } from 'vitest'
3
4
 
4
5
  import { renderWithProviders } from '../../test/utils'
@@ -77,8 +78,8 @@ describe('ChannelList', () => {
77
78
  expect(streamProps.onAddedToChannel).toBe(onAddedToChannel)
78
79
  })
79
80
 
80
- it('passes channelRenderFilterFn through to Stream ChannelList', () => {
81
- const filterFn = vi.fn()
81
+ it('wraps channelRenderFilterFn to restore pending messages and delegates to consumer filter', () => {
82
+ const filterFn = vi.fn((channels: Channel[]) => channels)
82
83
 
83
84
  renderWithProviders(
84
85
  React.createElement(ChannelList, {
@@ -89,9 +90,18 @@ describe('ChannelList', () => {
89
90
 
90
91
  expect(streamChannelListMock).toHaveBeenCalledOnce()
91
92
  const streamProps = streamChannelListMock.mock.calls[0][0] as {
92
- channelRenderFilterFn?: unknown
93
+ channelRenderFilterFn?: (channels: unknown[]) => unknown[]
93
94
  }
94
95
 
95
- expect(streamProps.channelRenderFilterFn).toBe(filterFn)
96
+ // The wrapper should not be the same reference as the original filter
97
+ expect(streamProps.channelRenderFilterFn).not.toBe(filterFn)
98
+ expect(typeof streamProps.channelRenderFilterFn).toBe('function')
99
+
100
+ // When the wrapper is called, it should delegate to the consumer's filter
101
+ const mockChannels = [
102
+ { cid: 'ch-1', state: { pending_messages: [], addMessageSorted: vi.fn() } },
103
+ ]
104
+ streamProps.channelRenderFilterFn!(mockChannels)
105
+ expect(filterFn).toHaveBeenCalledWith(mockChannels)
96
106
  })
97
107
  })
@@ -1,7 +1,9 @@
1
1
  import classNames from 'classnames'
2
2
  import React from 'react'
3
+ import type { Channel } from 'stream-chat'
3
4
  import { ChannelList as StreamChannelList } from 'stream-chat-react'
4
5
 
6
+ import { restorePendingMessages } from '../../hooks/useRestorePendingMessages'
5
7
  import { useMessagingContext } from '../../providers/MessagingProvider'
6
8
  import type { ChannelListProps } from '../../types'
7
9
 
@@ -34,6 +36,20 @@ export const ChannelList = React.memo<ChannelListProps>(
34
36
  // Get debug flag from context
35
37
  const { debug = false } = useMessagingContext()
36
38
 
39
+ // Wrap channelRenderFilterFn to restore pending messages for all channels
40
+ // as soon as they are loaded, without waiting for the user to click into each one.
41
+ const wrappedChannelRenderFilterFn = React.useCallback(
42
+ (channels: Channel[]) => {
43
+ for (const channel of channels) {
44
+ restorePendingMessages(channel)
45
+ }
46
+ return channelRenderFilterFn
47
+ ? channelRenderFilterFn(channels)
48
+ : channels
49
+ },
50
+ [channelRenderFilterFn]
51
+ )
52
+
37
53
  if (debug) {
38
54
  console.log('📺 [ChannelList] 🔄 RENDER START', {
39
55
  renderCount: renderCountRef.current,
@@ -72,7 +88,7 @@ export const ChannelList = React.memo<ChannelListProps>(
72
88
  }
73
89
  onMessageNew={onMessageNew}
74
90
  onAddedToChannel={onAddedToChannel}
75
- channelRenderFilterFn={channelRenderFilterFn}
91
+ channelRenderFilterFn={wrappedChannelRenderFilterFn}
76
92
  Preview={CustomChannelPreview}
77
93
  EmptyStateIndicator={customEmptyStateIndicator}
78
94
  />
@@ -1,8 +1,4 @@
1
- import {
2
- ArrowLeftIcon,
3
- DotsThreeIcon,
4
- StarIcon,
5
- } from '@phosphor-icons/react'
1
+ import { ArrowLeftIcon, DotsThreeIcon, StarIcon } from '@phosphor-icons/react'
6
2
  import classNames from 'classnames'
7
3
  import React, { useCallback, useRef } from 'react'
8
4
  import { Channel as ChannelType, LocalMessage } from 'stream-chat'
@@ -211,7 +207,10 @@ const ChannelViewInner: React.FC<{
211
207
  onReportParticipantClick?: () => void
212
208
  showStarButton?: boolean
213
209
  chatbotVotingEnabled?: boolean
214
- onAttachmentUnlock?: (message: LocalMessage, channel: ChannelType) => Promise<LockedAttachmentSource>
210
+ onAttachmentUnlock?: (
211
+ message: LocalMessage,
212
+ channel: ChannelType
213
+ ) => Promise<LockedAttachmentSource>
215
214
  onAttachmentDownload?: (message: LocalMessage, channel: ChannelType) => void
216
215
  renderChannelBanner?: () => React.ReactNode
217
216
  customProfileContent?: React.ReactNode
@@ -0,0 +1,104 @@
1
+ import type { Channel } from 'stream-chat'
2
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
3
+
4
+ import { restorePendingMessages } from './useRestorePendingMessages'
5
+
6
+ const createChannel = (
7
+ overrides: {
8
+ cid?: string
9
+ pending_messages?: Array<{
10
+ message: Record<string, unknown>
11
+ pending_message_metadata?: Record<string, string>
12
+ }>
13
+ } = {}
14
+ ) =>
15
+ ({
16
+ cid: overrides.cid ?? 'messaging:channel-1',
17
+ state: {
18
+ pending_messages: overrides.pending_messages ?? [],
19
+ addMessageSorted: vi.fn(),
20
+ },
21
+ }) as unknown as Channel
22
+
23
+ describe('restorePendingMessages', () => {
24
+ beforeEach(() => {
25
+ vi.clearAllMocks()
26
+ })
27
+
28
+ it('adds all pending messages to channel state', () => {
29
+ const pendingMsg = {
30
+ message: {
31
+ id: 'msg-1',
32
+ text: 'Hello',
33
+ },
34
+ }
35
+ const channel = createChannel({ pending_messages: [pendingMsg] })
36
+
37
+ restorePendingMessages(channel)
38
+
39
+ expect(channel.state.addMessageSorted).toHaveBeenCalledOnce()
40
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(
41
+ pendingMsg.message
42
+ )
43
+ })
44
+
45
+ it('restores multiple pending messages', () => {
46
+ const msg1 = {
47
+ message: {
48
+ id: 'msg-1',
49
+ text: 'First message',
50
+ },
51
+ }
52
+ const msg2 = {
53
+ message: {
54
+ id: 'msg-2',
55
+ text: 'Second message',
56
+ },
57
+ }
58
+ const channel = createChannel({ pending_messages: [msg1, msg2] })
59
+
60
+ restorePendingMessages(channel)
61
+
62
+ expect(channel.state.addMessageSorted).toHaveBeenCalledTimes(2)
63
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(msg1.message)
64
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(msg2.message)
65
+ })
66
+
67
+ it('does nothing when there are no pending messages', () => {
68
+ const channel = createChannel({ pending_messages: [] })
69
+
70
+ restorePendingMessages(channel)
71
+
72
+ expect(channel.state.addMessageSorted).not.toHaveBeenCalled()
73
+ })
74
+
75
+ it('does nothing when pending_messages is undefined', () => {
76
+ const channel = {
77
+ cid: 'messaging:channel-1',
78
+ state: {
79
+ pending_messages: undefined,
80
+ addMessageSorted: vi.fn(),
81
+ },
82
+ } as unknown as Channel
83
+
84
+ restorePendingMessages(channel)
85
+
86
+ expect(channel.state.addMessageSorted).not.toHaveBeenCalled()
87
+ })
88
+
89
+ it('handles pending messages with no metadata gracefully', () => {
90
+ const noMetadataMsg = {
91
+ message: { id: 'msg-1', text: 'No metadata' },
92
+ }
93
+ const channel = createChannel({
94
+ pending_messages: [noMetadataMsg],
95
+ })
96
+
97
+ restorePendingMessages(channel)
98
+
99
+ expect(channel.state.addMessageSorted).toHaveBeenCalledOnce()
100
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(
101
+ noMetadataMsg.message
102
+ )
103
+ })
104
+ })
@@ -0,0 +1,19 @@
1
+ import type { Channel } from 'stream-chat'
2
+
3
+ /**
4
+ * Restores pending messages into the channel's visible message list so they
5
+ * appear as if they were already sent.
6
+ *
7
+ * Stream's pending-messages feature removes messages from the channel view
8
+ * once client state is lost (e.g. page refresh). This function works around
9
+ * that limitation by reading `channel.state.pending_messages` and
10
+ * re-inserting them via `channel.state.addMessageSorted`.
11
+ */
12
+ export function restorePendingMessages(channel: Channel) {
13
+ const pendingMessages = channel.state.pending_messages
14
+ if (!pendingMessages?.length) return
15
+
16
+ for (const pending of pendingMessages) {
17
+ channel.state.addMessageSorted(pending.message)
18
+ }
19
+ }