@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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
setVisibleRoom,
|
|
8
8
|
} from '../roomStore/roomsSlice';
|
|
9
9
|
import { store } from '../roomStore';
|
|
10
|
+
import { getServerReadTimestamp } from '../helpers/getServerReadTimestamp';
|
|
10
11
|
|
|
11
12
|
interface UseChatRoomFocusOptions {
|
|
12
13
|
/** The room JID that the consumer's tab/screen is currently showing. */
|
|
@@ -32,7 +33,10 @@ interface UseChatRoomFocusOptions {
|
|
|
32
33
|
* Without this hook, `useUnread()` will always return 0 for the chat
|
|
33
34
|
* room because the SDK assumes "mounted == active". With this hook,
|
|
34
35
|
* focus marks the room visible (clearing the badge) and blur stamps
|
|
35
|
-
* `lastViewedTimestamp
|
|
36
|
+
* `lastViewedTimestamp` to the newest SERVER-acknowledged message so
|
|
37
|
+
* future messages count as unread. Deliberately not `Date.now()`: a
|
|
38
|
+
* device clock running ahead would write a future marker that the
|
|
39
|
+
* forward-only private-store merge could never correct again (bug #38).
|
|
36
40
|
*
|
|
37
41
|
* Usage with React Navigation:
|
|
38
42
|
*
|
|
@@ -59,11 +63,16 @@ export const useChatRoomFocus = ({
|
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
const leaveRoom = (jid: string) => {
|
|
62
|
-
const
|
|
63
|
-
|
|
66
|
+
const state = store.getState();
|
|
67
|
+
const rooms = state.rooms?.rooms;
|
|
68
|
+
const timestamp = getServerReadTimestamp(rooms?.[jid], state.roomHeapSlice);
|
|
69
|
+
// Only stamp when we actually have something to anchor to - skip
|
|
70
|
+
// rather than fall back to the device clock (bug #38).
|
|
71
|
+
if (timestamp > 0) {
|
|
72
|
+
dispatch(setLastViewedTimestamp({ chatJID: jid, timestamp }));
|
|
73
|
+
}
|
|
64
74
|
dispatch(clearVisibleRoom());
|
|
65
|
-
|
|
66
|
-
(store.getState().chatSettingStore as any)?.client
|
|
75
|
+
(state.chatSettingStore as any)?.client
|
|
67
76
|
?.flushLastViewedToPrivateStoreStanza(rooms, { visibleRoomJID: jid })
|
|
68
77
|
.catch(() => {});
|
|
69
78
|
};
|
|
@@ -8,7 +8,6 @@ import { AppDispatch, RootState } from '../roomStore';
|
|
|
8
8
|
import { useXmppClient } from '../context/xmppProvider';
|
|
9
9
|
import { chatAutoEnterer } from '../helpers/chatAutoEnterer';
|
|
10
10
|
import { initRoomsPresence } from '../helpers/initRoomsPresence';
|
|
11
|
-
import { updatedChatLastTimestamps } from '../helpers/updatedChatLastTimestamps';
|
|
12
11
|
import { updateMessagesTillLast } from '../helpers/updateMessagesTillLast';
|
|
13
12
|
import { refreshAuthTokensQuietly } from '../networking/authRefresh';
|
|
14
13
|
import { setLangSource, setConfig } from '../roomStore/chatSettingsSlice';
|
|
@@ -170,15 +169,21 @@ const useChatWrapperInit = ({
|
|
|
170
169
|
}
|
|
171
170
|
}
|
|
172
171
|
try {
|
|
172
|
+
// getChatsPrivateStoreRequestStanza hydrates redux itself
|
|
173
|
+
// (dispatch(applyPrivateStoreMarkers(...)) internally,
|
|
174
|
+
// clamped + forward-only) - do not also apply the raw
|
|
175
|
+
// return value here. That used to double-dispatch via
|
|
176
|
+
// updatedChatLastTimestamps' unconditional
|
|
177
|
+
// setLastViewedTimestamp, which has neither the
|
|
178
|
+
// forward-only guard nor the future-marker clamp, so on
|
|
179
|
+
// every reconnect it could silently re-clobber a value
|
|
180
|
+
// the clamp had just corrected (bug #38).
|
|
173
181
|
const roomTimestampObject: any =
|
|
174
182
|
await newClient.getChatsPrivateStoreRequestStanza();
|
|
175
183
|
console.log(
|
|
176
184
|
'Got chats private store',
|
|
177
185
|
roomTimestampObject?.length || 0
|
|
178
186
|
);
|
|
179
|
-
if (roomTimestampObject) {
|
|
180
|
-
updatedChatLastTimestamps(roomTimestampObject, dispatch);
|
|
181
|
-
}
|
|
182
187
|
if (!hasSyncedHistoryRef.current) {
|
|
183
188
|
console.log('Updating messages till last');
|
|
184
189
|
await updateMessagesTillLast(rooms, newClient);
|
|
@@ -220,15 +225,15 @@ const useChatWrapperInit = ({
|
|
|
220
225
|
setInited(true);
|
|
221
226
|
console.log(' Getting chats private store for existing client');
|
|
222
227
|
try {
|
|
228
|
+
// See the "new client" branch above: getChatsPrivateStoreRequestStanza
|
|
229
|
+
// already hydrates redux (clamped + forward-only); do not
|
|
230
|
+
// also apply the raw return value here.
|
|
223
231
|
const roomTimestampObject: any =
|
|
224
232
|
await client.getChatsPrivateStoreRequestStanza();
|
|
225
233
|
console.log(
|
|
226
234
|
'Got chats private store for existing client',
|
|
227
235
|
roomTimestampObject?.length || 0
|
|
228
236
|
);
|
|
229
|
-
if (roomTimestampObject) {
|
|
230
|
-
updatedChatLastTimestamps(roomTimestampObject, dispatch);
|
|
231
|
-
}
|
|
232
237
|
if (!hasSyncedHistoryRef.current) {
|
|
233
238
|
console.log(
|
|
234
239
|
'Updating messages till last for existing client'
|
|
@@ -227,11 +227,11 @@ const onMessageHistory = async (stanza: any) => {
|
|
|
227
227
|
// what senderJID would have held — fall back to it. createMessageFromXml
|
|
228
228
|
// already derives a display name from the jid when senderFirstName/
|
|
229
229
|
// senderLastName are absent.
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
const forwardedMessage = stanza
|
|
231
|
+
.getChild('result')
|
|
232
|
+
?.getChild('forwarded')
|
|
233
|
+
?.getChild('message');
|
|
234
|
+
const innerFrom = forwardedMessage?.attrs?.from || stanza.attrs.from;
|
|
235
235
|
const senderJID = data?.attrs?.senderJID || innerFrom;
|
|
236
236
|
if (!data?.attrs || !senderJID) {
|
|
237
237
|
// console.log(
|
|
@@ -242,11 +242,25 @@ const onMessageHistory = async (stanza: any) => {
|
|
|
242
242
|
}
|
|
243
243
|
const mergedAttrs = { ...data.attrs };
|
|
244
244
|
if (!mergedAttrs.senderJID) {mergedAttrs.senderJID = senderJID;}
|
|
245
|
+
// The forwarded <message>'s own stanza id is our original send id
|
|
246
|
+
// (what `xmppId` holds on the live/wrapped paths) - capture it so
|
|
247
|
+
// this MAM-replayed message dedupes against its live echo /
|
|
248
|
+
// optimistic pending bubble the same way, instead of never carrying
|
|
249
|
+
// an xmppId at all.
|
|
250
|
+
if (forwardedMessage?.attrs?.id) {mergedAttrs.xmppId = forwardedMessage.attrs.id;}
|
|
245
251
|
const rawMessage = await createMessageFromXml(
|
|
246
252
|
mergedAttrs,
|
|
247
253
|
body,
|
|
248
254
|
id,
|
|
249
|
-
|
|
255
|
+
// Bug #41: pass the resource-bearing occupant `from`
|
|
256
|
+
// (room@conference/sender-id) here, not the outer envelope's
|
|
257
|
+
// `stanza.attrs.from` (which has no resource for a MAM result).
|
|
258
|
+
// createMessageFromXml derives `user.id` from this field's
|
|
259
|
+
// resource - the same authoritative source getDataFromXml uses
|
|
260
|
+
// for live messages - so catch-up rows resolve the correct
|
|
261
|
+
// sender from the very first render instead of needing a later
|
|
262
|
+
// correction once the "official" history fetch lands.
|
|
263
|
+
innerFrom,
|
|
250
264
|
!!deleted
|
|
251
265
|
);
|
|
252
266
|
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import { getChatsPrivateStoreRequest } from './getChatsPrivateStoreRequest.xmpp';
|
|
2
2
|
import { setChatsPrivateStoreRequest } from './setChatsPrivateStoreRequest.xmpp';
|
|
3
|
+
import {
|
|
4
|
+
getServerReadTimestamp,
|
|
5
|
+
isCorruptFutureReadMarker,
|
|
6
|
+
type ReadTimestampHeapState,
|
|
7
|
+
} from '../../helpers/getServerReadTimestamp';
|
|
8
|
+
import type { IMessage } from '../../types/types';
|
|
3
9
|
|
|
4
10
|
interface RoomLike {
|
|
5
11
|
jid: string;
|
|
6
12
|
lastViewedTimestamp?: number;
|
|
7
13
|
unreadMessages?: number;
|
|
14
|
+
// Needed to resolve a SERVER-timestamp read marker via
|
|
15
|
+
// getServerReadTimestamp instead of the device clock - see bug #38.
|
|
16
|
+
messages?: IMessage[];
|
|
8
17
|
}
|
|
9
18
|
|
|
10
19
|
interface FlushOpts {
|
|
11
20
|
// When visibleRoomJID is set, treat it as "user just viewed everything
|
|
12
|
-
// up to now" and stamp it with
|
|
13
|
-
//
|
|
21
|
+
// up to now" and stamp it with the newest server-acknowledged message
|
|
22
|
+
// timestamp for that room - even if its in-memory lastViewedTimestamp
|
|
23
|
+
// has not been updated yet.
|
|
14
24
|
visibleRoomJID?: string | null;
|
|
15
|
-
// Explicit read boundary for the visible room, used INSTEAD of
|
|
16
|
-
//
|
|
17
|
-
// user actually reached when they leave a room while
|
|
18
|
-
// without it, the
|
|
19
|
-
// server and the messages they never scrolled down to come back as
|
|
25
|
+
// Explicit read boundary for the visible room, used INSTEAD of the
|
|
26
|
+
// newest-message fallback. ChatRoom passes the timestamp of the newest
|
|
27
|
+
// message the user actually reached when they leave a room while
|
|
28
|
+
// scrolled up - without it, the default writes "read everything" to
|
|
29
|
+
// the server and the messages they never scrolled down to come back as
|
|
20
30
|
// read on the next login, even though the local count was correct.
|
|
21
31
|
// Customer-reported #33 (server half).
|
|
22
32
|
visibleRoomTs?: number | null;
|
|
@@ -24,6 +34,12 @@ interface FlushOpts {
|
|
|
24
34
|
// (or the visible room). Used at logout time: rooms with outstanding
|
|
25
35
|
// unread keep their old marker so the next login still surfaces them.
|
|
26
36
|
onlyIfNoUnread?: boolean;
|
|
37
|
+
// Pending/failed-send state (store.getState().roomHeapSlice), combined
|
|
38
|
+
// with each room's `messages` to resolve the newest SERVER-acked
|
|
39
|
+
// message timestamp via getServerReadTimestamp. Passed in rather than
|
|
40
|
+
// read from the store here so this module stays a pure function of its
|
|
41
|
+
// arguments - existing jest coverage builds `rooms` by hand.
|
|
42
|
+
heapState?: ReadTimestampHeapState;
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
/**
|
|
@@ -33,8 +49,11 @@ interface FlushOpts {
|
|
|
33
49
|
* each other's entries.
|
|
34
50
|
*
|
|
35
51
|
* Called on: tab change away from chat, AppState background/inactive,
|
|
36
|
-
* and (with onlyIfNoUnread=true) logout. ChatRoom's own unmount path
|
|
37
|
-
*
|
|
52
|
+
* and (with onlyIfNoUnread=true) logout. ChatRoom's own unmount path also
|
|
53
|
+
* goes through this (via flushLastViewedToPrivateStoreStanza) for the
|
|
54
|
+
* single-room case - actionSetTimestampToPrivateStore is a separate,
|
|
55
|
+
* unconditional low-level write primitive kept for direct/external use
|
|
56
|
+
* and is not part of this merge path.
|
|
38
57
|
*/
|
|
39
58
|
export async function flushLastViewedToPrivateStore(
|
|
40
59
|
client: any,
|
|
@@ -42,7 +61,7 @@ export async function flushLastViewedToPrivateStore(
|
|
|
42
61
|
opts: FlushOpts = {}
|
|
43
62
|
): Promise<boolean> {
|
|
44
63
|
if (!client?.client) {return false;}
|
|
45
|
-
const { visibleRoomJID, visibleRoomTs, onlyIfNoUnread } = opts;
|
|
64
|
+
const { visibleRoomJID, visibleRoomTs, onlyIfNoUnread, heapState } = opts;
|
|
46
65
|
const roomList = Object.values(rooms || {});
|
|
47
66
|
if (roomList.length === 0) {return false;}
|
|
48
67
|
|
|
@@ -54,7 +73,6 @@ export async function flushLastViewedToPrivateStore(
|
|
|
54
73
|
}
|
|
55
74
|
if (!storeObj || typeof storeObj !== 'object') {storeObj = {};}
|
|
56
75
|
|
|
57
|
-
const nowMs = Date.now();
|
|
58
76
|
let dirty = false;
|
|
59
77
|
|
|
60
78
|
for (const room of roomList) {
|
|
@@ -62,27 +80,56 @@ export async function flushLastViewedToPrivateStore(
|
|
|
62
80
|
const isVisible = !!visibleRoomJID && room.jid === visibleRoomJID;
|
|
63
81
|
const hasUnread = Number(room.unreadMessages || 0) > 0;
|
|
64
82
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
83
|
+
// Newest message this room actually has server-side proof of. Used
|
|
84
|
+
// both as the "read up to now" fallback for the visible room and as
|
|
85
|
+
// the yardstick for detecting a corrupt (future) stored marker below
|
|
86
|
+
// - never as a substitute for the device clock (bug #38: a device
|
|
87
|
+
// with a fast clock previously stamped Date.now() here, and once
|
|
88
|
+
// that future value reached the server the forward-only check a few
|
|
89
|
+
// lines down rejected every later correct write, forever).
|
|
90
|
+
const newestAckedMs = getServerReadTimestamp(room as any, heapState);
|
|
91
|
+
|
|
92
|
+
// For the visible room, persist "read up to the newest message we
|
|
93
|
+
// know the server has" - read state is driven by room visibility,
|
|
94
|
+
// not by a sentinel timestamp. The exception is an explicit
|
|
95
|
+
// `visibleRoomTs`: the caller knows the user only read up to a
|
|
96
|
+
// certain point (left the room while scrolled up), so stamping
|
|
97
|
+
// "everything" here would mark genuinely-unread messages as read.
|
|
70
98
|
let ts: number | undefined;
|
|
71
99
|
if (isVisible) {
|
|
72
100
|
ts =
|
|
73
101
|
typeof visibleRoomTs === 'number' && visibleRoomTs > 0
|
|
74
102
|
? visibleRoomTs
|
|
75
|
-
:
|
|
103
|
+
: newestAckedMs || undefined;
|
|
76
104
|
} else if (room.lastViewedTimestamp && room.lastViewedTimestamp > 0) {
|
|
77
105
|
ts = room.lastViewedTimestamp;
|
|
78
106
|
}
|
|
79
107
|
|
|
108
|
+
// Nothing to anchor a write to (no boundary, no existing marker, no
|
|
109
|
+
// known messages yet) - skip rather than write a device-clock guess.
|
|
80
110
|
if (!ts) {continue;}
|
|
81
111
|
if (onlyIfNoUnread && hasUnread && !isVisible) {continue;}
|
|
82
112
|
|
|
83
113
|
const prev = storeObj[room.jid];
|
|
84
114
|
const prevNum = prev != null ? Number(prev) : 0;
|
|
85
|
-
|
|
115
|
+
// A currently-stored value that sits well past the newest message we
|
|
116
|
+
// know this room has is not "fresher" - it's a leftover future
|
|
117
|
+
// marker (bug #38). Forward-only logic normally protects a fresher
|
|
118
|
+
// read from being clobbered by a stale fetch, but applied to a
|
|
119
|
+
// corrupt future value it does the opposite: it permanently blocks
|
|
120
|
+
// the correct (smaller) value from ever landing. Detect that case
|
|
121
|
+
// and waive the forward-only check for it specifically; sane values
|
|
122
|
+
// keep the normal forward-only protection.
|
|
123
|
+
//
|
|
124
|
+
// "Well past the newest message we know about" is deliberately NOT
|
|
125
|
+
// sufficient on its own - see isCorruptFutureReadMarker. THIS device's
|
|
126
|
+
// copy of the room can simply be stale (history not synced yet this
|
|
127
|
+
// session), in which case a legitimate marker written by another
|
|
128
|
+
// device that HAS seen newer messages looks exactly like a corrupt
|
|
129
|
+
// one, and "healing" it here would push the server marker BACKWARDS
|
|
130
|
+
// and resurrect already-read messages on every device.
|
|
131
|
+
const prevLooksCorrupt = isCorruptFutureReadMarker(prevNum, newestAckedMs);
|
|
132
|
+
if (!prevLooksCorrupt && Number.isFinite(prevNum) && prevNum >= ts) {continue;}
|
|
86
133
|
storeObj[room.jid] = String(ts);
|
|
87
134
|
dirty = true;
|
|
88
135
|
}
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
enqueueOutboundSend,
|
|
33
33
|
flushOutboundSends,
|
|
34
34
|
} from './outboundQueue';
|
|
35
|
+
import {
|
|
36
|
+
getServerReadTimestamp,
|
|
37
|
+
isCorruptFutureReadMarker,
|
|
38
|
+
} from '../helpers/getServerReadTimestamp';
|
|
35
39
|
|
|
36
40
|
// Canonical production XMPP WSS endpoint (standard wss/443, no port suffix).
|
|
37
41
|
const DEFAULT_DEV_SERVER = 'xmpp.chat.ethora.com';
|
|
@@ -1060,9 +1064,37 @@ export class XmppClient {
|
|
|
1060
1064
|
// `getChatsPrivateStoreRequest` directly and intentionally skip this.
|
|
1061
1065
|
if (markers && typeof markers === 'object') {
|
|
1062
1066
|
const normalized: Record<string, number> = {};
|
|
1067
|
+
const roomsState = store.getState().rooms?.rooms || {};
|
|
1068
|
+
const heapState = store.getState().roomHeapSlice;
|
|
1063
1069
|
for (const jid of Object.keys(markers as Record<string, unknown>)) {
|
|
1064
1070
|
const n = Number((markers as Record<string, unknown>)[jid]);
|
|
1065
|
-
if (jid
|
|
1071
|
+
if (!jid || !Number.isFinite(n) || n <= 0) {continue;}
|
|
1072
|
+
// Defensive clamp (bug #38): a marker written while some
|
|
1073
|
+
// device's clock was ahead can land in the future relative to
|
|
1074
|
+
// the newest message the server actually has for this room.
|
|
1075
|
+
// The redux merge (applyPrivateStoreMarkers) and the
|
|
1076
|
+
// server-side merge (flushLastViewedToPrivateStore) both treat
|
|
1077
|
+
// "newer" as "better", so an uncorrected future value would
|
|
1078
|
+
// block every later correct write forever - useUnread() would
|
|
1079
|
+
// report 0 for that room on every cold start. When we already
|
|
1080
|
+
// know this room's messages locally, clamp an implausible
|
|
1081
|
+
// marker down to the newest one we actually have instead of
|
|
1082
|
+
// trusting the server value verbatim.
|
|
1083
|
+
//
|
|
1084
|
+
// "Implausible" needs BOTH "past the newest message we know
|
|
1085
|
+
// about" AND "past real time" (isCorruptFutureReadMarker):
|
|
1086
|
+
// this device's cache for a room is routinely stale, and
|
|
1087
|
+
// clamping on staleness alone would drag a perfectly good
|
|
1088
|
+
// marker written by another device backwards.
|
|
1089
|
+
const room = roomsState[jid];
|
|
1090
|
+
const newestKnownMs = room
|
|
1091
|
+
? getServerReadTimestamp(room, heapState)
|
|
1092
|
+
: 0;
|
|
1093
|
+
if (isCorruptFutureReadMarker(n, newestKnownMs)) {
|
|
1094
|
+
normalized[jid] = newestKnownMs;
|
|
1095
|
+
} else {
|
|
1096
|
+
normalized[jid] = n;
|
|
1097
|
+
}
|
|
1066
1098
|
}
|
|
1067
1099
|
if (Object.keys(normalized).length > 0) {
|
|
1068
1100
|
store.dispatch(applyPrivateStoreMarkers(normalized));
|
|
@@ -1104,7 +1136,14 @@ export class XmppClient {
|
|
|
1104
1136
|
) {
|
|
1105
1137
|
if (this.disableLastRead) {return false;}
|
|
1106
1138
|
try {
|
|
1107
|
-
|
|
1139
|
+
// Auto-inject the pending/failed-send heap so every caller gets
|
|
1140
|
+
// getServerReadTimestamp's exclusion of optimistic messages for
|
|
1141
|
+
// free, without needing to reach into the redux store themselves.
|
|
1142
|
+
const heapState = store.getState().roomHeapSlice;
|
|
1143
|
+
return await flushLastViewedToPrivateStore(this, rooms, {
|
|
1144
|
+
...opts,
|
|
1145
|
+
heapState,
|
|
1146
|
+
});
|
|
1108
1147
|
} catch {
|
|
1109
1148
|
return false;
|
|
1110
1149
|
}
|
package/src/roomStore/index.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { configureStore, combineReducers } from '@reduxjs/toolkit';
|
|
2
2
|
import chatSettingsReducer, { setUser } from './chatSettingsSlice';
|
|
3
3
|
import roomsSlice, { addRoom } from './roomsSlice';
|
|
4
|
-
import { roomHeapSlice } from './roomHeapSlice';
|
|
4
|
+
import { roomHeapSlice, markMessageFailed } from './roomHeapSlice';
|
|
5
5
|
import callReducer from './callSlice';
|
|
6
6
|
import { IRoom } from '../types/types';
|
|
7
7
|
import { unreadMiddleware } from './Middleware/unreadMidlleware';
|
|
8
8
|
import { logoutMiddleware } from './Middleware/logoutMiddleware';
|
|
9
9
|
import { newMessageMidlleware } from './Middleware/newMessageMidlleware';
|
|
10
10
|
import { reactionsMiddleware } from './Middleware/reactionsMiddleware';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
persistenceMiddleware,
|
|
13
|
+
readPersistedState,
|
|
14
|
+
computeBootTimeFailures,
|
|
15
|
+
} from './persistence';
|
|
12
16
|
|
|
13
17
|
const rootReducer = combineReducers({
|
|
14
18
|
chatSettingStore: chatSettingsReducer,
|
|
@@ -68,11 +72,12 @@ export const getActiveRoom = (state: RootState): IRoom | null => {
|
|
|
68
72
|
: null;
|
|
69
73
|
};
|
|
70
74
|
|
|
71
|
-
// Async rehydrate
|
|
75
|
+
// Async rehydrate: read persisted slices and replay them as standard
|
|
76
|
+
// actions.
|
|
72
77
|
export const persistorReady =
|
|
73
78
|
globalScope.__CHAT_PERSISTOR_READY__ ||
|
|
74
79
|
(globalScope.__CHAT_PERSISTOR_READY__ = (async () => {
|
|
75
|
-
const { chat, rooms } = await readPersistedState();
|
|
80
|
+
const { chat, rooms, heap } = await readPersistedState();
|
|
76
81
|
if (chat?.user && chat.user.walletAddress) {
|
|
77
82
|
store.dispatch(setUser(chat.user));
|
|
78
83
|
}
|
|
@@ -82,4 +87,35 @@ export const persistorReady =
|
|
|
82
87
|
store.dispatch(addRoom({ roomData: room as IRoom }));
|
|
83
88
|
}
|
|
84
89
|
}
|
|
90
|
+
|
|
91
|
+
// bug #39: restore failures that were already flagged (watchdog or
|
|
92
|
+
// an explicit send error) before the app was killed, so the bubble
|
|
93
|
+
// still shows "Failed / tap to retry" instead of quietly reverting
|
|
94
|
+
// to a normal sent bubble.
|
|
95
|
+
// Skip entries whose bubble no longer exists in the restored cache.
|
|
96
|
+
// A failure flag is only ever READ as `failedMessages[message.id]`,
|
|
97
|
+
// so one with no matching message is dead weight that would be
|
|
98
|
+
// replayed and re-persisted on every launch forever. This happens
|
|
99
|
+
// for real: a send that reached the server but whose echo was lost
|
|
100
|
+
// gets flagged failed at boot, then the history merge replaces the
|
|
101
|
+
// optimistic bubble with the archived copy under its server id (see
|
|
102
|
+
// mergeHistoryIntoCache), leaving the flag behind.
|
|
103
|
+
const restoredFailed = heap?.failedMessages || {};
|
|
104
|
+
for (const payload of Object.values(restoredFailed)) {
|
|
105
|
+
const stillRendered = (
|
|
106
|
+
rooms?.rooms?.[payload.roomJID]?.messages || []
|
|
107
|
+
).some((m) => m?.id === payload.id);
|
|
108
|
+
if (!stillRendered) {continue;}
|
|
109
|
+
store.dispatch(markMessageFailed(payload));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// bug #39: a message that was still `pending: true` at kill time
|
|
113
|
+
// never got a server echo AND never got the chance to trip the
|
|
114
|
+
// in-session send watchdog (messageHeap is process-lifetime only,
|
|
115
|
+
// never persisted). Treat every such message as failed so it gets
|
|
116
|
+
// a working retry instead of silently rendering as delivered.
|
|
117
|
+
const bootFailures = computeBootTimeFailures(rooms?.rooms, restoredFailed);
|
|
118
|
+
for (const payload of bootFailures) {
|
|
119
|
+
store.dispatch(markMessageFailed(payload));
|
|
120
|
+
}
|
|
85
121
|
})());
|