@linktr.ee/messaging-react 3.1.0 → 3.1.1

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-Cq-cN9n1.cjs → Card-Bh-xdrvU.cjs} +2 -2
  2. package/dist/{Card-Cq-cN9n1.cjs.map → Card-Bh-xdrvU.cjs.map} +1 -1
  3. package/dist/{Card-BfvsO78k.js → Card-Bm_eCczn.js} +3 -3
  4. package/dist/{Card-BfvsO78k.js.map → Card-Bm_eCczn.js.map} +1 -1
  5. package/dist/{Card-NPXVehHb.cjs → Card-C33YVsqY.cjs} +2 -2
  6. package/dist/{Card-NPXVehHb.cjs.map → Card-C33YVsqY.cjs.map} +1 -1
  7. package/dist/{Card-BhO5jeP9.js → Card-C8Q8MH6Y.js} +2 -2
  8. package/dist/{Card-BhO5jeP9.js.map → Card-C8Q8MH6Y.js.map} +1 -1
  9. package/dist/{Card-BGOWR4lW.js → Card-D8QPP3I9.js} +2 -2
  10. package/dist/{Card-BGOWR4lW.js.map → Card-D8QPP3I9.js.map} +1 -1
  11. package/dist/{Card-CRJ4l5KM.cjs → Card-DO7ipVZF.cjs} +2 -2
  12. package/dist/{Card-CRJ4l5KM.cjs.map → Card-DO7ipVZF.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-Bu9jNPUi.js → LockedThumbnail-6Ykc8JiU.js} +2 -2
  14. package/dist/{LockedThumbnail-Bu9jNPUi.js.map → LockedThumbnail-6Ykc8JiU.js.map} +1 -1
  15. package/dist/{LockedThumbnail-B8MKBVXz.cjs → LockedThumbnail-yEutwXEz.cjs} +2 -2
  16. package/dist/{LockedThumbnail-B8MKBVXz.cjs.map → LockedThumbnail-yEutwXEz.cjs.map} +1 -1
  17. package/dist/{index-CJEl_fID.js → index-Cj6b1oEe.js} +383 -373
  18. package/dist/index-Cj6b1oEe.js.map +1 -0
  19. package/dist/index-DluSX5DB.cjs +2 -0
  20. package/dist/index-DluSX5DB.cjs.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.js +1 -1
  23. package/package.json +1 -1
  24. package/src/components/MessagingShell/MessagingShell.test.tsx +93 -0
  25. package/src/components/MessagingShell/index.tsx +46 -4
  26. package/dist/index-CJEl_fID.js.map +0 -1
  27. package/dist/index-D-5Igybf.cjs +0 -2
  28. package/dist/index-D-5Igybf.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-D-5Igybf.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-DluSX5DB.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-CJEl_fID.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-Cj6b1oEe.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.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -168,4 +168,97 @@ describe('MessagingShell', () => {
168
168
  'Conversation ended'
169
169
  )
170
170
  })
171
+
172
+ it('does not create the channel again when the load effect re-runs for the same pair (e.g. service identity settles during connect)', async () => {
173
+ queryChannelsMock.mockResolvedValue([])
174
+ startChannelWithParticipantMock.mockResolvedValue(makeChannel('created-1'))
175
+
176
+ const { rerender } = renderWithProviders(
177
+ <MessagingShell
178
+ initialParticipantFilter="other-1"
179
+ initialParticipantData={{ id: 'other-1', name: 'Other Person' }}
180
+ />
181
+ )
182
+
183
+ await waitFor(() =>
184
+ expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
185
+ )
186
+
187
+ // A fresh `service` reference (as happens while the connection settles)
188
+ // re-runs the load effect. The once-per-pair guard must prevent a second
189
+ // create for the same viewer/participant pair before the new channel is
190
+ // indexed by queryChannels.
191
+ useMessagingReturn = {
192
+ ...useMessagingReturn,
193
+ service: { startChannelWithParticipant: startChannelWithParticipantMock },
194
+ }
195
+ rerender(
196
+ <MessagingShell
197
+ initialParticipantFilter="other-1"
198
+ initialParticipantData={{ id: 'other-1', name: 'Other Person' }}
199
+ />
200
+ )
201
+
202
+ await new Promise((resolve) => setTimeout(resolve, 50))
203
+ expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
204
+ })
205
+
206
+ it('does not wipe a newer pair load guard when an earlier in-flight load fails', async () => {
207
+ const PAIR_A = 'pair-A'
208
+ const PAIR_B = 'pair-B'
209
+
210
+ // Pair A's query stays pending until we reject it, simulating a load still
211
+ // in flight when the participant changes. Pair B resolves with no channel.
212
+ let rejectA: (reason?: unknown) => void = () => {}
213
+ queryChannelsMock.mockImplementation(
214
+ (query: { members?: { $eq?: string[] } }) => {
215
+ if (query?.members?.$eq?.includes(PAIR_A)) {
216
+ return new Promise((_resolve, reject) => {
217
+ rejectA = reject
218
+ })
219
+ }
220
+ return Promise.resolve([])
221
+ }
222
+ )
223
+ startChannelWithParticipantMock.mockResolvedValue(makeChannel('created-b'))
224
+
225
+ const { rerender } = renderWithProviders(
226
+ <MessagingShell
227
+ initialParticipantFilter={PAIR_A}
228
+ initialParticipantData={{ id: PAIR_A, name: 'A' }}
229
+ />
230
+ )
231
+
232
+ // Switch to pair B while A's load is still pending; B creates its channel.
233
+ rerender(
234
+ <MessagingShell
235
+ initialParticipantFilter={PAIR_B}
236
+ initialParticipantData={{ id: PAIR_B, name: 'B' }}
237
+ />
238
+ )
239
+
240
+ await waitFor(() =>
241
+ expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
242
+ )
243
+
244
+ // A's stale load now fails. Its catch must not clear pair B's guard.
245
+ rejectA(new Error('transient'))
246
+ await new Promise((resolve) => setTimeout(resolve, 0))
247
+
248
+ // Re-run the load effect for pair B (a fresh `service` reference). With B's
249
+ // guard intact this must not create a second channel.
250
+ useMessagingReturn = {
251
+ ...useMessagingReturn,
252
+ service: { startChannelWithParticipant: startChannelWithParticipantMock },
253
+ }
254
+ rerender(
255
+ <MessagingShell
256
+ initialParticipantFilter={PAIR_B}
257
+ initialParticipantData={{ id: PAIR_B, name: 'B' }}
258
+ />
259
+ )
260
+
261
+ await new Promise((resolve) => setTimeout(resolve, 50))
262
+ expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
263
+ })
171
264
  })
