@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.
- package/dist/{Card-Cq-cN9n1.cjs → Card-Bh-xdrvU.cjs} +2 -2
- package/dist/{Card-Cq-cN9n1.cjs.map → Card-Bh-xdrvU.cjs.map} +1 -1
- package/dist/{Card-BfvsO78k.js → Card-Bm_eCczn.js} +3 -3
- package/dist/{Card-BfvsO78k.js.map → Card-Bm_eCczn.js.map} +1 -1
- package/dist/{Card-NPXVehHb.cjs → Card-C33YVsqY.cjs} +2 -2
- package/dist/{Card-NPXVehHb.cjs.map → Card-C33YVsqY.cjs.map} +1 -1
- package/dist/{Card-BhO5jeP9.js → Card-C8Q8MH6Y.js} +2 -2
- package/dist/{Card-BhO5jeP9.js.map → Card-C8Q8MH6Y.js.map} +1 -1
- package/dist/{Card-BGOWR4lW.js → Card-D8QPP3I9.js} +2 -2
- package/dist/{Card-BGOWR4lW.js.map → Card-D8QPP3I9.js.map} +1 -1
- package/dist/{Card-CRJ4l5KM.cjs → Card-DO7ipVZF.cjs} +2 -2
- package/dist/{Card-CRJ4l5KM.cjs.map → Card-DO7ipVZF.cjs.map} +1 -1
- package/dist/{LockedThumbnail-Bu9jNPUi.js → LockedThumbnail-6Ykc8JiU.js} +2 -2
- package/dist/{LockedThumbnail-Bu9jNPUi.js.map → LockedThumbnail-6Ykc8JiU.js.map} +1 -1
- package/dist/{LockedThumbnail-B8MKBVXz.cjs → LockedThumbnail-yEutwXEz.cjs} +2 -2
- package/dist/{LockedThumbnail-B8MKBVXz.cjs.map → LockedThumbnail-yEutwXEz.cjs.map} +1 -1
- package/dist/{index-CJEl_fID.js → index-Cj6b1oEe.js} +383 -373
- package/dist/index-Cj6b1oEe.js.map +1 -0
- package/dist/index-DluSX5DB.cjs +2 -0
- package/dist/index-DluSX5DB.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/MessagingShell/MessagingShell.test.tsx +93 -0
- package/src/components/MessagingShell/index.tsx +46 -4
- package/dist/index-CJEl_fID.js.map +0 -1
- package/dist/index-D-5Igybf.cjs +0 -2
- 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-
|
|
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-
|
|
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
|
@@ -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
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|