@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/dist/index.js +395 -381
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelList/index.test.tsx +14 -4
- package/src/components/ChannelList/index.tsx +17 -1
- package/src/components/ChannelView.tsx +5 -6
- package/src/hooks/useRestorePendingMessages.test.ts +104 -0
- package/src/hooks/useRestorePendingMessages.ts +19 -0
package/package.json
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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={
|
|
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?: (
|
|
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
|
+
}
|