@linktr.ee/messaging-react 3.3.11-rc-1781859612 → 3.3.11

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.
Files changed (28) hide show
  1. package/dist/{Card-CizcvpTa.cjs → Card-BxHcN_uK.cjs} +2 -2
  2. package/dist/{Card-CizcvpTa.cjs.map → Card-BxHcN_uK.cjs.map} +1 -1
  3. package/dist/{Card-PGPY8ZRL.cjs → Card-Dx9vU_aU.cjs} +2 -2
  4. package/dist/{Card-PGPY8ZRL.cjs.map → Card-Dx9vU_aU.cjs.map} +1 -1
  5. package/dist/{Card-D-sSG334.js → Card-Likc_mEq.js} +3 -3
  6. package/dist/{Card-D-sSG334.js.map → Card-Likc_mEq.js.map} +1 -1
  7. package/dist/{Card-omzCiCc9.cjs → Card-Som2Laov.cjs} +2 -2
  8. package/dist/{Card-omzCiCc9.cjs.map → Card-Som2Laov.cjs.map} +1 -1
  9. package/dist/{Card-BmU_ztmU.js → Card-VgnjOBSF.js} +2 -2
  10. package/dist/{Card-BmU_ztmU.js.map → Card-VgnjOBSF.js.map} +1 -1
  11. package/dist/{Card-CHDQEG9i.js → Card-datOr9-K.js} +2 -2
  12. package/dist/{Card-CHDQEG9i.js.map → Card-datOr9-K.js.map} +1 -1
  13. package/dist/{LockedThumbnail-BynAff_z.js → LockedThumbnail-MW14CvSv.js} +2 -2
  14. package/dist/{LockedThumbnail-BynAff_z.js.map → LockedThumbnail-MW14CvSv.js.map} +1 -1
  15. package/dist/{LockedThumbnail-UEkCam1N.cjs → LockedThumbnail-QWGt7mcF.cjs} +2 -2
  16. package/dist/{LockedThumbnail-UEkCam1N.cjs.map → LockedThumbnail-QWGt7mcF.cjs.map} +1 -1
  17. package/dist/index-CRVJo6HP.cjs +2 -0
  18. package/dist/index-CRVJo6HP.cjs.map +1 -0
  19. package/dist/{index-CAAtAfn1.js → index-cIErdakH.js} +531 -537
  20. package/dist/index-cIErdakH.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.js +1 -1
  23. package/package.json +2 -2
  24. package/src/providers/MessagingProvider.test.tsx +46 -2
  25. package/src/providers/MessagingProvider.tsx +51 -44
  26. package/dist/index-CAAtAfn1.js.map +0 -1
  27. package/dist/index-SMjrfgpc.cjs +0 -2
  28. package/dist/index-SMjrfgpc.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-SMjrfgpc.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-CRVJo6HP.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-CAAtAfn1.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-cIErdakH.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.3.11-rc-1781859612",
