@ethora/chat-component-rn 26.5.17 → 26.5.18
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/lib/commonjs/assets/EmptyChatIllustration.js +106 -0
- package/lib/commonjs/assets/EmptyChatIllustration.js.map +1 -0
- package/lib/commonjs/assets/icons.js +13 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/DropdownMenu/DropdownMenu.js +2 -1
- package/lib/commonjs/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +2 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +5 -2
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChooseChatMessage.js +9 -2
- package/lib/commonjs/components/MainComponents/ChooseChatMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/EditWrapper.js +12 -4
- package/lib/commonjs/components/MainComponents/EditWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageList.js +2 -1
- package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
- package/lib/commonjs/components/MainComponents/NoMessagesPlaceholder.js +11 -3
- package/lib/commonjs/components/MainComponents/NoMessagesPlaceholder.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js +12 -7
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
- package/lib/commonjs/components/MainComponents/RoomList.js +2 -1
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Avatar.js +16 -7
- package/lib/commonjs/components/MessageBubble/Avatar.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +16 -1
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js +9 -0
- package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
- package/lib/commonjs/components/styled/Button.js +4 -2
- package/lib/commonjs/components/styled/Button.js.map +1 -1
- package/lib/commonjs/components/styled/DateLabel.js +12 -4
- package/lib/commonjs/components/styled/DateLabel.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +4 -3
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +8 -1
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/getAvatarColor.js +33 -0
- package/lib/commonjs/helpers/getAvatarColor.js.map +1 -0
- package/lib/commonjs/helpers/getDateLabelColor.js +9 -0
- package/lib/commonjs/helpers/getDateLabelColor.js.map +1 -0
- package/lib/commonjs/helpers/getIconColor.js +9 -0
- package/lib/commonjs/helpers/getIconColor.js.map +1 -0
- package/lib/commonjs/helpers/getSenderNameColor.js +9 -0
- package/lib/commonjs/helpers/getSenderNameColor.js.map +1 -0
- package/lib/commonjs/hooks/useSendMessage.js +72 -22
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/networking/outboundQueue.js +22 -4
- package/lib/commonjs/networking/outboundQueue.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +50 -0
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/Middleware/newMessageMidlleware.js +22 -0
- package/lib/commonjs/roomStore/Middleware/newMessageMidlleware.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +13 -1
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/assets/EmptyChatIllustration.js +99 -0
- package/lib/module/assets/EmptyChatIllustration.js.map +1 -0
- package/lib/module/assets/icons.js +13 -2
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/DropdownMenu/DropdownMenu.js +2 -1
- package/lib/module/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +2 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +5 -2
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ChooseChatMessage.js +9 -2
- package/lib/module/components/MainComponents/ChooseChatMessage.js.map +1 -1
- package/lib/module/components/MainComponents/EditWrapper.js +12 -4
- package/lib/module/components/MainComponents/EditWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MessageList.js +2 -1
- package/lib/module/components/MainComponents/MessageList.js.map +1 -1
- package/lib/module/components/MainComponents/NoMessagesPlaceholder.js +11 -3
- package/lib/module/components/MainComponents/NoMessagesPlaceholder.js.map +1 -1
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.js +12 -7
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
- package/lib/module/components/MainComponents/RoomList.js +2 -1
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/MessageBubble/Avatar.js +16 -7
- package/lib/module/components/MessageBubble/Avatar.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +16 -1
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js +9 -0
- package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
- package/lib/module/components/styled/Button.js +4 -2
- package/lib/module/components/styled/Button.js.map +1 -1
- package/lib/module/components/styled/DateLabel.js +12 -4
- package/lib/module/components/styled/DateLabel.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +4 -3
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/context/xmppProvider.js +8 -1
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/getAvatarColor.js +28 -0
- package/lib/module/helpers/getAvatarColor.js.map +1 -0
- package/lib/module/helpers/getDateLabelColor.js +4 -0
- package/lib/module/helpers/getDateLabelColor.js.map +1 -0
- package/lib/module/helpers/getIconColor.js +4 -0
- package/lib/module/helpers/getIconColor.js.map +1 -0
- package/lib/module/helpers/getSenderNameColor.js +4 -0
- package/lib/module/helpers/getSenderNameColor.js.map +1 -0
- package/lib/module/hooks/useSendMessage.js +72 -22
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/networking/outboundQueue.js +21 -4
- package/lib/module/networking/outboundQueue.js.map +1 -1
- package/lib/module/networking/xmppClient.js +50 -0
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/Middleware/newMessageMidlleware.js +22 -0
- package/lib/module/roomStore/Middleware/newMessageMidlleware.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +13 -1
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/EmptyChatIllustration.d.ts +10 -0
- package/lib/typescript/assets/EmptyChatIllustration.d.ts.map +1 -0
- package/lib/typescript/assets/icons.d.ts +3 -12
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChooseChatMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/EditWrapper.d.ts +5 -1
- package/lib/typescript/components/MainComponents/EditWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/NoMessagesPlaceholder.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ProfileImagePlaceholder.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Avatar.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts.map +1 -1
- package/lib/typescript/components/styled/Button.d.ts +1 -0
- package/lib/typescript/components/styled/Button.d.ts.map +1 -1
- package/lib/typescript/components/styled/DateLabel.d.ts +9 -8
- package/lib/typescript/components/styled/DateLabel.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/getAvatarColor.d.ts +7 -0
- package/lib/typescript/helpers/getAvatarColor.d.ts.map +1 -0
- package/lib/typescript/helpers/getDateLabelColor.d.ts +8 -0
- package/lib/typescript/helpers/getDateLabelColor.d.ts.map +1 -0
- package/lib/typescript/helpers/getIconColor.d.ts +8 -0
- package/lib/typescript/helpers/getIconColor.d.ts.map +1 -0
- package/lib/typescript/helpers/getSenderNameColor.d.ts +8 -0
- package/lib/typescript/helpers/getSenderNameColor.d.ts.map +1 -0
- package/lib/typescript/hooks/useSendMessage.d.ts +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/networking/outboundQueue.d.ts +16 -1
- package/lib/typescript/networking/outboundQueue.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +3 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/Middleware/newMessageMidlleware.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/message.model.d.ts +1 -0
- package/lib/typescript/types/models/message.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +14 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assets/EmptyChatIllustration.tsx +49 -0
- package/src/assets/icons.tsx +64 -81
- package/src/components/DropdownMenu/DropdownMenu.tsx +2 -1
- package/src/components/MainComponents/ChatHeader.tsx +2 -1
- package/src/components/MainComponents/ChatRoom.tsx +5 -2
- package/src/components/MainComponents/ChooseChatMessage.tsx +5 -2
- package/src/components/MainComponents/EditWrapper.tsx +9 -6
- package/src/components/MainComponents/MessageList.tsx +2 -1
- package/src/components/MainComponents/NoMessagesPlaceholder.tsx +4 -1
- package/src/components/MainComponents/ProfileImagePlaceholder.tsx +14 -7
- package/src/components/MainComponents/RoomList.tsx +2 -1
- package/src/components/MessageBubble/Avatar.tsx +18 -9
- package/src/components/MessageBubble/Message.tsx +16 -1
- package/src/components/Modals/ModalWrapper/ModalWrapper.tsx +6 -0
- package/src/components/styled/Button.tsx +5 -1
- package/src/components/styled/DateLabel.tsx +10 -7
- package/src/components/styled/SendInput.tsx +4 -3
- package/src/context/xmppProvider.tsx +6 -1
- package/src/helpers/getAvatarColor.ts +31 -0
- package/src/helpers/getDateLabelColor.ts +3 -0
- package/src/helpers/getIconColor.ts +3 -0
- package/src/helpers/getSenderNameColor.ts +3 -0
- package/src/hooks/useSendMessage.tsx +101 -58
- package/src/networking/outboundQueue.ts +25 -4
- package/src/networking/xmppClient.ts +47 -0
- package/src/roomStore/Middleware/newMessageMidlleware.tsx +25 -0
- package/src/roomStore/roomsSlice.ts +15 -1
- package/src/types/models/message.model.ts +3 -0
- package/src/types/types.ts +18 -1
|
@@ -20,11 +20,25 @@ import type { IConfig, IMessage } from '../types/types';
|
|
|
20
20
|
// long after the optimistic dispatch, mark it failed so the bubble
|
|
21
21
|
// flips out of the "sending..." state. Covers the silent-failure
|
|
22
22
|
// case (e.g. device offline before the message ever made it to the
|
|
23
|
-
// XMPP socket) — bug #18.
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// XMPP socket) — bug #18.
|
|
24
|
+
//
|
|
25
|
+
// 5s window: a brief network blip that reconnects inside this window
|
|
26
|
+
// still resolves on its own — the offline send is buffered in the
|
|
27
|
+
// outbound queue (OUTBOUND_QUEUE_TTL_MS, kept in lockstep) and replayed
|
|
28
|
+
// the moment the stream comes back, so the server echo flips the bubble
|
|
29
|
+
// to delivered. Only a drop that outlasts 5s flips it to "Failed → tap
|
|
30
|
+
// to retry". If a slow send completes just after the window, the
|
|
31
|
+
// delivery clears the failed flag (see newMessageMidlleware) so it
|
|
32
|
+
// doesn't get stuck showing "Failed".
|
|
26
33
|
const PENDING_WATCHDOG_MS = 30_000;
|
|
27
34
|
|
|
35
|
+
// Media gets a longer window than text: the optimistic bubble appears before
|
|
36
|
+
// the file upload even starts, and a large file (up to the 50MB cap) over a
|
|
37
|
+
// slow link can legitimately take far longer than 5s to upload + send. Failing
|
|
38
|
+
// it at 5s mid-upload would flash a wrong "Failed" and let the user kick off a
|
|
39
|
+
// duplicate upload. The text watchdog stays at 5s per product spec.
|
|
40
|
+
const MEDIA_PENDING_WATCHDOG_MS = 30_000;
|
|
41
|
+
|
|
28
42
|
// Monotonic counter for stanza ids. Bumped on each send within a single
|
|
29
43
|
// process. Two rapid sends in the same millisecond used to collide on
|
|
30
44
|
// `send-text-message-${Date.now()}`; that collision made
|
|
@@ -76,7 +90,14 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
76
90
|
activeRoomJID: string,
|
|
77
91
|
isReply?: boolean,
|
|
78
92
|
isChecked?: boolean,
|
|
79
|
-
mainMessage?: string
|
|
93
|
+
mainMessage?: string,
|
|
94
|
+
// Set by retryMessage to the failed message's own id. When present we
|
|
95
|
+
// re-use that id (and its existing optimistic bubble) instead of minting
|
|
96
|
+
// a fresh one — mirroring sendMedia's `existingId`. Without this, a text
|
|
97
|
+
// retry created a second bubble while the original stayed pending
|
|
98
|
+
// forever, so a failed message that the user retried showed as two
|
|
99
|
+
// bubbles, one stuck on "sending...".
|
|
100
|
+
existingId?: string
|
|
80
101
|
) => {
|
|
81
102
|
if (editAction?.isEdit) {
|
|
82
103
|
try {
|
|
@@ -113,7 +134,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
113
134
|
// dedupes by id and flips `pending: false` → DoubleTick renders.
|
|
114
135
|
// Without this, the user taps send and sees nothing for ~200ms
|
|
115
136
|
// until the echo lands, which feels broken.
|
|
116
|
-
const optimisticId = nextStanzaId('send-text-message');
|
|
137
|
+
const optimisticId = existingId || nextStanzaId('send-text-message');
|
|
117
138
|
const optimisticDate = new Date().toISOString();
|
|
118
139
|
const selfId =
|
|
119
140
|
(user as any)?.xmppUsername || (user as any)?.walletAddress || '';
|
|
@@ -135,7 +156,11 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
135
156
|
name: `${user?.firstName || ''} ${user?.lastName || ''}`.trim() || selfId,
|
|
136
157
|
} as any,
|
|
137
158
|
} as IMessage;
|
|
138
|
-
|
|
159
|
+
// On retry (`existingId`) the original optimistic bubble is still in
|
|
160
|
+
// the room (pending:true) — clearMessageFailure just flipped it back out
|
|
161
|
+
// of the "failed" state — so re-adding would duplicate it. Only push a
|
|
162
|
+
// fresh bubble for a brand-new send.
|
|
163
|
+
if (!config?.disableSentLogic && !existingId) {
|
|
139
164
|
dispatch(
|
|
140
165
|
addRoomMessage({ roomJID: activeRoomJID, message: optimisticMessage })
|
|
141
166
|
);
|
|
@@ -177,15 +202,54 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
177
202
|
}, PENDING_WATCHDOG_MS);
|
|
178
203
|
|
|
179
204
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
205
|
+
// The single send closure, used both for the immediate send and for
|
|
206
|
+
// the buffered replay. Passing the optimistic id as the stanza id
|
|
207
|
+
// means the server echoes it back with the same value, so the
|
|
208
|
+
// reducer's dedupe matches and the bubble flips pending → delivered
|
|
209
|
+
// in-place (rather than rendering a second copy).
|
|
210
|
+
const replaySend = (c: any) =>
|
|
211
|
+
c.sendMessage(
|
|
212
|
+
activeRoomJID,
|
|
213
|
+
user.firstName,
|
|
214
|
+
user.lastName,
|
|
215
|
+
'',
|
|
216
|
+
user.walletAddress,
|
|
217
|
+
message,
|
|
218
|
+
'',
|
|
219
|
+
isReply || false,
|
|
220
|
+
isChecked || false,
|
|
221
|
+
mainMessage || '',
|
|
222
|
+
optimisticId
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Always buffer a copy BEFORE attempting the live send. The WebSocket
|
|
226
|
+
// can still read `status: 'online'` for a beat after the network
|
|
227
|
+
// actually drops (the WS error lands a moment later), so a stanza sent
|
|
228
|
+
// "while online" can vanish into a dying socket and never get echoed.
|
|
229
|
+
// Buffering every send means the onOnline reconnect flush replays
|
|
230
|
+
// anything that wasn't confirmed within the window. The server echo
|
|
231
|
+
// removes the entry (see newMessageMidlleware), so a healthy online
|
|
232
|
+
// send never actually replays — only genuinely-lost ones do.
|
|
233
|
+
enqueueOutboundSend({
|
|
234
|
+
optimisticId,
|
|
235
|
+
roomJID: activeRoomJID,
|
|
236
|
+
enqueuedAt: Date.now(),
|
|
237
|
+
send: replaySend,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Only treat a client as usable if its underlying stream is actually
|
|
241
|
+
// `online` — the instance survives a network drop with a stale
|
|
242
|
+
// 'connecting'/'offline' status, and sending through it would just
|
|
243
|
+
// lose the stanza.
|
|
244
|
+
const effectiveClient: any = [
|
|
245
|
+
client,
|
|
246
|
+
getGlobalXmppClient(),
|
|
247
|
+
store.getState()?.chatSettingStore?.client,
|
|
248
|
+
].find((c: any) => c && c.status === 'online');
|
|
187
249
|
|
|
188
250
|
if (!effectiveClient) {
|
|
251
|
+
// No live stream — kick a reconnect; the buffered copy above is
|
|
252
|
+
// replayed by the onOnline flush once the stream is back.
|
|
189
253
|
const anyClient: any =
|
|
190
254
|
client ||
|
|
191
255
|
getGlobalXmppClient() ||
|
|
@@ -193,45 +257,13 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
193
257
|
try {
|
|
194
258
|
anyClient?.forceReconnect?.();
|
|
195
259
|
} catch {}
|
|
196
|
-
enqueueOutboundSend({
|
|
197
|
-
optimisticId,
|
|
198
|
-
roomJID: activeRoomJID,
|
|
199
|
-
enqueuedAt: Date.now(),
|
|
200
|
-
send: (c) =>
|
|
201
|
-
c.sendMessage(
|
|
202
|
-
activeRoomJID,
|
|
203
|
-
user.firstName,
|
|
204
|
-
user.lastName,
|
|
205
|
-
'',
|
|
206
|
-
user.walletAddress,
|
|
207
|
-
message,
|
|
208
|
-
'',
|
|
209
|
-
isReply || false,
|
|
210
|
-
isChecked || false,
|
|
211
|
-
mainMessage || '',
|
|
212
|
-
optimisticId
|
|
213
|
-
),
|
|
214
|
-
});
|
|
215
260
|
return;
|
|
216
261
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
effectiveClient.sendMessage(
|
|
223
|
-
activeRoomJID,
|
|
224
|
-
user.firstName,
|
|
225
|
-
user.lastName,
|
|
226
|
-
'',
|
|
227
|
-
user.walletAddress,
|
|
228
|
-
message,
|
|
229
|
-
'',
|
|
230
|
-
isReply || false,
|
|
231
|
-
isChecked || false,
|
|
232
|
-
mainMessage || '',
|
|
233
|
-
optimisticId
|
|
234
|
-
);
|
|
262
|
+
|
|
263
|
+
replaySend(effectiveClient);
|
|
264
|
+
try {
|
|
265
|
+
effectiveClient.ensureStreamAlive?.();
|
|
266
|
+
} catch {}
|
|
235
267
|
await handleMessageSent({
|
|
236
268
|
message,
|
|
237
269
|
roomJID: activeRoomJID,
|
|
@@ -405,7 +437,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
405
437
|
messageType: 'media',
|
|
406
438
|
});
|
|
407
439
|
}
|
|
408
|
-
},
|
|
440
|
+
}, MEDIA_PENDING_WATCHDOG_MS);
|
|
409
441
|
|
|
410
442
|
// Help RN's FormData polyfill build a proper multipart/file part
|
|
411
443
|
// (instead of serialising the JS object as JSON). On iOS the
|
|
@@ -487,12 +519,14 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
487
519
|
// Stanza id == placeholder id so the MUC echo's outer
|
|
488
520
|
// <message id="..."> matches xmppId on the placeholder and
|
|
489
521
|
// insertMessageWithDelimiter merges in place + flips pending.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
522
|
+
// Same online-only guard as the text path: a surviving-but-offline
|
|
523
|
+
// client must NOT be used (the stanza would vanish into a dead
|
|
524
|
+
// socket) — fall through to enqueue so the reconnect flush replays.
|
|
525
|
+
const effectiveMediaClient: any = [
|
|
526
|
+
client,
|
|
527
|
+
getGlobalXmppClient(),
|
|
528
|
+
store.getState()?.chatSettingStore?.client,
|
|
529
|
+
].find((c: any) => c && c.status === 'online');
|
|
496
530
|
if (effectiveMediaClient) {
|
|
497
531
|
effectiveMediaClient.setActiveRoomJid?.(activeRoomJID);
|
|
498
532
|
await Promise.resolve(
|
|
@@ -503,6 +537,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
503
537
|
messagePayload,
|
|
504
538
|
id
|
|
505
539
|
);
|
|
540
|
+
try {
|
|
541
|
+
effectiveMediaClient.ensureStreamAlive?.();
|
|
542
|
+
} catch {}
|
|
506
543
|
} else {
|
|
507
544
|
const anyMediaClient: any =
|
|
508
545
|
client ||
|
|
@@ -515,6 +552,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
515
552
|
optimisticId: id,
|
|
516
553
|
roomJID: activeRoomJID,
|
|
517
554
|
enqueuedAt: Date.now(),
|
|
555
|
+
// Match the media watchdog so a reconnect within the (longer)
|
|
556
|
+
// media window still auto-replays the buffered stanza.
|
|
557
|
+
ttlMs: MEDIA_PENDING_WATCHDOG_MS,
|
|
518
558
|
send: (c) =>
|
|
519
559
|
c.sendMediaMessageStanza(activeRoomJID, messagePayload, id),
|
|
520
560
|
});
|
|
@@ -605,7 +645,10 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
605
645
|
payload.roomJID,
|
|
606
646
|
payload.isReply,
|
|
607
647
|
payload.isChecked,
|
|
608
|
-
payload.mainMessage
|
|
648
|
+
payload.mainMessage,
|
|
649
|
+
// Re-use the failed message's id so the retry resends in place
|
|
650
|
+
// instead of spawning a duplicate bubble.
|
|
651
|
+
failedId
|
|
609
652
|
);
|
|
610
653
|
} else {
|
|
611
654
|
await sendMedia(
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Each queued item carries a `send(client)` closure that captures the
|
|
20
20
|
* original args (text or media), so flush is type-agnostic.
|
|
21
21
|
*
|
|
22
|
-
* TTL is deliberately aligned with the
|
|
22
|
+
* TTL is deliberately aligned with the pending-send watchdog in
|
|
23
23
|
* useSendMessage (PENDING_WATCHDOG_MS): a queued item older than that has
|
|
24
24
|
* already been — or is about to be — flipped to "Failed → tap to retry" by
|
|
25
25
|
* the watchdog, so replaying it on a late reconnect would duplicate the
|
|
@@ -37,11 +37,20 @@ export interface QueuedSend {
|
|
|
37
37
|
roomJID: string;
|
|
38
38
|
/** Wall-clock ms when first queued; drives the TTL drop on flush. */
|
|
39
39
|
enqueuedAt: number;
|
|
40
|
+
/**
|
|
41
|
+
* Per-item replay window. Kept in lockstep with the matching send
|
|
42
|
+
* watchdog so a reconnect inside the window replays the buffered send,
|
|
43
|
+
* while a longer drop lets the watchdog own the failure (no duplicate).
|
|
44
|
+
* Text uses 5s, media 30s (large uploads need longer). Falls back to
|
|
45
|
+
* OUTBOUND_QUEUE_TTL_MS when omitted.
|
|
46
|
+
*/
|
|
47
|
+
ttlMs?: number;
|
|
40
48
|
/** Replays the original send against a live, online client. */
|
|
41
49
|
send: (client: OutboundQueueClient) => void;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
//
|
|
52
|
+
// Default replay window (text). Mirrors PENDING_WATCHDOG_MS in useSendMessage.
|
|
53
|
+
// Media enqueues a longer per-item ttlMs (see QueuedSend.ttlMs).
|
|
45
54
|
export const OUTBOUND_QUEUE_TTL_MS = 30_000;
|
|
46
55
|
|
|
47
56
|
let queue: QueuedSend[] = [];
|
|
@@ -74,8 +83,8 @@ export function flushOutboundSends(
|
|
|
74
83
|
const pending = queue;
|
|
75
84
|
queue = [];
|
|
76
85
|
for (const item of pending) {
|
|
77
|
-
if (now - item.enqueuedAt > OUTBOUND_QUEUE_TTL_MS) {
|
|
78
|
-
// Stale — the
|
|
86
|
+
if (now - item.enqueuedAt > (item.ttlMs ?? OUTBOUND_QUEUE_TTL_MS)) {
|
|
87
|
+
// Stale — the send watchdog owns the failure; don't duplicate.
|
|
79
88
|
continue;
|
|
80
89
|
}
|
|
81
90
|
try {
|
|
@@ -87,6 +96,18 @@ export function flushOutboundSends(
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Drop a single buffered send by its optimistic/stanza id. Called when a
|
|
101
|
+
* server-confirmed copy of the message arrives (see newMessageMidlleware):
|
|
102
|
+
* the send went through, so the buffered replay is no longer needed and must
|
|
103
|
+
* not fire on a later reconnect. No-op if the id isn't queued.
|
|
104
|
+
*/
|
|
105
|
+
export function removeOutboundSend(optimisticId: string): void {
|
|
106
|
+
if (!optimisticId) {return;}
|
|
107
|
+
const idx = queue.findIndex((q) => q.optimisticId === optimisticId);
|
|
108
|
+
if (idx >= 0) {queue.splice(idx, 1);}
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
/** Drop everything — called on permanent teardown (logout / close). */
|
|
91
112
|
export function clearOutboundSends(): void {
|
|
92
113
|
queue = [];
|
|
@@ -7,6 +7,7 @@ import { getChatsPrivateStoreRequest } from './xmpp/getChatsPrivateStoreRequest.
|
|
|
7
7
|
import { actionSetTimestampToPrivateStore } from './xmpp/actionSetTimestampToPrivateStore.xmpp';
|
|
8
8
|
import { flushLastViewedToPrivateStore } from './xmpp/flushLastViewedToPrivateStore';
|
|
9
9
|
import { sendTypingRequest } from './xmpp/sendTypingRequest.xmpp';
|
|
10
|
+
import { sendPing } from './xmpp/sendPing.xmpp';
|
|
10
11
|
import { getHistory } from './xmpp/getHistory.xmpp';
|
|
11
12
|
import { sendTextMessage } from './xmpp/sendTextMessage.xmpp';
|
|
12
13
|
import { deleteMessage } from './xmpp/deleteMessage.xmpp';
|
|
@@ -614,6 +615,52 @@ export class XmppClient {
|
|
|
614
615
|
this.reconnect();
|
|
615
616
|
}
|
|
616
617
|
|
|
618
|
+
private streamAliveProbe: Promise<boolean> | null = null;
|
|
619
|
+
|
|
620
|
+
verifyStreamAlive(timeoutMs: number = 4000): Promise<boolean> {
|
|
621
|
+
if (this.status !== 'online' || !this.client) {
|
|
622
|
+
return Promise.resolve(false);
|
|
623
|
+
}
|
|
624
|
+
if (this.streamAliveProbe) {return this.streamAliveProbe;}
|
|
625
|
+
const underlying: any = this.client;
|
|
626
|
+
this.streamAliveProbe = new Promise<boolean>((resolve) => {
|
|
627
|
+
let settled = false;
|
|
628
|
+
const done = (alive: boolean) => {
|
|
629
|
+
if (settled) {return;}
|
|
630
|
+
settled = true;
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
try {
|
|
633
|
+
underlying.removeListener?.('stanza', onStanza);
|
|
634
|
+
} catch {}
|
|
635
|
+
resolve(alive);
|
|
636
|
+
};
|
|
637
|
+
const onStanza = () => done(true);
|
|
638
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
639
|
+
try {
|
|
640
|
+
underlying.on('stanza', onStanza);
|
|
641
|
+
sendPing(underlying, this.host, `alive-${Date.now()}`);
|
|
642
|
+
} catch {
|
|
643
|
+
// Couldn't even write the probe — treat as dead.
|
|
644
|
+
done(false);
|
|
645
|
+
}
|
|
646
|
+
}).finally(() => {
|
|
647
|
+
this.streamAliveProbe = null;
|
|
648
|
+
}) as Promise<boolean>;
|
|
649
|
+
return this.streamAliveProbe;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async ensureStreamAlive(timeoutMs: number = 4000): Promise<void> {
|
|
653
|
+
if (this.suppressReconnect || this.status !== 'online') {return;}
|
|
654
|
+
const alive = await this.verifyStreamAlive(timeoutMs);
|
|
655
|
+
// Re-check state: a reconnect may have started meanwhile.
|
|
656
|
+
if (!alive && this.status === 'online' && !this.suppressReconnect) {
|
|
657
|
+
try {
|
|
658
|
+
devPushLog('xmpp', 'stream is zombie (ping unanswered) → forceReconnect');
|
|
659
|
+
} catch {}
|
|
660
|
+
this.forceReconnect();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
617
664
|
async reconnect() {
|
|
618
665
|
if (this.suppressReconnect) {return;}
|
|
619
666
|
// Single-flight: ignore overlapping triggers (AppState + NetInfo +
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Middleware } from '@reduxjs/toolkit';
|
|
2
2
|
import { updateRoom } from '../roomsSlice';
|
|
3
|
+
import { clearMessageFailure } from '../roomHeapSlice';
|
|
4
|
+
import { removeOutboundSend } from '../../networking/outboundQueue';
|
|
3
5
|
import { IRoom } from '../../types/types';
|
|
4
6
|
|
|
5
7
|
export const newMessageMidlleware: Middleware =
|
|
@@ -24,6 +26,29 @@ export const newMessageMidlleware: Middleware =
|
|
|
24
26
|
|
|
25
27
|
const { roomJID, message } = action.payload;
|
|
26
28
|
|
|
29
|
+
// Un-fail on confirmed delivery. A server-confirmed copy (echo or MAM
|
|
30
|
+
// history, never `pending`) arriving for a message we'd flagged failed
|
|
31
|
+
// means the send actually went through — e.g. a slow send that completed
|
|
32
|
+
// just after the 5s watchdog window, or a queued auto-resend landing on
|
|
33
|
+
// reconnect. The failed flag is keyed by our optimistic/stanza id, which
|
|
34
|
+
// the echo carries back as `id`/`xmppId`, so clear it here and let the
|
|
35
|
+
// bubble flip to delivered instead of staying stuck on "Failed".
|
|
36
|
+
if (message && !message.pending) {
|
|
37
|
+
// The confirmed copy carries our optimistic/stanza id back as id/xmppId.
|
|
38
|
+
// Drop any still-buffered replay for it so a later reconnect flush can't
|
|
39
|
+
// re-send an already-delivered message.
|
|
40
|
+
removeOutboundSend(message.id);
|
|
41
|
+
removeOutboundSend(message.xmppId);
|
|
42
|
+
|
|
43
|
+
const failedMap = state.roomHeapSlice?.failedMessages || {};
|
|
44
|
+
const failedKey = [message.id, message.xmppId].find(
|
|
45
|
+
(k: string | undefined) => k && failedMap[k]
|
|
46
|
+
);
|
|
47
|
+
if (failedKey) {
|
|
48
|
+
storeAPI.dispatch(clearMessageFailure(failedKey as string));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
if ((rooms[roomJID]?.lastMessageTimestamp ?? 0) <= Number(message.id)) {
|
|
28
53
|
storeAPI.dispatch(
|
|
29
54
|
updateRoom({
|
|
@@ -76,7 +76,17 @@ function mergeHistoryIntoCache(
|
|
|
76
76
|
// the freshest server state (reactions / edits / deletions).
|
|
77
77
|
const byId = new Map<string, IMessage>();
|
|
78
78
|
for (const m of realExisting) {byId.set(String(m.id), m);}
|
|
79
|
-
for (const m of realFetched) {
|
|
79
|
+
for (const m of realFetched) {
|
|
80
|
+
// Fetched wins on body/reactions/etc., but MAM doesn't carry our
|
|
81
|
+
// client-side "edited" flag (the archived stanza just has the corrected
|
|
82
|
+
// body). Preserve `isEdited` from the cached copy so a message edited
|
|
83
|
+
// before reload keeps its marker instead of losing it on history sync.
|
|
84
|
+
const prev = byId.get(String(m.id));
|
|
85
|
+
byId.set(
|
|
86
|
+
String(m.id),
|
|
87
|
+
prev?.isEdited && !m.isEdited ? { ...m, isEdited: true } : m
|
|
88
|
+
);
|
|
89
|
+
}
|
|
80
90
|
const merged = Array.from(byId.values()).sort(byMs);
|
|
81
91
|
return [...capTail(merged), ...pending];
|
|
82
92
|
}
|
|
@@ -307,6 +317,10 @@ const reducers = {
|
|
|
307
317
|
state.rooms[roomJID].messages.map((message) => {
|
|
308
318
|
if (message.id === messageId) {
|
|
309
319
|
message.body = text;
|
|
320
|
+
// Flag the correction so the bubble can render an "edited" marker.
|
|
321
|
+
// Covers both the author's own edit and edits from other users —
|
|
322
|
+
// every <replace> echo flows through here.
|
|
323
|
+
message.isEdited = true;
|
|
310
324
|
}
|
|
311
325
|
});
|
|
312
326
|
}
|
|
@@ -33,6 +33,9 @@ export interface IMessage {
|
|
|
33
33
|
xmppId?: string;
|
|
34
34
|
xmppFrom?: string;
|
|
35
35
|
deleted?: boolean;
|
|
36
|
+
// Set once a message has been corrected via XEP-0308 (<replace>), so the
|
|
37
|
+
// bubble can show a small "edited" marker (Slack/WhatsApp style).
|
|
38
|
+
isEdited?: boolean;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export interface ReactionMessage {
|
package/src/types/types.ts
CHANGED
|
@@ -270,7 +270,24 @@ export interface IConfig {
|
|
|
270
270
|
projectName?: string;
|
|
271
271
|
|
|
272
272
|
// ----- theming -----
|
|
273
|
-
colors?: {
|
|
273
|
+
colors?: {
|
|
274
|
+
primary: string;
|
|
275
|
+
secondary: string;
|
|
276
|
+
/** Tint for chrome icons (attach, mic/send, burger menu, edit banner,
|
|
277
|
+
* scroll-to-bottom FAB). Falls back to `primary`, then `#0052CD`. */
|
|
278
|
+
icon?: string;
|
|
279
|
+
/** Color of the sender's name above incoming message bubbles.
|
|
280
|
+
* Falls back to `primary`, then `#0052CD`. */
|
|
281
|
+
senderName?: string;
|
|
282
|
+
/** Background of initials avatars (message bubbles, chat header,
|
|
283
|
+
* profile modals). When omitted, each user keeps their per-name
|
|
284
|
+
* pastel color from the hash palette (current default). */
|
|
285
|
+
avatar?: string;
|
|
286
|
+
/** Text color of the day-separator pill ("Today", "June 8") in the
|
|
287
|
+
* message list; the pill background is a light tint of it. Falls
|
|
288
|
+
* back to `primary`, then `#0052CD`. */
|
|
289
|
+
dateLabel?: string;
|
|
290
|
+
};
|
|
274
291
|
/** Configurable font family / weights for the chat UI. See TypographyConfig. */
|
|
275
292
|
typography?: TypographyConfig;
|
|
276
293
|
messageColor?: {
|