@linktr.ee/messaging-react 1.0.2 → 1.1.0-rc-1760927977
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 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +836 -1079
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/ActionButton/ActionButton.stories.tsx +2 -1
- package/src/components/ActionButton/ActionButton.test.tsx +2 -0
- package/src/components/Avatar/Avatar.stories.tsx +2 -1
- package/src/components/Avatar/index.tsx +2 -1
- package/src/components/ChannelList/ChannelList.stories.tsx +4 -2
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +31 -27
- package/src/components/ChannelList/CustomChannelPreview.tsx +5 -11
- package/src/components/ChannelList/index.tsx +43 -35
- package/src/components/ChannelView.tsx +150 -127
- package/src/components/CloseButton/index.tsx +4 -5
- package/src/components/IconButton/IconButton.stories.tsx +3 -3
- package/src/components/Loading/Loading.stories.tsx +2 -1
- package/src/components/Loading/index.tsx +7 -9
- package/src/components/MessagingShell/EmptyState.stories.tsx +2 -1
- package/src/components/MessagingShell/ErrorState.stories.tsx +2 -1
- package/src/components/MessagingShell/LoadingState.stories.tsx +2 -1
- package/src/components/MessagingShell/LoadingState.tsx +3 -5
- package/src/components/MessagingShell/index.tsx +159 -135
- package/src/components/ParticipantPicker/ParticipantItem.stories.tsx +4 -2
- package/src/components/ParticipantPicker/ParticipantItem.tsx +25 -21
- package/src/components/ParticipantPicker/ParticipantPicker.stories.tsx +4 -2
- package/src/components/ParticipantPicker/ParticipantPicker.tsx +104 -76
- package/src/components/ParticipantPicker/index.tsx +93 -72
- package/src/components/SearchInput/SearchInput.stories.tsx +2 -1
- package/src/components/SearchInput/SearchInput.test.tsx +4 -2
- package/src/components/SearchInput/index.tsx +14 -15
- package/src/hooks/useParticipants.ts +1 -0
- package/src/index.ts +3 -0
- package/src/providers/MessagingProvider.tsx +213 -135
- package/src/stories/mocks.tsx +18 -19
- package/src/styles.css +75 -0
- package/src/test/setup.ts +11 -12
- package/src/test/utils.tsx +6 -7
- package/src/types.ts +1 -1
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { StreamChatService } from '@linktr.ee/messaging-core'
|
|
2
|
+
import React, {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState,
|
|
7
|
+
useRef,
|
|
8
|
+
useCallback,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import type { StreamChat } from 'stream-chat'
|
|
11
|
+
import { Chat } from 'stream-chat-react'
|
|
5
12
|
|
|
6
|
-
import type {
|
|
7
|
-
MessagingProviderProps,
|
|
13
|
+
import type {
|
|
14
|
+
MessagingProviderProps,
|
|
8
15
|
MessagingCapabilities,
|
|
9
|
-
MessagingCustomization
|
|
10
|
-
} from '../types'
|
|
16
|
+
MessagingCustomization,
|
|
17
|
+
} from '../types'
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Context value for messaging state and service
|
|
14
21
|
*/
|
|
15
22
|
export interface MessagingContextValue {
|
|
16
|
-
service: StreamChatService | null
|
|
17
|
-
client:
|
|
18
|
-
isConnected: boolean
|
|
19
|
-
isLoading: boolean
|
|
20
|
-
error: string | null
|
|
21
|
-
capabilities: MessagingCapabilities
|
|
22
|
-
customization: MessagingCustomization
|
|
23
|
-
refreshConnection: () => Promise<void
|
|
24
|
-
debug: boolean
|
|
23
|
+
service: StreamChatService | null
|
|
24
|
+
client: StreamChat | null // Stream Chat client
|
|
25
|
+
isConnected: boolean
|
|
26
|
+
isLoading: boolean
|
|
27
|
+
error: string | null
|
|
28
|
+
capabilities: MessagingCapabilities
|
|
29
|
+
customization: MessagingCustomization
|
|
30
|
+
refreshConnection: () => Promise<void>
|
|
31
|
+
debug: boolean
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
const MessagingContext = createContext<MessagingContextValue>({
|
|
@@ -34,12 +41,12 @@ const MessagingContext = createContext<MessagingContextValue>({
|
|
|
34
41
|
customization: {},
|
|
35
42
|
refreshConnection: async () => {},
|
|
36
43
|
debug: false,
|
|
37
|
-
})
|
|
44
|
+
})
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* Hook to access messaging context
|
|
41
48
|
*/
|
|
42
|
-
export const useMessagingContext = () => useContext(MessagingContext)
|
|
49
|
+
export const useMessagingContext = () => useContext(MessagingContext)
|
|
43
50
|
|
|
44
51
|
/**
|
|
45
52
|
* Provider component that wraps messaging-core with React state management
|
|
@@ -54,91 +61,122 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
54
61
|
debug = false,
|
|
55
62
|
}) => {
|
|
56
63
|
// Create debug logger that respects the debug prop
|
|
57
|
-
const debugLog = (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
const debugLog = useCallback(
|
|
65
|
+
(message: string, ...args: unknown[]) => {
|
|
66
|
+
if (debug) {
|
|
67
|
+
console.log(`🔥 [MessagingProvider] ${message}`, ...args)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[debug]
|
|
71
|
+
)
|
|
62
72
|
|
|
63
73
|
debugLog('🔄 RENDER START', {
|
|
64
74
|
userId: user?.id,
|
|
65
75
|
apiKey: apiKey?.substring(0, 8) + '...',
|
|
66
76
|
serviceConfig: !!serviceConfig,
|
|
67
77
|
capabilities: Object.keys(capabilities),
|
|
68
|
-
customization: Object.keys(customization)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
const [service, setService] = useState<StreamChatService | null>(null)
|
|
72
|
-
const [client, setClient] = useState<
|
|
73
|
-
const [isConnected, setIsConnected] = useState(false)
|
|
74
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
75
|
-
const [error, setError] = useState<string | null>(null)
|
|
76
|
-
|
|
78
|
+
customization: Object.keys(customization),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const [service, setService] = useState<StreamChatService | null>(null)
|
|
82
|
+
const [client, setClient] = useState<StreamChat | null>(null)
|
|
83
|
+
const [isConnected, setIsConnected] = useState(false)
|
|
84
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
85
|
+
const [error, setError] = useState<string | null>(null)
|
|
86
|
+
|
|
77
87
|
// Prevent multiple concurrent connection attempts
|
|
78
|
-
const connectingRef = useRef(false)
|
|
88
|
+
const connectingRef = useRef(false)
|
|
79
89
|
|
|
80
90
|
// Track renders and prop changes
|
|
81
|
-
const prevPropsRef = useRef({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
const prevPropsRef = useRef({
|
|
92
|
+
userId: user?.id,
|
|
93
|
+
apiKey,
|
|
94
|
+
serviceConfig,
|
|
95
|
+
capabilities,
|
|
96
|
+
customization,
|
|
97
|
+
})
|
|
98
|
+
const renderCountRef = useRef(0)
|
|
99
|
+
renderCountRef.current++
|
|
100
|
+
|
|
85
101
|
debugLog('📊 RENDER INFO', {
|
|
86
102
|
renderCount: renderCountRef.current,
|
|
87
103
|
currentProps: { userId: user?.id, apiKey: apiKey?.substring(0, 8) + '...' },
|
|
88
104
|
propChanges: {
|
|
89
105
|
userChanged: prevPropsRef.current.userId !== user?.id,
|
|
90
106
|
apiKeyChanged: prevPropsRef.current.apiKey !== apiKey,
|
|
91
|
-
serviceConfigChanged:
|
|
107
|
+
serviceConfigChanged:
|
|
108
|
+
prevPropsRef.current.serviceConfig !== serviceConfig,
|
|
92
109
|
capabilitiesChanged: prevPropsRef.current.capabilities !== capabilities,
|
|
93
|
-
customizationChanged:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
customizationChanged:
|
|
111
|
+
prevPropsRef.current.customization !== customization,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
prevPropsRef.current = {
|
|
116
|
+
userId: user?.id,
|
|
117
|
+
apiKey,
|
|
118
|
+
serviceConfig,
|
|
119
|
+
capabilities,
|
|
120
|
+
customization,
|
|
121
|
+
}
|
|
98
122
|
|
|
99
123
|
// Initialize service when config changes
|
|
100
124
|
useEffect(() => {
|
|
101
|
-
const currentRender = renderCountRef.current
|
|
125
|
+
const currentRender = renderCountRef.current
|
|
102
126
|
debugLog('🔧 SERVICE INIT EFFECT TRIGGERED', {
|
|
103
127
|
renderCount: currentRender,
|
|
104
128
|
apiKey: !!apiKey,
|
|
105
129
|
serviceConfig: !!serviceConfig,
|
|
106
|
-
dependencies: {
|
|
107
|
-
apiKey: apiKey?.substring(0, 8) + '...',
|
|
130
|
+
dependencies: {
|
|
131
|
+
apiKey: apiKey?.substring(0, 8) + '...',
|
|
108
132
|
serviceConfigRef: serviceConfig,
|
|
109
|
-
serviceConfigStable:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
serviceConfigStable:
|
|
134
|
+
prevPropsRef.current.serviceConfig === serviceConfig,
|
|
135
|
+
apiKeyStable: prevPropsRef.current.apiKey === apiKey,
|
|
136
|
+
},
|
|
137
|
+
})
|
|
113
138
|
|
|
114
139
|
if (!apiKey || !serviceConfig) {
|
|
115
|
-
debugLog('⚠️ SERVICE INIT SKIPPED', {
|
|
116
|
-
|
|
140
|
+
debugLog('⚠️ SERVICE INIT SKIPPED', {
|
|
141
|
+
renderCount: currentRender,
|
|
142
|
+
reason: 'Missing apiKey or serviceConfig',
|
|
143
|
+
})
|
|
144
|
+
return
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
debugLog('🚀 CREATING NEW SERVICE', {
|
|
147
|
+
debugLog('🚀 CREATING NEW SERVICE', {
|
|
120
148
|
renderCount: currentRender,
|
|
121
149
|
apiKey: apiKey?.substring(0, 8) + '...',
|
|
122
|
-
serviceConfigChanged:
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
serviceConfigChanged:
|
|
151
|
+
prevPropsRef.current.serviceConfig !== serviceConfig,
|
|
152
|
+
})
|
|
153
|
+
|
|
125
154
|
const newService = new StreamChatService({
|
|
126
155
|
...serviceConfig,
|
|
127
156
|
apiKey,
|
|
128
157
|
debug,
|
|
129
|
-
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
setService(newService)
|
|
161
|
+
debugLog('✅ SERVICE SET', {
|
|
162
|
+
renderCount: currentRender,
|
|
163
|
+
serviceInstance: !!newService,
|
|
164
|
+
})
|
|
130
165
|
|
|
131
|
-
setService(newService);
|
|
132
|
-
debugLog('✅ SERVICE SET', { renderCount: currentRender, serviceInstance: !!newService });
|
|
133
|
-
|
|
134
166
|
return () => {
|
|
135
|
-
debugLog('🧹 SERVICE CLEANUP', {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
167
|
+
debugLog('🧹 SERVICE CLEANUP', {
|
|
168
|
+
renderCount: currentRender,
|
|
169
|
+
reason: 'Effect cleanup',
|
|
170
|
+
})
|
|
171
|
+
newService.disconnectUser().catch(console.error)
|
|
172
|
+
}
|
|
173
|
+
}, [apiKey, serviceConfig, debug, debugLog]) // Use serviceConfig object directly, not individual properties
|
|
139
174
|
|
|
140
175
|
// Track if we've already connected this user with this service to prevent duplicate connections
|
|
141
|
-
const connectedUserRef = useRef<{
|
|
176
|
+
const connectedUserRef = useRef<{
|
|
177
|
+
serviceId: StreamChatService
|
|
178
|
+
userId: string
|
|
179
|
+
} | null>(null)
|
|
142
180
|
|
|
143
181
|
// Connect user when service and user are available
|
|
144
182
|
useEffect(() => {
|
|
@@ -148,94 +186,125 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
148
186
|
userId: user?.id,
|
|
149
187
|
isConnecting: connectingRef.current,
|
|
150
188
|
isConnected: isConnected,
|
|
151
|
-
dependencies: { service: !!service, userId: user?.id }
|
|
152
|
-
})
|
|
189
|
+
dependencies: { service: !!service, userId: user?.id },
|
|
190
|
+
})
|
|
153
191
|
|
|
154
192
|
if (!service || !user) {
|
|
155
|
-
debugLog('⚠️ USER CONNECTION SKIPPED', 'Missing service or user')
|
|
156
|
-
return
|
|
193
|
+
debugLog('⚠️ USER CONNECTION SKIPPED', 'Missing service or user')
|
|
194
|
+
return
|
|
157
195
|
}
|
|
158
|
-
|
|
196
|
+
|
|
159
197
|
if (connectingRef.current) {
|
|
160
|
-
debugLog('⚠️ USER CONNECTION SKIPPED', 'Already connecting')
|
|
161
|
-
return
|
|
198
|
+
debugLog('⚠️ USER CONNECTION SKIPPED', 'Already connecting')
|
|
199
|
+
return
|
|
162
200
|
}
|
|
163
|
-
|
|
201
|
+
|
|
164
202
|
// Check if we've already connected this exact user with this exact service instance
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
203
|
+
if (
|
|
204
|
+
connectedUserRef.current?.serviceId === service &&
|
|
205
|
+
connectedUserRef.current?.userId === user.id
|
|
206
|
+
) {
|
|
207
|
+
debugLog(
|
|
208
|
+
'⚠️ USER CONNECTION SKIPPED',
|
|
209
|
+
'Already connected this user with this service'
|
|
210
|
+
)
|
|
211
|
+
return
|
|
168
212
|
}
|
|
169
213
|
|
|
170
214
|
const connectUser = async () => {
|
|
171
|
-
debugLog('🚀 STARTING USER CONNECTION', { userId: user.id })
|
|
172
|
-
connectingRef.current = true
|
|
173
|
-
setIsLoading(true)
|
|
174
|
-
setError(null)
|
|
215
|
+
debugLog('🚀 STARTING USER CONNECTION', { userId: user.id })
|
|
216
|
+
connectingRef.current = true
|
|
217
|
+
setIsLoading(true)
|
|
218
|
+
setError(null)
|
|
175
219
|
|
|
176
220
|
try {
|
|
177
|
-
debugLog('📞 CALLING SERVICE.CONNECTUSER', { userId: user.id })
|
|
178
|
-
const streamClient = await service.connectUser(user)
|
|
179
|
-
setClient(streamClient)
|
|
180
|
-
setIsConnected(true)
|
|
181
|
-
connectedUserRef.current = { serviceId: service, userId: user.id }
|
|
182
|
-
debugLog('✅ USER CONNECTION SUCCESS', {
|
|
221
|
+
debugLog('📞 CALLING SERVICE.CONNECTUSER', { userId: user.id })
|
|
222
|
+
const streamClient = await service.connectUser(user)
|
|
223
|
+
setClient(streamClient)
|
|
224
|
+
setIsConnected(true)
|
|
225
|
+
connectedUserRef.current = { serviceId: service, userId: user.id } // Mark as connected
|
|
226
|
+
debugLog('✅ USER CONNECTION SUCCESS', {
|
|
227
|
+
userId: user.id,
|
|
228
|
+
clientId: streamClient.userID,
|
|
229
|
+
})
|
|
183
230
|
} catch (err) {
|
|
184
|
-
const errorMessage =
|
|
185
|
-
|
|
186
|
-
|
|
231
|
+
const errorMessage =
|
|
232
|
+
err instanceof Error ? err.message : 'Connection failed'
|
|
233
|
+
setError(errorMessage)
|
|
234
|
+
debugLog('❌ USER CONNECTION ERROR', {
|
|
235
|
+
userId: user.id,
|
|
236
|
+
error: errorMessage,
|
|
237
|
+
})
|
|
187
238
|
} finally {
|
|
188
|
-
setIsLoading(false)
|
|
189
|
-
connectingRef.current = false
|
|
190
|
-
debugLog('🔄 USER CONNECTION FINISHED', {
|
|
239
|
+
setIsLoading(false)
|
|
240
|
+
connectingRef.current = false
|
|
241
|
+
debugLog('🔄 USER CONNECTION FINISHED', {
|
|
242
|
+
userId: user.id,
|
|
243
|
+
isConnected,
|
|
244
|
+
})
|
|
191
245
|
}
|
|
192
|
-
}
|
|
246
|
+
}
|
|
193
247
|
|
|
194
|
-
connectUser()
|
|
195
|
-
}, [service, user])
|
|
248
|
+
connectUser()
|
|
249
|
+
}, [service, user, debugLog, isConnected]) // Remove isConnected to prevent circular dependency
|
|
196
250
|
|
|
197
251
|
// Disconnect when user is removed (cleanup effect)
|
|
198
252
|
useEffect(() => {
|
|
199
|
-
debugLog('🔌 CLEANUP EFFECT REGISTERED', {
|
|
253
|
+
debugLog('🔌 CLEANUP EFFECT REGISTERED', {
|
|
254
|
+
hasService: !!service,
|
|
255
|
+
isConnected,
|
|
256
|
+
})
|
|
200
257
|
return () => {
|
|
201
258
|
if (service && isConnected) {
|
|
202
|
-
debugLog(
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
debugLog(
|
|
260
|
+
'🧹 CLEANUP EFFECT TRIGGERED',
|
|
261
|
+
'Cleaning up connection on unmount'
|
|
262
|
+
)
|
|
263
|
+
connectedUserRef.current = null // Reset connection tracking
|
|
264
|
+
service.disconnectUser().catch(console.error)
|
|
205
265
|
} else {
|
|
206
|
-
debugLog('🔇 CLEANUP EFFECT SKIPPED', {
|
|
266
|
+
debugLog('🔇 CLEANUP EFFECT SKIPPED', {
|
|
267
|
+
hasService: !!service,
|
|
268
|
+
isConnected,
|
|
269
|
+
})
|
|
207
270
|
}
|
|
208
|
-
}
|
|
209
|
-
}, [service, isConnected])
|
|
271
|
+
}
|
|
272
|
+
}, [service, isConnected, debugLog])
|
|
210
273
|
|
|
211
274
|
const refreshConnection = useCallback(async () => {
|
|
212
|
-
debugLog('🔄 REFRESH CONNECTION CALLED', {
|
|
213
|
-
|
|
275
|
+
debugLog('🔄 REFRESH CONNECTION CALLED', {
|
|
276
|
+
hasService: !!service,
|
|
277
|
+
hasUser: !!user,
|
|
278
|
+
})
|
|
279
|
+
|
|
214
280
|
if (!service || !user) {
|
|
215
|
-
debugLog('⚠️ REFRESH CONNECTION SKIPPED', 'Missing service or user')
|
|
216
|
-
return
|
|
281
|
+
debugLog('⚠️ REFRESH CONNECTION SKIPPED', 'Missing service or user')
|
|
282
|
+
return
|
|
217
283
|
}
|
|
218
284
|
|
|
219
|
-
debugLog('🚀 STARTING CONNECTION REFRESH', { userId: user.id })
|
|
220
|
-
setIsLoading(true)
|
|
285
|
+
debugLog('🚀 STARTING CONNECTION REFRESH', { userId: user.id })
|
|
286
|
+
setIsLoading(true)
|
|
221
287
|
try {
|
|
222
|
-
debugLog('🔌 DISCONNECTING FOR REFRESH')
|
|
223
|
-
await service.disconnectUser()
|
|
224
|
-
debugLog('📞 RECONNECTING FOR REFRESH')
|
|
225
|
-
const streamClient = await service.connectUser(user)
|
|
226
|
-
setClient(streamClient)
|
|
227
|
-
setIsConnected(true)
|
|
228
|
-
setError(null)
|
|
229
|
-
debugLog('✅ CONNECTION REFRESH SUCCESS', { userId: user.id })
|
|
288
|
+
debugLog('🔌 DISCONNECTING FOR REFRESH')
|
|
289
|
+
await service.disconnectUser()
|
|
290
|
+
debugLog('📞 RECONNECTING FOR REFRESH')
|
|
291
|
+
const streamClient = await service.connectUser(user)
|
|
292
|
+
setClient(streamClient)
|
|
293
|
+
setIsConnected(true)
|
|
294
|
+
setError(null)
|
|
295
|
+
debugLog('✅ CONNECTION REFRESH SUCCESS', { userId: user.id })
|
|
230
296
|
} catch (err) {
|
|
231
|
-
const errorMessage = err instanceof Error ? err.message : 'Refresh failed'
|
|
232
|
-
setError(errorMessage)
|
|
233
|
-
debugLog('❌ CONNECTION REFRESH ERROR', {
|
|
297
|
+
const errorMessage = err instanceof Error ? err.message : 'Refresh failed'
|
|
298
|
+
setError(errorMessage)
|
|
299
|
+
debugLog('❌ CONNECTION REFRESH ERROR', {
|
|
300
|
+
userId: user.id,
|
|
301
|
+
error: errorMessage,
|
|
302
|
+
})
|
|
234
303
|
} finally {
|
|
235
|
-
setIsLoading(false)
|
|
236
|
-
debugLog('🔄 CONNECTION REFRESH FINISHED', { userId: user.id })
|
|
304
|
+
setIsLoading(false)
|
|
305
|
+
debugLog('🔄 CONNECTION REFRESH FINISHED', { userId: user.id })
|
|
237
306
|
}
|
|
238
|
-
}, [service, user])
|
|
307
|
+
}, [service, user, debugLog])
|
|
239
308
|
|
|
240
309
|
// Memoize context value to prevent unnecessary re-renders
|
|
241
310
|
const contextValue: MessagingContextValue = React.useMemo(() => {
|
|
@@ -246,9 +315,9 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
246
315
|
isLoading,
|
|
247
316
|
hasError: !!error,
|
|
248
317
|
capabilitiesKeys: Object.keys(capabilities),
|
|
249
|
-
customizationKeys: Object.keys(customization)
|
|
250
|
-
})
|
|
251
|
-
|
|
318
|
+
customizationKeys: Object.keys(customization),
|
|
319
|
+
})
|
|
320
|
+
|
|
252
321
|
return {
|
|
253
322
|
service,
|
|
254
323
|
client,
|
|
@@ -259,24 +328,33 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
259
328
|
customization,
|
|
260
329
|
refreshConnection,
|
|
261
330
|
debug,
|
|
262
|
-
}
|
|
263
|
-
}, [
|
|
331
|
+
}
|
|
332
|
+
}, [
|
|
333
|
+
service,
|
|
334
|
+
client,
|
|
335
|
+
isConnected,
|
|
336
|
+
isLoading,
|
|
337
|
+
error,
|
|
338
|
+
capabilities,
|
|
339
|
+
customization,
|
|
340
|
+
refreshConnection,
|
|
341
|
+
debug,
|
|
342
|
+
debugLog,
|
|
343
|
+
])
|
|
264
344
|
|
|
265
345
|
debugLog('🔄 RENDER END', {
|
|
266
346
|
renderCount: renderCountRef.current,
|
|
267
347
|
willRenderChat: !!(client && isConnected),
|
|
268
|
-
contextValueReady: !!contextValue
|
|
269
|
-
})
|
|
348
|
+
contextValueReady: !!contextValue,
|
|
349
|
+
})
|
|
270
350
|
|
|
271
351
|
return (
|
|
272
352
|
<MessagingContext.Provider value={contextValue}>
|
|
273
353
|
{client && isConnected ? (
|
|
274
|
-
<Chat client={client}>
|
|
275
|
-
{children}
|
|
276
|
-
</Chat>
|
|
354
|
+
<Chat client={client}>{children}</Chat>
|
|
277
355
|
) : (
|
|
278
356
|
children
|
|
279
357
|
)}
|
|
280
358
|
</MessagingContext.Provider>
|
|
281
|
-
)
|
|
282
|
-
}
|
|
359
|
+
)
|
|
360
|
+
}
|
package/src/stories/mocks.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { StreamChat } from 'stream-chat'
|
|
2
|
+
import { ConnectionOpen, StreamChat } from 'stream-chat'
|
|
3
3
|
import { Chat } from 'stream-chat-react'
|
|
4
4
|
|
|
5
5
|
// Mock Stream Chat client for Storybook
|
|
@@ -13,25 +13,25 @@ const createMockClient = () => {
|
|
|
13
13
|
const client = new StreamChat('mock-api-key', {
|
|
14
14
|
allowServerSideConnect: true,
|
|
15
15
|
})
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
// Mock the client methods
|
|
18
18
|
client.connectUser = async () => {
|
|
19
|
-
return { me: mockUser } as
|
|
19
|
+
return { me: mockUser } as unknown as ConnectionOpen
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
client.disconnectUser = async () => {
|
|
23
23
|
return Promise.resolve()
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// @ts-ignore
|
|
27
26
|
client.userID = mockUser.id
|
|
28
|
-
// @ts-ignore
|
|
29
27
|
client.user = mockUser
|
|
30
|
-
|
|
28
|
+
|
|
31
29
|
return client
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
export const MockChatProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
32
|
+
export const MockChatProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
33
|
+
children,
|
|
34
|
+
}) => {
|
|
35
35
|
const [client] = React.useState(() => createMockClient())
|
|
36
36
|
|
|
37
37
|
React.useEffect(() => {
|
|
@@ -44,17 +44,17 @@ export const MockChatProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Create a mock chat provider with pre-populated channels
|
|
47
|
-
export const MockChatProviderWithChannels: React.FC<{
|
|
48
|
-
children: React.ReactNode
|
|
49
|
-
channelCount?: number
|
|
47
|
+
export const MockChatProviderWithChannels: React.FC<{
|
|
48
|
+
children: React.ReactNode
|
|
49
|
+
channelCount?: number
|
|
50
50
|
}> = ({ children, channelCount = 3 }) => {
|
|
51
51
|
const [client] = React.useState(() => {
|
|
52
52
|
const mockClient = createMockClient()
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
// Create mock channels
|
|
55
55
|
const mockChannels = Array.from({ length: channelCount }, (_, i) => {
|
|
56
56
|
const participant = mockParticipants[i % mockParticipants.length]
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
const mockChannel = {
|
|
59
59
|
id: `channel-${i + 1}`,
|
|
60
60
|
cid: `messaging:channel-${i + 1}`,
|
|
@@ -62,11 +62,11 @@ export const MockChatProviderWithChannels: React.FC<{
|
|
|
62
62
|
_client: mockClient,
|
|
63
63
|
state: {
|
|
64
64
|
members: {
|
|
65
|
-
[mockUser.id]: {
|
|
65
|
+
[mockUser.id]: {
|
|
66
66
|
user: mockUser,
|
|
67
67
|
user_id: mockUser.id,
|
|
68
68
|
},
|
|
69
|
-
[participant.id]: {
|
|
69
|
+
[participant.id]: {
|
|
70
70
|
user: participant,
|
|
71
71
|
user_id: participant.id,
|
|
72
72
|
},
|
|
@@ -86,13 +86,13 @@ export const MockChatProviderWithChannels: React.FC<{
|
|
|
86
86
|
query: async () => ({ messages: [] }),
|
|
87
87
|
watch: async () => ({ messages: [] }),
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
return mockChannel
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
// @ts-
|
|
93
|
+
// @ts-expect-error - Mock queryChannels method
|
|
94
94
|
mockClient.queryChannels = async () => mockChannels
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
return mockClient
|
|
97
97
|
})
|
|
98
98
|
|
|
@@ -154,4 +154,3 @@ export const mockParticipantSource = {
|
|
|
154
154
|
}
|
|
155
155
|
},
|
|
156
156
|
}
|
|
157
|
-
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* Stream Chat base styles */
|
|
2
|
+
@import 'stream-chat-react/dist/css/v2/index.css';
|
|
3
|
+
|
|
4
|
+
/* Dialog component styles - used by messaging components */
|
|
5
|
+
/* Note: Dialogs get moved to the top layer when opened with .showModal() */
|
|
6
|
+
.mes-dialog {
|
|
7
|
+
--transition-duration: 0.2s;
|
|
8
|
+
|
|
9
|
+
border: none;
|
|
10
|
+
padding: 0;
|
|
11
|
+
margin: 0;
|
|
12
|
+
background: transparent;
|
|
13
|
+
max-height: 100vh;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 100vh;
|
|
16
|
+
position: fixed;
|
|
17
|
+
top: 0;
|
|
18
|
+
right: 0;
|
|
19
|
+
bottom: 0;
|
|
20
|
+
left: auto;
|
|
21
|
+
|
|
22
|
+
/* Mobile: full width */
|
|
23
|
+
max-width: 100%;
|
|
24
|
+
|
|
25
|
+
/* Desktop (md and up): constrained width respecting sidebar */
|
|
26
|
+
@media (min-width: 767px) {
|
|
27
|
+
max-width: min(500px, calc(100% - var(--sidebar-width, 0px)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Transitions for slide-in effect */
|
|
31
|
+
transition:
|
|
32
|
+
translate var(--transition-duration) cubic-bezier(0.4, 0, 0.2, 1),
|
|
33
|
+
overlay var(--transition-duration) allow-discrete,
|
|
34
|
+
display var(--transition-duration) allow-discrete;
|
|
35
|
+
|
|
36
|
+
/* Start off-screen to the right */
|
|
37
|
+
translate: 100% 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.mes-dialog[open] {
|
|
41
|
+
translate: 0 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.mes-dialog::backdrop {
|
|
45
|
+
transition:
|
|
46
|
+
display var(--transition-duration) allow-discrete,
|
|
47
|
+
overlay var(--transition-duration) allow-discrete,
|
|
48
|
+
background-color var(--transition-duration) linear;
|
|
49
|
+
background-color: rgba(0, 0, 0, 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.mes-dialog[open]::backdrop {
|
|
53
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Starting style for smooth entry animation */
|
|
57
|
+
@starting-style {
|
|
58
|
+
.mes-dialog[open] {
|
|
59
|
+
translate: 100% 0;
|
|
60
|
+
|
|
61
|
+
&::backdrop {
|
|
62
|
+
background-color: rgba(0, 0, 0, 0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Textarea composer focus styles */
|
|
68
|
+
.mes-textarea-composer-container:has(.mes-textarea-composer:focus) {
|
|
69
|
+
outline: 2px solid black;
|
|
70
|
+
|
|
71
|
+
.mes-textarea-composer {
|
|
72
|
+
outline: none;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
package/src/test/setup.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import '@testing-library/jest-dom'
|
|
2
|
-
import { cleanup } from '@testing-library/react'
|
|
3
|
-
import { afterEach } from 'vitest'
|
|
1
|
+
import '@testing-library/jest-dom/vitest'
|
|
2
|
+
import { cleanup } from '@testing-library/react'
|
|
3
|
+
import { afterEach } from 'vitest'
|
|
4
4
|
|
|
5
5
|
// Cleanup after each test
|
|
6
6
|
afterEach(() => {
|
|
7
|
-
cleanup()
|
|
8
|
-
})
|
|
7
|
+
cleanup()
|
|
8
|
+
})
|
|
9
9
|
|
|
10
10
|
// Mock Stream Chat if needed
|
|
11
11
|
global.ResizeObserver = class ResizeObserver {
|
|
12
12
|
observe() {}
|
|
13
13
|
unobserve() {}
|
|
14
14
|
disconnect() {}
|
|
15
|
-
}
|
|
15
|
+
}
|
|
16
16
|
|
|
17
17
|
// Mock IntersectionObserver for virtualization
|
|
18
18
|
global.IntersectionObserver = class IntersectionObserver {
|
|
@@ -21,10 +21,9 @@ global.IntersectionObserver = class IntersectionObserver {
|
|
|
21
21
|
unobserve() {}
|
|
22
22
|
disconnect() {}
|
|
23
23
|
takeRecords() {
|
|
24
|
-
return []
|
|
24
|
+
return []
|
|
25
25
|
}
|
|
26
|
-
root = null
|
|
27
|
-
rootMargin = ''
|
|
28
|
-
thresholds = []
|
|
29
|
-
}
|
|
30
|
-
|
|
26
|
+
root = null
|
|
27
|
+
rootMargin = ''
|
|
28
|
+
thresholds = []
|
|
29
|
+
}
|