@ethora/chat-component-rn 26.7.7 → 26.7.9
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/components/MainComponents/ChatRoom.js +17 -8
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +8 -5
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageContainer.js +5 -1
- package/lib/commonjs/components/MainComponents/MessageContainer.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/Modals/FilePreviewModal/FilePreviewModal.js +51 -12
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +12 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +13 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/commonjs/components/styled/MessageImage.js +3 -1
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/UnsupportedType.js +14 -4
- package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +3 -1
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +46 -31
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/createMessageFromXml.js +19 -1
- package/lib/commonjs/helpers/createMessageFromXml.js.map +1 -1
- package/lib/commonjs/helpers/getDisplayFileName.js +79 -0
- package/lib/commonjs/helpers/getDisplayFileName.js.map +1 -0
- package/lib/commonjs/helpers/getServerReadTimestamp.js +125 -0
- package/lib/commonjs/helpers/getServerReadTimestamp.js.map +1 -0
- package/lib/commonjs/helpers/initXmppRooms.js +9 -7
- package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
- package/lib/commonjs/helpers/isOwnMessage.js +75 -0
- package/lib/commonjs/helpers/isOwnMessage.js.map +1 -0
- package/lib/commonjs/helpers/mimeToExtension.js +6 -14
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/hooks/useChatRoomFocus.js +17 -8
- package/lib/commonjs/hooks/useChatRoomFocus.js.map +1 -1
- package/lib/commonjs/hooks/useChatWrapperInit.js +12 -7
- package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/commonjs/networking/stanzaHandlers.js +20 -2
- package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
- package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
- package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +36 -2
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/index.js +35 -2
- package/lib/commonjs/roomStore/index.js.map +1 -1
- package/lib/commonjs/roomStore/persistence.js +200 -11
- package/lib/commonjs/roomStore/persistence.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +83 -6
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +17 -8
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +9 -6
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/MessageContainer.js +5 -1
- package/lib/module/components/MainComponents/MessageContainer.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/Modals/FilePreviewModal/FilePreviewModal.js +52 -13
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js +12 -1
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +13 -2
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/module/components/styled/MessageImage.js +3 -1
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/UnsupportedType.js +14 -4
- package/lib/module/components/styled/UnsupportedType.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +3 -1
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +47 -32
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/createMessageFromXml.js +19 -1
- package/lib/module/helpers/createMessageFromXml.js.map +1 -1
- package/lib/module/helpers/getDisplayFileName.js +73 -0
- package/lib/module/helpers/getDisplayFileName.js.map +1 -0
- package/lib/module/helpers/getServerReadTimestamp.js +120 -0
- package/lib/module/helpers/getServerReadTimestamp.js.map +1 -0
- package/lib/module/helpers/initXmppRooms.js +9 -7
- package/lib/module/helpers/initXmppRooms.js.map +1 -1
- package/lib/module/helpers/isOwnMessage.js +70 -0
- package/lib/module/helpers/isOwnMessage.js.map +1 -0
- package/lib/module/helpers/mimeToExtension.js +6 -13
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/hooks/useChatRoomFocus.js +17 -8
- package/lib/module/hooks/useChatRoomFocus.js.map +1 -1
- package/lib/module/hooks/useChatWrapperInit.js +12 -7
- package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/module/networking/stanzaHandlers.js +20 -2
- package/lib/module/networking/stanzaHandlers.js.map +1 -1
- package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
- package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
- package/lib/module/networking/xmppClient.js +36 -2
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/index.js +37 -4
- package/lib/module/roomStore/index.js.map +1 -1
- package/lib/module/roomStore/persistence.js +197 -11
- package/lib/module/roomStore/persistence.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +83 -6
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageContainer.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
- package/lib/typescript/components/styled/MessageImage.d.ts +6 -0
- package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
- package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts +6 -0
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/createMessageFromXml.d.ts.map +1 -1
- package/lib/typescript/helpers/getDisplayFileName.d.ts +49 -0
- package/lib/typescript/helpers/getDisplayFileName.d.ts.map +1 -0
- package/lib/typescript/helpers/getServerReadTimestamp.d.ts +69 -0
- package/lib/typescript/helpers/getServerReadTimestamp.d.ts.map +1 -0
- package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
- package/lib/typescript/helpers/isOwnMessage.d.ts +36 -0
- package/lib/typescript/helpers/isOwnMessage.d.ts.map +1 -0
- package/lib/typescript/helpers/mimeToExtension.d.ts +0 -15
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/hooks/useChatRoomFocus.d.ts +4 -1
- package/lib/typescript/hooks/useChatRoomFocus.d.ts.map +1 -1
- package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
- package/lib/typescript/networking/stanzaHandlers.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts +9 -2
- package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/index.d.ts.map +1 -1
- package/lib/typescript/roomStore/persistence.d.ts +18 -1
- package/lib/typescript/roomStore/persistence.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +13 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/MainComponents/ChatRoom.tsx +21 -10
- package/src/components/MainComponents/MediaMessage.tsx +8 -5
- package/src/components/MainComponents/MessageContainer.tsx +5 -1
- package/src/components/MainComponents/MessageList.tsx +2 -1
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +63 -26
- package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
- package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +14 -2
- package/src/components/RoomComponents/styled/LastMessageVideo.tsx +14 -2
- package/src/components/styled/MessageImage.tsx +8 -1
- package/src/components/styled/UnsupportedType.tsx +14 -4
- package/src/components/styled/VideoMessage.tsx +8 -1
- package/src/context/xmppProvider.tsx +47 -24
- package/src/helpers/createMessageFromXml.ts +20 -1
- package/src/helpers/getDisplayFileName.ts +87 -0
- package/src/helpers/getServerReadTimestamp.ts +120 -0
- package/src/helpers/initXmppRooms.tsx +9 -9
- package/src/helpers/isOwnMessage.ts +90 -0
- package/src/helpers/mimeToExtension.ts +6 -22
- package/src/hooks/useChatRoomFocus.ts +14 -5
- package/src/hooks/useChatWrapperInit.ts +12 -7
- package/src/networking/stanzaHandlers.ts +20 -6
- package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +65 -18
- package/src/networking/xmppClient.ts +41 -2
- package/src/roomStore/index.ts +40 -4
- package/src/roomStore/persistence.ts +197 -7
- package/src/roomStore/roomsSlice.ts +84 -6
|
@@ -2,6 +2,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
2
2
|
import { Middleware } from '@reduxjs/toolkit';
|
|
3
3
|
import { IMessage, IRoom, User } from '../types/types';
|
|
4
4
|
import { encryptForPersist, decryptFromPersist } from '../helpers/persistCrypto';
|
|
5
|
+
import type { FailedMessagePayload } from './roomHeapSlice';
|
|
5
6
|
|
|
6
7
|
// -------------------------------------------------------------------
|
|
7
8
|
// Lightweight RN persistence layer. Mirrors what redux-persist gives
|
|
@@ -21,12 +22,24 @@ import { encryptForPersist, decryptFromPersist } from '../helpers/persistCrypto'
|
|
|
21
22
|
|
|
22
23
|
const KEY_CHAT = '@ethora/persist:chatSettingStore';
|
|
23
24
|
const KEY_ROOMS = '@ethora/persist:rooms';
|
|
25
|
+
// bug #39: a failed send needs to survive a relaunch as "failed / tap
|
|
26
|
+
// to retry", not silently render as delivered. `roomHeapStore.failedMessages`
|
|
27
|
+
// is the map the bubble reads (`isFailed = failedMessages[id]`, see
|
|
28
|
+
// Message.tsx / useMessageHeapState): it lives in a separate, previously
|
|
29
|
+
// non-persisted slice from `rooms`, so it gets its own key here.
|
|
30
|
+
const KEY_HEAP = '@ethora/persist:roomHeap';
|
|
24
31
|
|
|
25
32
|
// Per-room message cap on disk. Mirror this value in `roomsSlice`'s
|
|
26
33
|
// in-memory cap (see `enforceMessageCap`) so the runtime and persisted
|
|
27
34
|
// shapes stay aligned.
|
|
28
35
|
export const MESSAGE_LIMIT = 100;
|
|
29
36
|
|
|
37
|
+
// Defensive cap on the number of failed-message payloads written to
|
|
38
|
+
// disk. In practice this map only grows while the user is actively
|
|
39
|
+
// sending offline, but a runaway sender (or a bug that never clears an
|
|
40
|
+
// entry) must not turn this key into an unbounded write.
|
|
41
|
+
export const MAX_PERSISTED_FAILED_MESSAGES = 200;
|
|
42
|
+
|
|
30
43
|
interface PersistedChatState {
|
|
31
44
|
user: User;
|
|
32
45
|
}
|
|
@@ -36,6 +49,14 @@ interface PersistedRoomsState {
|
|
|
36
49
|
// intentionally NOT persisted: activeRoomJID, editAction, isLoading
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
interface PersistedHeapState {
|
|
53
|
+
failedMessages: Record<string, FailedMessagePayload>;
|
|
54
|
+
// intentionally NOT persisted: messageHeap, a process-lifetime
|
|
55
|
+
// "in flight" buffer (see roomHeapSlice.ts); anything still in it when
|
|
56
|
+
// the app dies is handled by the boot-time pending->failed scan below,
|
|
57
|
+
// not by replaying the heap itself.
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
const sanitizeUser = (user?: User): User | null => {
|
|
40
61
|
if (!user) {return null;}
|
|
41
62
|
// Drop secrets we don't want at rest — destructure to omit, then
|
|
@@ -81,6 +102,13 @@ const PERSISTED_MESSAGE_FIELDS: (keyof IMessage)[] = [
|
|
|
81
102
|
'size',
|
|
82
103
|
'langSource',
|
|
83
104
|
'callLog',
|
|
105
|
+
// bug #39: without this, a message that was still in flight (no
|
|
106
|
+
// server echo yet) at kill time comes back from disk indistinguishable
|
|
107
|
+
// from a normal delivered message, so the bubble renders it as sent.
|
|
108
|
+
// Persisting `pending` lets the boot-time scan (see
|
|
109
|
+
// computeBootTimeFailures below) find exactly the messages that never
|
|
110
|
+
// got confirmed and flip them to failed instead.
|
|
111
|
+
'pending',
|
|
84
112
|
];
|
|
85
113
|
|
|
86
114
|
// The sender identity that rides along on every message over the wire
|
|
@@ -180,6 +208,154 @@ const sanitizeRooms = (
|
|
|
180
208
|
return out;
|
|
181
209
|
};
|
|
182
210
|
|
|
211
|
+
// Only the fields retryMessage actually needs to replay the send (see
|
|
212
|
+
// FailedMessagePayload / useSendMessage.retryMessage). In particular, the
|
|
213
|
+
// media `data` blob is trimmed to `uri/type/name/size` (the picker never
|
|
214
|
+
// hands us auth material, but this stays defensive against a future
|
|
215
|
+
// picker change smuggling extra fields, or a huge inline payload, into
|
|
216
|
+
// `data`).
|
|
217
|
+
const sanitizeMediaData = (
|
|
218
|
+
data: any
|
|
219
|
+
): { uri?: string; type?: string; name?: string; size?: number } => {
|
|
220
|
+
if (!data || typeof data !== 'object') {return {};}
|
|
221
|
+
const out: { uri?: string; type?: string; name?: string; size?: number } = {};
|
|
222
|
+
if (typeof data.uri === 'string') {out.uri = data.uri;}
|
|
223
|
+
if (typeof data.type === 'string') {out.type = data.type;}
|
|
224
|
+
if (typeof data.name === 'string') {out.name = data.name;}
|
|
225
|
+
if (typeof data.size === 'number') {out.size = data.size;}
|
|
226
|
+
return out;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const sanitizeFailedMessagePayload = (
|
|
230
|
+
payload: FailedMessagePayload
|
|
231
|
+
): FailedMessagePayload | null => {
|
|
232
|
+
if (!payload || typeof payload !== 'object') {return null;}
|
|
233
|
+
if (!payload.id || typeof payload.id !== 'string') {return null;}
|
|
234
|
+
if (!payload.roomJID || typeof payload.roomJID !== 'string') {return null;}
|
|
235
|
+
if (payload.kind === 'media') {
|
|
236
|
+
return {
|
|
237
|
+
kind: 'media',
|
|
238
|
+
id: payload.id,
|
|
239
|
+
roomJID: payload.roomJID,
|
|
240
|
+
data: sanitizeMediaData(payload.data),
|
|
241
|
+
type: typeof payload.type === 'string' ? payload.type : '',
|
|
242
|
+
isReply: payload.isReply,
|
|
243
|
+
isChecked: payload.isChecked,
|
|
244
|
+
mainMessage: payload.mainMessage,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (payload.kind === 'text') {
|
|
248
|
+
return {
|
|
249
|
+
kind: 'text',
|
|
250
|
+
id: payload.id,
|
|
251
|
+
roomJID: payload.roomJID,
|
|
252
|
+
body: typeof payload.body === 'string' ? payload.body : '',
|
|
253
|
+
isReply: payload.isReply,
|
|
254
|
+
isChecked: payload.isChecked,
|
|
255
|
+
mainMessage: payload.mainMessage,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const sanitizeFailedMessages = (
|
|
262
|
+
failedMessages: Record<string, FailedMessagePayload> | undefined
|
|
263
|
+
): Record<string, FailedMessagePayload> => {
|
|
264
|
+
if (!failedMessages || typeof failedMessages !== 'object') {return {};}
|
|
265
|
+
const out: Record<string, FailedMessagePayload> = {};
|
|
266
|
+
let count = 0;
|
|
267
|
+
for (const [id, payload] of Object.entries(failedMessages)) {
|
|
268
|
+
if (count >= MAX_PERSISTED_FAILED_MESSAGES) {break;}
|
|
269
|
+
const clean = sanitizeFailedMessagePayload(payload);
|
|
270
|
+
if (clean) {
|
|
271
|
+
out[id] = clean;
|
|
272
|
+
count++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// -------------------------------------------------------------------
|
|
279
|
+
// Boot-time pending -> failed conversion (bug #39).
|
|
280
|
+
//
|
|
281
|
+
// `messageHeap` (the "still sending" buffer) is intentionally NOT
|
|
282
|
+
// persisted: it's a process-lifetime thing. That means on a cold start
|
|
283
|
+
// there is no such thing as "still sending": a persisted message that
|
|
284
|
+
// carries `pending: true` never got a server echo NOR got the chance to
|
|
285
|
+
// trip the in-session watchdog (see PENDING_WATCHDOG_MS in
|
|
286
|
+
// useSendMessage.tsx) before the process died. It must render as failed
|
|
287
|
+
// with a working retry, not as delivered and not as stuck "sending...".
|
|
288
|
+
//
|
|
289
|
+
// Reconstructs the same FailedMessagePayload shape the live send path
|
|
290
|
+
// builds in useSendMessage (markMessageFailed calls), from the fields we
|
|
291
|
+
// persist per message.
|
|
292
|
+
// -------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
export function reconstructFailedMessagePayload(
|
|
295
|
+
message: IMessage,
|
|
296
|
+
roomJID: string
|
|
297
|
+
): FailedMessagePayload | null {
|
|
298
|
+
if (!message?.id || !roomJID) {return null;}
|
|
299
|
+
const isReply = !!message.isReply;
|
|
300
|
+
const isChecked = message.showInChannel === 'true';
|
|
301
|
+
const mainMessage = message.mainMessage;
|
|
302
|
+
if (message.isMediafile === 'true') {
|
|
303
|
+
return {
|
|
304
|
+
kind: 'media',
|
|
305
|
+
id: message.id,
|
|
306
|
+
roomJID,
|
|
307
|
+
data: sanitizeMediaData({
|
|
308
|
+
uri: message.location,
|
|
309
|
+
name: message.fileName,
|
|
310
|
+
type: message.mimetype,
|
|
311
|
+
size:
|
|
312
|
+
message.size !== undefined && message.size !== null
|
|
313
|
+
? Number(message.size)
|
|
314
|
+
: undefined,
|
|
315
|
+
}),
|
|
316
|
+
type: message.mimetype || '',
|
|
317
|
+
isReply,
|
|
318
|
+
isChecked,
|
|
319
|
+
mainMessage,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
kind: 'text',
|
|
324
|
+
id: message.id,
|
|
325
|
+
roomJID,
|
|
326
|
+
body: message.body || '',
|
|
327
|
+
isReply,
|
|
328
|
+
isChecked,
|
|
329
|
+
mainMessage,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Scan every persisted room for messages still `pending: true` that
|
|
335
|
+
* DON'T already have a restored failure entry (those are handled by the
|
|
336
|
+
* caller separately, from the persisted `failedMessages` map), and
|
|
337
|
+
* synthesize a retry-able FailedMessagePayload for each. Pure function:
|
|
338
|
+
* the caller (roomStore/index.ts) is responsible for dispatching
|
|
339
|
+
* `markMessageFailed` for the results.
|
|
340
|
+
*/
|
|
341
|
+
export function computeBootTimeFailures(
|
|
342
|
+
rooms: Record<string, IRoom> | null | undefined,
|
|
343
|
+
alreadyFailed: Record<string, FailedMessagePayload> | null | undefined
|
|
344
|
+
): FailedMessagePayload[] {
|
|
345
|
+
if (!rooms || typeof rooms !== 'object') {return [];}
|
|
346
|
+
const out: FailedMessagePayload[] = [];
|
|
347
|
+
for (const [jid, room] of Object.entries(rooms)) {
|
|
348
|
+
const messages = Array.isArray(room?.messages) ? room.messages : [];
|
|
349
|
+
for (const message of messages) {
|
|
350
|
+
if (!message || !message.pending) {continue;}
|
|
351
|
+
if (alreadyFailed && alreadyFailed[message.id]) {continue;}
|
|
352
|
+
const payload = reconstructFailedMessagePayload(message, jid);
|
|
353
|
+
if (payload) {out.push(payload);}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
183
359
|
let writeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
184
360
|
|
|
185
361
|
/**
|
|
@@ -194,6 +370,7 @@ export const persistenceMiddleware: Middleware = (storeAPI) => (next) => (
|
|
|
194
370
|
const type: string = action?.type || '';
|
|
195
371
|
if (
|
|
196
372
|
!type.startsWith('roomMessages/') &&
|
|
373
|
+
!type.startsWith('roomHeapStore/') &&
|
|
197
374
|
!type.startsWith('chat/setUser') &&
|
|
198
375
|
!type.startsWith('chat/updateUser') &&
|
|
199
376
|
!type.startsWith('chat/refreshTokens') &&
|
|
@@ -213,13 +390,20 @@ export const persistenceMiddleware: Middleware = (storeAPI) => (next) => (
|
|
|
213
390
|
const roomsPayload: PersistedRoomsState = {
|
|
214
391
|
rooms: sanitizeRooms(state.rooms?.rooms || {}),
|
|
215
392
|
};
|
|
216
|
-
const
|
|
393
|
+
const heapPayload: PersistedHeapState = {
|
|
394
|
+
failedMessages: sanitizeFailedMessages(
|
|
395
|
+
state.roomHeapSlice?.failedMessages
|
|
396
|
+
),
|
|
397
|
+
};
|
|
398
|
+
const [chatCipher, roomsCipher, heapCipher] = await Promise.all([
|
|
217
399
|
encryptForPersist(JSON.stringify(chatPayload)),
|
|
218
400
|
encryptForPersist(JSON.stringify(roomsPayload)),
|
|
401
|
+
encryptForPersist(JSON.stringify(heapPayload)),
|
|
219
402
|
]);
|
|
220
403
|
await AsyncStorage.multiSet([
|
|
221
404
|
[KEY_CHAT, chatCipher],
|
|
222
405
|
[KEY_ROOMS, roomsCipher],
|
|
406
|
+
[KEY_HEAP, heapCipher],
|
|
223
407
|
]);
|
|
224
408
|
} catch (e) {
|
|
225
409
|
console.warn('persist write failed', e);
|
|
@@ -238,15 +422,18 @@ export const persistenceMiddleware: Middleware = (storeAPI) => (next) => (
|
|
|
238
422
|
export async function readPersistedState(): Promise<{
|
|
239
423
|
chat: PersistedChatState | null;
|
|
240
424
|
rooms: PersistedRoomsState | null;
|
|
425
|
+
heap: PersistedHeapState | null;
|
|
241
426
|
}> {
|
|
242
427
|
try {
|
|
243
|
-
const [chatRaw, roomsRaw] = await AsyncStorage.multiGet([
|
|
428
|
+
const [chatRaw, roomsRaw, heapRaw] = await AsyncStorage.multiGet([
|
|
244
429
|
KEY_CHAT,
|
|
245
430
|
KEY_ROOMS,
|
|
431
|
+
KEY_HEAP,
|
|
246
432
|
]);
|
|
247
|
-
const [chatPlain, roomsPlain] = await Promise.all([
|
|
433
|
+
const [chatPlain, roomsPlain, heapPlain] = await Promise.all([
|
|
248
434
|
chatRaw[1] ? decryptFromPersist(chatRaw[1]) : Promise.resolve(null),
|
|
249
435
|
roomsRaw[1] ? decryptFromPersist(roomsRaw[1]) : Promise.resolve(null),
|
|
436
|
+
heapRaw[1] ? decryptFromPersist(heapRaw[1]) : Promise.resolve(null),
|
|
250
437
|
]);
|
|
251
438
|
// `decryptFromPersist` returns null both for "nothing stored" and
|
|
252
439
|
// for "stored value isn't a valid envelope for the current key" —
|
|
@@ -257,19 +444,22 @@ export async function readPersistedState(): Promise<{
|
|
|
257
444
|
const rooms = roomsPlain
|
|
258
445
|
? (JSON.parse(roomsPlain) as PersistedRoomsState)
|
|
259
446
|
: null;
|
|
260
|
-
|
|
447
|
+
const heap = heapPlain
|
|
448
|
+
? (JSON.parse(heapPlain) as PersistedHeapState)
|
|
449
|
+
: null;
|
|
450
|
+
return { chat, rooms, heap };
|
|
261
451
|
} catch (e) {
|
|
262
452
|
console.warn('persist read failed', e);
|
|
263
|
-
return { chat: null, rooms: null };
|
|
453
|
+
return { chat: null, rooms: null, heap: null };
|
|
264
454
|
}
|
|
265
455
|
}
|
|
266
456
|
|
|
267
457
|
export async function clearPersistedState(): Promise<void> {
|
|
268
458
|
try {
|
|
269
|
-
await AsyncStorage.multiRemove([KEY_CHAT, KEY_ROOMS]);
|
|
459
|
+
await AsyncStorage.multiRemove([KEY_CHAT, KEY_ROOMS, KEY_HEAP]);
|
|
270
460
|
} catch (e) {
|
|
271
461
|
console.warn('persist clear failed', e);
|
|
272
462
|
}
|
|
273
463
|
}
|
|
274
464
|
|
|
275
|
-
export const PERSIST_KEYS = { KEY_CHAT, KEY_ROOMS };
|
|
465
|
+
export const PERSIST_KEYS = { KEY_CHAT, KEY_ROOMS, KEY_HEAP };
|
|
@@ -142,7 +142,6 @@ function mergeHistoryIntoCache(
|
|
|
142
142
|
const ex = stripCallSignals(existing);
|
|
143
143
|
const fe = stripCallSignals(fetched);
|
|
144
144
|
|
|
145
|
-
const pending = ex.filter((m) => m?.pending);
|
|
146
145
|
const realExisting = ex.filter(
|
|
147
146
|
(m) => m && !m.pending && m.id !== 'delimiter-new'
|
|
148
147
|
);
|
|
@@ -151,6 +150,29 @@ function mergeHistoryIntoCache(
|
|
|
151
150
|
// Nothing usable came back → keep the cache exactly as it was.
|
|
152
151
|
if (realFetched.length === 0) {return ex;}
|
|
153
152
|
|
|
153
|
+
// Optimistic sends are preserved across a history merge - EXCEPT the
|
|
154
|
+
// ones the fetched page proves the server already has. That happens
|
|
155
|
+
// whenever a send reached the server but its echo did not reach us
|
|
156
|
+
// before the process died: the optimistic bubble is restored from disk
|
|
157
|
+
// still `pending: true` (and, since bug #39, flagged failed at boot),
|
|
158
|
+
// while MAM hands back the very same message under its ARCHIVE id.
|
|
159
|
+
// Keying only on `id` never matched those two, so the room ended up
|
|
160
|
+
// showing the message twice - once for real and once as a bubble stuck
|
|
161
|
+
// on "sending…"/"Failed" that nothing could ever clear. The archived
|
|
162
|
+
// copy carries our original stanza id as `xmppId` (getDataFromXml), so
|
|
163
|
+
// match on that too and drop the local ghost.
|
|
164
|
+
const fetchedKeys = new Set<string>();
|
|
165
|
+
for (const m of realFetched) {
|
|
166
|
+
if (m?.id != null) {fetchedKeys.add(String(m.id));}
|
|
167
|
+
if ((m as any)?.xmppId) {fetchedKeys.add(String((m as any).xmppId));}
|
|
168
|
+
}
|
|
169
|
+
const pending = ex.filter(
|
|
170
|
+
(m) =>
|
|
171
|
+
m?.pending &&
|
|
172
|
+
!fetchedKeys.has(String(m.id)) &&
|
|
173
|
+
!(m.xmppId && fetchedKeys.has(String(m.xmppId)))
|
|
174
|
+
);
|
|
175
|
+
|
|
154
176
|
const byMs = (a: IMessage, b: IMessage) =>
|
|
155
177
|
msgSortableMs(a) - msgSortableMs(b);
|
|
156
178
|
const capTail = (arr: IMessage[]) =>
|
|
@@ -580,7 +602,19 @@ const reducers = {
|
|
|
580
602
|
*
|
|
581
603
|
* Monotonic: a marker only ever moves a baseline FORWARD, so a stale
|
|
582
604
|
* server value can't resurrect already-read messages and a more-recent
|
|
583
|
-
* local read (tab blur stamping
|
|
605
|
+
* local read (tab blur stamping the server read timestamp) always wins.
|
|
606
|
+
*
|
|
607
|
+
* Bug #38 self-heal: forward-only is correct for a SANE existing
|
|
608
|
+
* value, but a device whose clock was ahead may have already stamped
|
|
609
|
+
* a marker in the future (either persisted locally from a previous
|
|
610
|
+
* session, or fetched from the server before this fix existed). To
|
|
611
|
+
* "only ever move forward" from a corrupt future value is to never
|
|
612
|
+
* move at all - the room would stay stuck at `unreadMessages: 0`
|
|
613
|
+
* forever. When the room is loaded, detect a marker that sits well
|
|
614
|
+
* past the newest message we actually know it has and waive the
|
|
615
|
+
* forward-only check for that one comparison, so the incoming
|
|
616
|
+
* (already-clamped, see xmppClient.getChatsPrivateStoreRequestStanza)
|
|
617
|
+
* value can correct it even though it's numerically smaller.
|
|
584
618
|
*/
|
|
585
619
|
applyPrivateStoreMarkers: (
|
|
586
620
|
state: WritableDraft<RoomMessagesState>,
|
|
@@ -592,15 +626,34 @@ const reducers = {
|
|
|
592
626
|
for (const jid of Object.keys(markers)) {
|
|
593
627
|
const ts = Number(markers[jid]);
|
|
594
628
|
if (!jid || !Number.isFinite(ts) || ts <= 0) {continue;}
|
|
629
|
+
|
|
630
|
+
const room = state.rooms[jid];
|
|
631
|
+
const newestKnownMs = room ? newestAckedMessageMs(room.messages) : 0;
|
|
632
|
+
// Mirrors isCorruptFutureReadMarker in helpers/getServerReadTimestamp.ts
|
|
633
|
+
// (duplicated to avoid a roomStore <-> helpers import cycle):
|
|
634
|
+
// BOTH signals are required. "Further ahead than the newest
|
|
635
|
+
// message this room has locally" happens all the time on a room
|
|
636
|
+
// whose history hasn't synced yet, and healing on that alone
|
|
637
|
+
// would drop a legitimate marker another device wrote after
|
|
638
|
+
// reading newer messages. A real read marker can never be ahead
|
|
639
|
+
// of real time, so that second signal is what distinguishes a
|
|
640
|
+
// stale cache from a wrong clock.
|
|
641
|
+
const nowMs = Date.now();
|
|
642
|
+
const looksCorrupt = (existing: number) =>
|
|
643
|
+
newestKnownMs > 0 &&
|
|
644
|
+
existing > newestKnownMs + FUTURE_MARKER_TOLERANCE_MS &&
|
|
645
|
+
existing > nowMs + FUTURE_MARKER_TOLERANCE_MS;
|
|
646
|
+
|
|
595
647
|
// Remember the marker so rooms that load LATER (via addRoom)
|
|
596
648
|
// inherit it even though they don't exist in the store yet.
|
|
597
|
-
|
|
649
|
+
const cachedMarker = state.privateStoreMarkers[jid] || 0;
|
|
650
|
+
if (ts > cachedMarker || looksCorrupt(cachedMarker)) {
|
|
598
651
|
state.privateStoreMarkers[jid] = ts;
|
|
599
652
|
}
|
|
600
653
|
// Upgrade an already-loaded room's baseline + recompute its badge.
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
if (room && ts > (room.lastViewedTimestamp || 0)) {
|
|
654
|
+
// Forward-only, unless the room's CURRENT baseline is itself the
|
|
655
|
+
// corrupt value being corrected.
|
|
656
|
+
if (room && (ts > (room.lastViewedTimestamp || 0) || looksCorrupt(room.lastViewedTimestamp || 0))) {
|
|
604
657
|
room.lastViewedTimestamp = ts;
|
|
605
658
|
room.unreadMessages = countNewerMessages(room.messages, ts);
|
|
606
659
|
}
|
|
@@ -872,6 +925,31 @@ const isOwn = (
|
|
|
872
925
|
return false;
|
|
873
926
|
};
|
|
874
927
|
|
|
928
|
+
// Bug #38 tolerance: a stored read marker more than this far past the
|
|
929
|
+
// newest message a room actually has is not "the user is a little
|
|
930
|
+
// ahead" - it's a leftover from a device whose clock was wrong when it
|
|
931
|
+
// stamped the marker. Kept in sync with (but duplicated from, to avoid
|
|
932
|
+
// a roomStore ↔ helpers import cycle) FUTURE_READ_MARKER_TOLERANCE_MS
|
|
933
|
+
// in helpers/getServerReadTimestamp.ts.
|
|
934
|
+
const FUTURE_MARKER_TOLERANCE_MS = 5 * 60 * 1000;
|
|
935
|
+
|
|
936
|
+
// Newest message in `messages` this room actually has proof the server
|
|
937
|
+
// accepted - used only to sanity-check an already-stored read marker
|
|
938
|
+
// against reality (see applyPrivateStoreMarkers's self-heal). Excludes
|
|
939
|
+
// pending/optimistic sends via the `pending` flag alone: unlike
|
|
940
|
+
// getServerReadTimestamp, this runs inside a reducer and can't reach
|
|
941
|
+
// across to the roomHeapSlice slice, but `pending` already identifies
|
|
942
|
+
// every optimistic send that matters for this comparison.
|
|
943
|
+
const newestAckedMessageMs = (messages: IMessage[] | undefined): number => {
|
|
944
|
+
let newest = 0;
|
|
945
|
+
for (const m of messages || []) {
|
|
946
|
+
if (!m || m.id === 'delimiter-new' || m.pending) {continue;}
|
|
947
|
+
const ms = msgSortableMs(m);
|
|
948
|
+
if (ms > newest) {newest = ms;}
|
|
949
|
+
}
|
|
950
|
+
return newest;
|
|
951
|
+
};
|
|
952
|
+
|
|
875
953
|
const countNewerMessages = (
|
|
876
954
|
messages: IMessage[],
|
|
877
955
|
timestamp: number,
|