3
+ "version": "3.3.11",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@linktr.ee/component-library": "11.8.6",
41
- "@linktr.ee/messaging-core": "2.2.0-rc-1781859612",
41
+ "@linktr.ee/messaging-core": "^2.2.0",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -147,8 +147,8 @@ describe('MessagingProvider', () => {
147
147
  await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(1))
148
148
 
149
149
  // Re-render with a NEW inline serviceConfig object (same apiKey). The
150
- // service is recreated but reuses the same injected client, so the stale
151
- // service must not tear down the live connection.
150
+ // service is created once per client lifetime, so a serviceConfig change
151
+ // neither recreates the service nor tears down the live connection.
152
152
  await act(async () => {
153
153
  rerender(
154
154
  <MessagingProvider
@@ -167,6 +167,50 @@ describe('MessagingProvider', () => {
167
167
  expect(disconnectUser).not.toHaveBeenCalled()
168
168
  })
169
169
 
170
+ it('does not disconnect the reused client when only the debug prop toggles', async () => {
171
+ const { connectUser, disconnectUser } = setupServiceMock()
172
+ const user = {
173
+ type: 'guest' as const,
174
+ id: 'guest-1',
175
+ name: 'Guest',
176
+ } as MessagingUser
177
+ const serviceConfig = {
178
+ fetchToken: async () => 'token',
179
+ createChannel: async () => ({ channelId: 'ch-1' }),
180
+ }
181
+
182
+ const { rerender } = render(
183
+ <MessagingProvider
184
+ apiKey="mock-api-key"
185
+ user={user}
186
+ serviceConfig={serviceConfig}
187
+ debug={false}
188
+ >
189
+ <div />
190
+ </MessagingProvider>
191
+ )
192
+
193
+ await waitFor(() => expect(connectUser).toHaveBeenCalledTimes(1))
194
+
195
+ // Toggling `debug` changes the internal debugLog identity. The client
196
+ // cleanup must stay keyed on the client's lifetime, not on debugLog, so the
197
+ // live connection is not torn down for a logging change.
198
+ await act(async () => {
199
+ rerender(
200
+ <MessagingProvider
201
+ apiKey="mock-api-key"
202
+ user={user}
203
+ serviceConfig={serviceConfig}
204
+ debug={true}
205
+ >
206
+ <div />
207
+ </MessagingProvider>
208
+ )
209
+ })
210
+
211
+ expect(disconnectUser).not.toHaveBeenCalled()
212
+ })
213
+
170
214
  it('disconnects and reconnects when remounted with a new key', async () => {
171
215
  const { connectUser, disconnectUser } = setupServiceMock()
172
216
 
@@ -79,10 +79,18 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
79
79
  // Prevent multiple concurrent connection attempts
80
80
  const connectingRef = useRef(false)
81
81
 
82
- // Latest service instance, for teardown that is keyed on the client's
83
- // lifetime rather than the service's (see the client-cleanup effect below).
82
+ // The single service instance, for teardown in the client-cleanup effect.
84
83
  const serviceRef = useRef<StreamChatService | null>(null)
85
84
 
85
+ // serviceConfig / debug are read at service-creation time via refs so that
86
+ // changing them does not recreate the service (and re-adopt the live client).
87
+ // The service is created once per client lifetime (apiKey); the connection is
88
+ // driven by `user`. Real consumers pass a stable, memoized serviceConfig.
89
+ const serviceConfigRef = useRef(serviceConfig)
90
+ serviceConfigRef.current = serviceConfig
91
+ const debugRef = useRef(debug)
92
+ debugRef.current = debug
93
+
86
94
  // Track renders and prop changes
87
95
  const prevPropsRef = useRef({
88
96
  userId: user?.id,
@@ -118,50 +126,50 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
118
126
  // connectUser resolved (~0.8s later) because the children's parent element
119
127
  // changed — re-running every mount effect (refetches, Stream listeners,
120
128
  // page-view events) and resetting transient state. This client is injected
121
- // into the service below, so connectUser connects this exact instance. One
122
- // client per provider mount (keyed on apiKey) — not the process-wide
123
- // StreamChat.getInstance, which is shared across keys and provider instances.
124
- const chatClient = React.useMemo(
125
- () => (apiKey ? new StreamChat(apiKey) : null),
126
- [apiKey]
127
- )
128
-
129
- // Initialize service when config changes
129
+ // into the service below, so connectUser connects this exact instance.
130
+ //
131
+ // Stored in a ref (not useMemo) so its identity is *guaranteed* stable for the
132
+ // apiKey's lifetime. useMemo is unsafe here: React may discard a memo and
133
+ // recompute it, which would mint a new client, re-run the service-init effect,
134
+ // and tear down + rebuild the live connection — the very remount this fix
135
+ // exists to prevent. Both effects below key on `chatClient`, so it must not
136
+ // depend on memo retention. One client per provider mount (keyed on apiKey) —
137
+ // not the process-wide StreamChat.getInstance, which is shared across keys and
138
+ // provider instances.
139
+ const clientRef = useRef<{ apiKey: string; client: StreamChat } | null>(null)
140
+ if (apiKey && clientRef.current?.apiKey !== apiKey) {
141
+ clientRef.current = { apiKey, client: new StreamChat(apiKey) }
142
+ }
143
+ const chatClient = apiKey ? (clientRef.current?.client ?? null) : null
144
+
145
+ // Create the service once per client lifetime (apiKey). Keyed on `chatClient`
146
+ // — NOT on serviceConfig/debug — so those changing does not spin up a second
147
+ // service that would re-adopt the live client and race the in-flight connect.
148
+ // serviceConfig/debug are read from refs at creation. There is therefore a
149
+ // single service per client, which owns the connection lifecycle and tracks
150
+ // the active user; `user` changes reconnect through it.
130
151
  useEffect(() => {
131
152
  const currentRender = renderCountRef.current
132
- debugLog('🔧 SERVICE INIT EFFECT TRIGGERED', {
133
- renderCount: currentRender,
134
- apiKey: !!apiKey,
135
- serviceConfig: !!serviceConfig,
136
- dependencies: {
137
- apiKey: apiKey?.substring(0, 8) + '...',
138
- serviceConfigRef: serviceConfig,
139
- serviceConfigStable:
140
- prevPropsRef.current.serviceConfig === serviceConfig,
141
- apiKeyStable: prevPropsRef.current.apiKey === apiKey,
142
- },
143
- })
153
+ const currentConfig = serviceConfigRef.current
144
154
 
145
- if (!apiKey || !serviceConfig) {
155
+ if (!apiKey || !chatClient || !currentConfig) {
146
156
  debugLog('⚠️ SERVICE INIT SKIPPED', {
147
157
  renderCount: currentRender,
148
- reason: 'Missing apiKey or serviceConfig',
158
+ reason: 'Missing apiKey, client, or serviceConfig',
149
159
  })
150
160
  return
151
161
  }
152
162
 
153
- debugLog('🚀 CREATING NEW SERVICE', {
163
+ debugLog('🚀 CREATING SERVICE', {
154
164
  renderCount: currentRender,
155
165
  apiKey: apiKey?.substring(0, 8) + '...',
156
- serviceConfigChanged:
157
- prevPropsRef.current.serviceConfig !== serviceConfig,
158
166
  })
159
167
 
160
168
  const newService = new StreamChatService({
161
- ...serviceConfig,
169
+ ...currentConfig,
162
170
  apiKey,
163
- debug,
164
- ...(chatClient ? { client: chatClient } : {}),
171
+ debug: debugRef.current,
172
+ client: chatClient,
165
173
  })
166
174
 
167
175
  serviceRef.current = newService
@@ -171,12 +179,10 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
171
179
  serviceInstance: !!newService,
172
180
  })
173
181
 
174
- // No disconnect on cleanup here. When only serviceConfig/debug change (same
175
- // apiKey), the replacement service reuses the same injected `chatClient`, so
176
- // disconnecting the old service would tear down the live connection the new
177
- // service is about to reuse. Disconnection is owned by the client-cleanup
178
- // effect below, keyed on `chatClient` (apiKey) and unmount.
179
- }, [apiKey, serviceConfig, debug, debugLog, chatClient]) // Use serviceConfig object directly, not individual properties
182
+ // No disconnect on cleanup here disconnection is owned by the
183
+ // client-cleanup effect below, keyed on `chatClient` (apiKey) and unmount.
184
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- serviceConfig/debug/debugLog are read via refs by design (see above); recreating on them would re-adopt the live client
185
+ }, [apiKey, chatClient])
180
186
 
181
187
  // Track if we've already connected this user with this service to prevent duplicate connections
182
188
  const connectedUserRef = useRef<{
@@ -252,22 +258,23 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
252
258
  }
253
259
 
254
260
  connectUser()
255
- }, [service, user, debugLog, isConnected]) // Remove isConnected to prevent circular dependency
261
+ }, [service, user, debugLog, isConnected])
256
262
 
257
263
  // Disconnect tied to the client's lifetime: only when `chatClient` changes
258
264
  // (i.e. `apiKey` changes) or the provider unmounts — never on a mere
259
265
  // serviceConfig/debug change, which reuses the same client. Uses `serviceRef`
260
- // so the disconnect goes through the latest service instance.
266
+ // so the disconnect goes through the latest service instance. Deliberately
267
+ // keyed on `chatClient` only: depending on `debugLog` (which changes identity
268
+ // when the `debug` prop toggles) would tear down the live client just for a
269
+ // logging change.
261
270
  useEffect(() => {
262
- debugLog('🔌 CLIENT CLEANUP EFFECT REGISTERED', {
263
- hasClient: !!chatClient,
264
- })
271
+ const client = chatClient
265
272
  return () => {
266
- debugLog('🧹 CLIENT CLEANUP EFFECT TRIGGERED', 'Disconnecting client')
273
+ if (!client) return
267
274
  connectedUserRef.current = null // Reset connection tracking
268
275
  serviceRef.current?.disconnectUser().catch(console.error)
269
276
  }
270
- }, [chatClient, debugLog])
277
+ }, [chatClient])
271
278
 
272
279
  const refreshConnection = useCallback(async () => {
273
280
  debugLog('🔄 REFRESH CONNECTION CALLED', {