@@ -65,13 +65,44 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
65
65
  const onChannelSelectRef = useRef(onChannelSelect)
66
66
  onChannelSelectRef.current = onChannelSelect
67
67
 
68
+ // Track the direct-conversation load to prevent repeated/concurrent loads.
69
+ // Identity-stable deps (above) stop the common re-fire, but legitimate dep
70
+ // changes (e.g. `service`/`client` settling during connect) can still re-run
71
+ // this effect before the just-created channel is indexed, each firing another
72
+ // startChannelWithParticipant call and creating duplicate welcome messages.
73
+ const directConversationLoadRef = useRef<string | null>(null)
74
+
75
+ // Mirror the currently-selected channel into a ref so the async load can read
76
+ // the latest value rather than the (possibly stale) effect-closure value.
77
+ const selectedChannelRef = useRef<Channel | null>(null)
78
+ useEffect(() => {
79
+ selectedChannelRef.current = selectedChannel
80
+ }, [selectedChannel])
81
+
68
82
  useEffect(() => {
69
83
  if (!client || !isConnected) return
70
84
 
71
- const loadInitialChannel = async () => {
72
- const userId = client.userID
73
- if (!userId) return
85
+ const userId = client.userID
86
+ if (!userId) return
87
+
88
+ // Only load once per viewer ↔ participant pair. Set synchronously (before
89
+ // the async work) so re-runs triggered by changing dependency identities
90
+ // bail out instead of issuing another channel-create request.
91
+ const loadKey = `${userId}::${initialParticipantFilter}`
92
+ if (directConversationLoadRef.current === loadKey) return
93
+ directConversationLoadRef.current = loadKey
74
94
 
95
+ // Release the guard only if it still belongs to this load. A newer load for
96
+ // a different pair may have taken ownership while this one was in flight, so
97
+ // clearing unconditionally could wipe the newer guard and let a duplicate
98
+ // load/create slip through.
99
+ const releaseLoadGuard = () => {
100
+ if (directConversationLoadRef.current === loadKey) {
101
+ directConversationLoadRef.current = null
102
+ }
103
+ }
104
+
105
+ const loadInitialChannel = async () => {
75
106
  try {
76
107
  if (debug) {
77
108
  console.log(
@@ -105,6 +136,8 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
105
136
 
106
137
  const participantData = initialParticipantDataRef.current
107
138
  if (!participantData || !service) {
139
+ // Allow a retry once participant data / service become available.
140
+ releaseLoadGuard()
108
141
  setDirectConversationError('No conversation found with this account')
109
142
  if (debug) {
110
143
  console.log(
@@ -136,6 +169,8 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
136
169
  '[MessagingShell] Failed to create conversation:',
137
170
  createErr
138
171
  )
172
+ // Allow a retry for this pair after a transient failure.
173
+ releaseLoadGuard()
139
174
  setDirectConversationError('Failed to create conversation')
140
175
  }
141
176
  } catch (err) {
@@ -143,7 +178,14 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
143
178
  '[MessagingShell] Failed to load initial conversation:',
144
179
  err
145
180
  )
146
- setDirectConversationError('Failed to load conversation')
181
+ // Allow a retry for this pair after a transient failure.
182
+ releaseLoadGuard()
183
+ // Don't replace an already-loaded conversation with an error screen.
184
+ // Read the latest selected channel via ref to avoid acting on a stale
185
+ // closure value when the channel was selected mid-flight.
186
+ if (!selectedChannelRef.current) {
187
+ setDirectConversationError('Failed to load conversation')
188
+ }
147
189
  }
148
190
  }
149
191