@ethora/chat-component-rn 26.7.8 → 26.7.10
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/CHANGELOG.md +3 -2
- package/README.md +10 -0
- package/lib/commonjs/assets/icons.js +2 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +76 -19
- 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/ChatProfileModal/ChatProfileModal.js +1 -51
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.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 +95 -36
- 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 +188 -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/insertMessageWithDelimiter.js +51 -9
- package/lib/commonjs/helpers/insertMessageWithDelimiter.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/keyboardLayout.js +12 -2
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/commonjs/helpers/mimeToExtension.js +6 -14
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/helpers/msgSortableMs.js +37 -0
- package/lib/commonjs/helpers/msgSortableMs.js.map +1 -0
- package/lib/commonjs/hooks/useChatRoomFocus.js +45 -12
- 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/hooks/useLogout.js +8 -0
- package/lib/commonjs/hooks/useLogout.js.map +1 -1
- package/lib/commonjs/i18n/strings.js +0 -18
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +0 -13
- package/lib/commonjs/networking/api-requests/rooms.api.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 +148 -37
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/assets/icons.js +1 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +77 -20
- 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/ChatProfileModal/ChatProfileModal.js +3 -53
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.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 +96 -37
- 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 +181 -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/insertMessageWithDelimiter.js +52 -9
- package/lib/module/helpers/insertMessageWithDelimiter.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/keyboardLayout.js +12 -2
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/helpers/mimeToExtension.js +6 -13
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/helpers/msgSortableMs.js +32 -0
- package/lib/module/helpers/msgSortableMs.js.map +1 -0
- package/lib/module/hooks/useChatRoomFocus.js +46 -13
- 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/hooks/useLogout.js +8 -0
- package/lib/module/hooks/useLogout.js.map +1 -1
- package/lib/module/i18n/strings.js +0 -18
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +0 -12
- package/lib/module/networking/api-requests/rooms.api.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 +136 -33
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts +0 -8
- package/lib/typescript/assets/icons.d.ts.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/ChatProfileModal/ChatProfileModal.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/getAvatarColor.d.ts +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 +108 -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/insertMessageWithDelimiter.d.ts +1 -1
- package/lib/typescript/helpers/insertMessageWithDelimiter.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/keyboardLayout.d.ts +5 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts +0 -15
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/helpers/msgSortableMs.d.ts +2 -0
- package/lib/typescript/helpers/msgSortableMs.d.ts.map +1 -0
- 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/hooks/useLogout.d.ts.map +1 -1
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +0 -1
- package/lib/typescript/networking/api-requests/rooms.api.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 +42 -3
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/room.model.d.ts +0 -1
- package/lib/typescript/types/models/room.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +1 -1
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assets/icons.tsx +0 -24
- package/src/components/MainComponents/ChatRoom.tsx +81 -19
- 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/ChatProfileModal/ChatProfileModal.tsx +2 -36
- 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 +122 -30
- package/src/helpers/createMessageFromXml.ts +20 -1
- package/src/helpers/getDisplayFileName.ts +87 -0
- package/src/helpers/getServerReadTimestamp.ts +197 -0
- package/src/helpers/initXmppRooms.tsx +9 -9
- package/src/helpers/insertMessageWithDelimiter.ts +55 -19
- package/src/helpers/isOwnMessage.ts +90 -0
- package/src/helpers/keyboardLayout.ts +23 -2
- package/src/helpers/mimeToExtension.ts +6 -22
- package/src/helpers/msgSortableMs.ts +25 -0
- package/src/hooks/useChatRoomFocus.ts +49 -9
- package/src/hooks/useChatWrapperInit.ts +12 -7
- package/src/hooks/useLogout.tsx +14 -0
- package/src/i18n/strings.ts +0 -18
- package/src/networking/api-requests/rooms.api.ts +0 -9
- 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 +158 -28
- package/src/types/models/room.model.ts +0 -1
- package/src/types/types.ts +14 -1
|
@@ -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
|
})());
|
|
@@ -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 };
|