@linktr.ee/messaging-react 1.28.0 → 1.29.0-rc-1776320021

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.0",
3
+ "version": "1.29.0-rc-1776320021",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,6 +17,7 @@ import {
17
17
  } from 'stream-chat-react'
18
18
 
19
19
  import { useChannelStar } from '../hooks/useChannelStar'
20
+ import { useRestorePendingMessages } from '../hooks/useRestorePendingMessages'
20
21
  import type { ChannelViewProps, LockedAttachmentSource } from '../types'
21
22
 
22
23
  import { Avatar } from './Avatar'
@@ -241,6 +242,7 @@ const ChannelViewInner: React.FC<{
241
242
  renderMessage,
242
243
  }) => {
243
244
  const { channel } = useChannelStateContext()
245
+ useRestorePendingMessages(channel)
244
246
  const infoDialogRef = useRef<HTMLDialogElement>(null)
245
247
 
246
248
  // Get participant info for info dialog
@@ -0,0 +1,136 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import type { Channel } from 'stream-chat'
3
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
4
+
5
+ import { useRestorePendingMessages } from './useRestorePendingMessages'
6
+
7
+ const createChannel = (
8
+ overrides: {
9
+ cid?: string
10
+ pending_messages?: Array<{
11
+ message: Record<string, unknown>
12
+ pending_message_metadata?: Record<string, string>
13
+ }>
14
+ } = {}
15
+ ) =>
16
+ ({
17
+ cid: overrides.cid ?? 'messaging:channel-1',
18
+ state: {
19
+ pending_messages: overrides.pending_messages ?? [],
20
+ addMessageSorted: vi.fn(),
21
+ },
22
+ }) as unknown as Channel
23
+
24
+ describe('useRestorePendingMessages', () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks()
27
+ })
28
+
29
+ it('adds all pending messages to channel state', () => {
30
+ const pendingMsg = {
31
+ message: {
32
+ id: 'msg-1',
33
+ text: 'Hello',
34
+ },
35
+ }
36
+ const channel = createChannel({ pending_messages: [pendingMsg] })
37
+
38
+ renderHook(() => useRestorePendingMessages(channel))
39
+
40
+ expect(channel.state.addMessageSorted).toHaveBeenCalledOnce()
41
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(
42
+ pendingMsg.message
43
+ )
44
+ })
45
+
46
+ it('restores multiple pending messages', () => {
47
+ const msg1 = {
48
+ message: {
49
+ id: 'msg-1',
50
+ text: 'First message',
51
+ },
52
+ }
53
+ const msg2 = {
54
+ message: {
55
+ id: 'msg-2',
56
+ text: 'Second message',
57
+ },
58
+ }
59
+ const channel = createChannel({ pending_messages: [msg1, msg2] })
60
+
61
+ renderHook(() => useRestorePendingMessages(channel))
62
+
63
+ expect(channel.state.addMessageSorted).toHaveBeenCalledTimes(2)
64
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(msg1.message)
65
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(msg2.message)
66
+ })
67
+
68
+ it('does nothing when there are no pending messages', () => {
69
+ const channel = createChannel({ pending_messages: [] })
70
+
71
+ renderHook(() => useRestorePendingMessages(channel))
72
+
73
+ expect(channel.state.addMessageSorted).not.toHaveBeenCalled()
74
+ })
75
+
76
+ it('only restores once per channel even if re-rendered', () => {
77
+ const pendingMsg = {
78
+ message: {
79
+ id: 'msg-1',
80
+ text: 'Hello',
81
+ },
82
+ }
83
+ const channel = createChannel({ pending_messages: [pendingMsg] })
84
+
85
+ const { rerender } = renderHook(() => useRestorePendingMessages(channel))
86
+
87
+ rerender()
88
+ rerender()
89
+
90
+ expect(channel.state.addMessageSorted).toHaveBeenCalledOnce()
91
+ })
92
+
93
+ it('restores again when switching to a different channel', () => {
94
+ const pendingMsg = {
95
+ message: {
96
+ id: 'msg-1',
97
+ text: 'Hello',
98
+ },
99
+ }
100
+ const channel1 = createChannel({
101
+ cid: 'messaging:channel-1',
102
+ pending_messages: [pendingMsg],
103
+ })
104
+ const channel2 = createChannel({
105
+ cid: 'messaging:channel-2',
106
+ pending_messages: [pendingMsg],
107
+ })
108
+
109
+ const { rerender } = renderHook(
110
+ ({ channel }) => useRestorePendingMessages(channel),
111
+ { initialProps: { channel: channel1 } }
112
+ )
113
+
114
+ expect(channel1.state.addMessageSorted).toHaveBeenCalledOnce()
115
+
116
+ rerender({ channel: channel2 })
117
+
118
+ expect(channel2.state.addMessageSorted).toHaveBeenCalledOnce()
119
+ })
120
+
121
+ it('handles pending messages with no metadata gracefully', () => {
122
+ const noMetadataMsg = {
123
+ message: { id: 'msg-1', text: 'No metadata' },
124
+ }
125
+ const channel = createChannel({
126
+ pending_messages: [noMetadataMsg],
127
+ })
128
+
129
+ renderHook(() => useRestorePendingMessages(channel))
130
+
131
+ expect(channel.state.addMessageSorted).toHaveBeenCalledOnce()
132
+ expect(channel.state.addMessageSorted).toHaveBeenCalledWith(
133
+ noMetadataMsg.message
134
+ )
135
+ })
136
+ })
@@ -0,0 +1,36 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { Channel } from 'stream-chat'
3
+
4
+ /**
5
+ * Restores pending messages into the channel's visible message list so they
6
+ * appear as if they were already sent.
7
+ *
8
+ * Stream's pending-messages feature removes messages from the channel view
9
+ * once client state is lost (e.g. page refresh). This hook works around that
10
+ * limitation by reading `channel.state.pending_messages` on channel load and
11
+ * re-inserting them via `channel.state.addMessageSorted`.
12
+ *
13
+ * The restoration runs once per channel (tracked by `channel.cid`).
14
+ */
15
+ export function useRestorePendingMessages(channel: Channel) {
16
+ const restoredChannelRef = useRef<string | null>(null)
17
+
18
+ useEffect(() => {
19
+ const cid = channel.cid
20
+ if (!cid || restoredChannelRef.current === cid) return
21
+
22
+ const pendingMessages = channel.state.pending_messages
23
+ if (!pendingMessages?.length) {
24
+ // No pending messages — mark as restored so we don't re-check on
25
+ // re-renders, but still allow a retry if the channel object changes.
26
+ restoredChannelRef.current = cid
27
+ return
28
+ }
29
+
30
+ for (const pending of pendingMessages) {
31
+ channel.state.addMessageSorted(pending.message)
32
+ }
33
+
34
+ restoredChannelRef.current = cid
35
+ }, [channel])
36
+ }