@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.
Files changed (232) hide show
  1. package/CHANGELOG.md +3 -2
  2. package/README.md +10 -0
  3. package/lib/commonjs/assets/icons.js +2 -2
  4. package/lib/commonjs/assets/icons.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatRoom.js +76 -19
  6. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/MediaMessage.js +8 -5
  8. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/MessageContainer.js +5 -1
  10. package/lib/commonjs/components/MainComponents/MessageContainer.js.map +1 -1
  11. package/lib/commonjs/components/MainComponents/MessageList.js +2 -1
  12. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  13. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +1 -51
  14. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +51 -12
  16. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  17. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +12 -1
  18. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  19. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
  20. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  21. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +13 -2
  22. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  23. package/lib/commonjs/components/styled/MessageImage.js +3 -1
  24. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  25. package/lib/commonjs/components/styled/UnsupportedType.js +14 -4
  26. package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
  27. package/lib/commonjs/components/styled/VideoMessage.js +3 -1
  28. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  29. package/lib/commonjs/context/xmppProvider.js +95 -36
  30. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  31. package/lib/commonjs/helpers/createMessageFromXml.js +19 -1
  32. package/lib/commonjs/helpers/createMessageFromXml.js.map +1 -1
  33. package/lib/commonjs/helpers/getDisplayFileName.js +79 -0
  34. package/lib/commonjs/helpers/getDisplayFileName.js.map +1 -0
  35. package/lib/commonjs/helpers/getServerReadTimestamp.js +188 -0
  36. package/lib/commonjs/helpers/getServerReadTimestamp.js.map +1 -0
  37. package/lib/commonjs/helpers/initXmppRooms.js +9 -7
  38. package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
  39. package/lib/commonjs/helpers/insertMessageWithDelimiter.js +51 -9
  40. package/lib/commonjs/helpers/insertMessageWithDelimiter.js.map +1 -1
  41. package/lib/commonjs/helpers/isOwnMessage.js +75 -0
  42. package/lib/commonjs/helpers/isOwnMessage.js.map +1 -0
  43. package/lib/commonjs/helpers/keyboardLayout.js +12 -2
  44. package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
  45. package/lib/commonjs/helpers/mimeToExtension.js +6 -14
  46. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
  47. package/lib/commonjs/helpers/msgSortableMs.js +37 -0
  48. package/lib/commonjs/helpers/msgSortableMs.js.map +1 -0
  49. package/lib/commonjs/hooks/useChatRoomFocus.js +45 -12
  50. package/lib/commonjs/hooks/useChatRoomFocus.js.map +1 -1
  51. package/lib/commonjs/hooks/useChatWrapperInit.js +12 -7
  52. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  53. package/lib/commonjs/hooks/useLogout.js +8 -0
  54. package/lib/commonjs/hooks/useLogout.js.map +1 -1
  55. package/lib/commonjs/i18n/strings.js +0 -18
  56. package/lib/commonjs/i18n/strings.js.map +1 -1
  57. package/lib/commonjs/networking/api-requests/rooms.api.js +0 -13
  58. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  59. package/lib/commonjs/networking/stanzaHandlers.js +20 -2
  60. package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
  61. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
  62. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  63. package/lib/commonjs/networking/xmppClient.js +36 -2
  64. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  65. package/lib/commonjs/roomStore/index.js +35 -2
  66. package/lib/commonjs/roomStore/index.js.map +1 -1
  67. package/lib/commonjs/roomStore/persistence.js +200 -11
  68. package/lib/commonjs/roomStore/persistence.js.map +1 -1
  69. package/lib/commonjs/roomStore/roomsSlice.js +148 -37
  70. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  71. package/lib/module/assets/icons.js +1 -1
  72. package/lib/module/assets/icons.js.map +1 -1
  73. package/lib/module/components/MainComponents/ChatRoom.js +77 -20
  74. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  75. package/lib/module/components/MainComponents/MediaMessage.js +9 -6
  76. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  77. package/lib/module/components/MainComponents/MessageContainer.js +5 -1
  78. package/lib/module/components/MainComponents/MessageContainer.js.map +1 -1
  79. package/lib/module/components/MainComponents/MessageList.js +2 -1
  80. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  81. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +3 -53
  82. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  83. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +52 -13
  84. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  85. package/lib/module/components/RoomComponents/styled/LastMessageFile.js +12 -1
  86. package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  87. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
  88. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  89. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +13 -2
  90. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  91. package/lib/module/components/styled/MessageImage.js +3 -1
  92. package/lib/module/components/styled/MessageImage.js.map +1 -1
  93. package/lib/module/components/styled/UnsupportedType.js +14 -4
  94. package/lib/module/components/styled/UnsupportedType.js.map +1 -1
  95. package/lib/module/components/styled/VideoMessage.js +3 -1
  96. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  97. package/lib/module/context/xmppProvider.js +96 -37
  98. package/lib/module/context/xmppProvider.js.map +1 -1
  99. package/lib/module/helpers/createMessageFromXml.js +19 -1
  100. package/lib/module/helpers/createMessageFromXml.js.map +1 -1
  101. package/lib/module/helpers/getDisplayFileName.js +73 -0
  102. package/lib/module/helpers/getDisplayFileName.js.map +1 -0
  103. package/lib/module/helpers/getServerReadTimestamp.js +181 -0
  104. package/lib/module/helpers/getServerReadTimestamp.js.map +1 -0
  105. package/lib/module/helpers/initXmppRooms.js +9 -7
  106. package/lib/module/helpers/initXmppRooms.js.map +1 -1
  107. package/lib/module/helpers/insertMessageWithDelimiter.js +52 -9
  108. package/lib/module/helpers/insertMessageWithDelimiter.js.map +1 -1
  109. package/lib/module/helpers/isOwnMessage.js +70 -0
  110. package/lib/module/helpers/isOwnMessage.js.map +1 -0
  111. package/lib/module/helpers/keyboardLayout.js +12 -2
  112. package/lib/module/helpers/keyboardLayout.js.map +1 -1
  113. package/lib/module/helpers/mimeToExtension.js +6 -13
  114. package/lib/module/helpers/mimeToExtension.js.map +1 -1
  115. package/lib/module/helpers/msgSortableMs.js +32 -0
  116. package/lib/module/helpers/msgSortableMs.js.map +1 -0
  117. package/lib/module/hooks/useChatRoomFocus.js +46 -13
  118. package/lib/module/hooks/useChatRoomFocus.js.map +1 -1
  119. package/lib/module/hooks/useChatWrapperInit.js +12 -7
  120. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  121. package/lib/module/hooks/useLogout.js +8 -0
  122. package/lib/module/hooks/useLogout.js.map +1 -1
  123. package/lib/module/i18n/strings.js +0 -18
  124. package/lib/module/i18n/strings.js.map +1 -1
  125. package/lib/module/networking/api-requests/rooms.api.js +0 -12
  126. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  127. package/lib/module/networking/stanzaHandlers.js +20 -2
  128. package/lib/module/networking/stanzaHandlers.js.map +1 -1
  129. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
  130. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  131. package/lib/module/networking/xmppClient.js +36 -2
  132. package/lib/module/networking/xmppClient.js.map +1 -1
  133. package/lib/module/roomStore/index.js +37 -4
  134. package/lib/module/roomStore/index.js.map +1 -1
  135. package/lib/module/roomStore/persistence.js +197 -11
  136. package/lib/module/roomStore/persistence.js.map +1 -1
  137. package/lib/module/roomStore/roomsSlice.js +136 -33
  138. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  139. package/lib/typescript/assets/icons.d.ts +0 -8
  140. package/lib/typescript/assets/icons.d.ts.map +1 -1
  141. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  142. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  143. package/lib/typescript/components/MainComponents/MessageContainer.d.ts.map +1 -1
  144. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  145. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  146. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  147. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts +1 -1
  148. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
  149. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts +1 -1
  150. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
  151. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts +1 -1
  152. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
  153. package/lib/typescript/components/styled/MessageImage.d.ts +6 -0
  154. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  155. package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
  156. package/lib/typescript/components/styled/VideoMessage.d.ts +6 -0
  157. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  158. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  159. package/lib/typescript/helpers/createMessageFromXml.d.ts.map +1 -1
  160. package/lib/typescript/helpers/getAvatarColor.d.ts +1 -1
  161. package/lib/typescript/helpers/getDisplayFileName.d.ts +49 -0
  162. package/lib/typescript/helpers/getDisplayFileName.d.ts.map +1 -0
  163. package/lib/typescript/helpers/getServerReadTimestamp.d.ts +108 -0
  164. package/lib/typescript/helpers/getServerReadTimestamp.d.ts.map +1 -0
  165. package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
  166. package/lib/typescript/helpers/insertMessageWithDelimiter.d.ts +1 -1
  167. package/lib/typescript/helpers/insertMessageWithDelimiter.d.ts.map +1 -1
  168. package/lib/typescript/helpers/isOwnMessage.d.ts +36 -0
  169. package/lib/typescript/helpers/isOwnMessage.d.ts.map +1 -0
  170. package/lib/typescript/helpers/keyboardLayout.d.ts +5 -1
  171. package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
  172. package/lib/typescript/helpers/mimeToExtension.d.ts +0 -15
  173. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
  174. package/lib/typescript/helpers/msgSortableMs.d.ts +2 -0
  175. package/lib/typescript/helpers/msgSortableMs.d.ts.map +1 -0
  176. package/lib/typescript/hooks/useChatRoomFocus.d.ts +4 -1
  177. package/lib/typescript/hooks/useChatRoomFocus.d.ts.map +1 -1
  178. package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
  179. package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
  180. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  181. package/lib/typescript/networking/api-requests/rooms.api.d.ts +0 -1
  182. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  183. package/lib/typescript/networking/stanzaHandlers.d.ts.map +1 -1
  184. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts +9 -2
  185. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts.map +1 -1
  186. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  187. package/lib/typescript/roomStore/index.d.ts.map +1 -1
  188. package/lib/typescript/roomStore/persistence.d.ts +18 -1
  189. package/lib/typescript/roomStore/persistence.d.ts.map +1 -1
  190. package/lib/typescript/roomStore/roomsSlice.d.ts +42 -3
  191. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  192. package/lib/typescript/types/models/room.model.d.ts +0 -1
  193. package/lib/typescript/types/models/room.model.d.ts.map +1 -1
  194. package/lib/typescript/types/types.d.ts +1 -1
  195. package/lib/typescript/types/types.d.ts.map +1 -1
  196. package/package.json +1 -1
  197. package/src/assets/icons.tsx +0 -24
  198. package/src/components/MainComponents/ChatRoom.tsx +81 -19
  199. package/src/components/MainComponents/MediaMessage.tsx +8 -5
  200. package/src/components/MainComponents/MessageContainer.tsx +5 -1
  201. package/src/components/MainComponents/MessageList.tsx +2 -1
  202. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +2 -36
  203. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +63 -26
  204. package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
  205. package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +14 -2
  206. package/src/components/RoomComponents/styled/LastMessageVideo.tsx +14 -2
  207. package/src/components/styled/MessageImage.tsx +8 -1
  208. package/src/components/styled/UnsupportedType.tsx +14 -4
  209. package/src/components/styled/VideoMessage.tsx +8 -1
  210. package/src/context/xmppProvider.tsx +122 -30
  211. package/src/helpers/createMessageFromXml.ts +20 -1
  212. package/src/helpers/getDisplayFileName.ts +87 -0
  213. package/src/helpers/getServerReadTimestamp.ts +197 -0
  214. package/src/helpers/initXmppRooms.tsx +9 -9
  215. package/src/helpers/insertMessageWithDelimiter.ts +55 -19
  216. package/src/helpers/isOwnMessage.ts +90 -0
  217. package/src/helpers/keyboardLayout.ts +23 -2
  218. package/src/helpers/mimeToExtension.ts +6 -22
  219. package/src/helpers/msgSortableMs.ts +25 -0
  220. package/src/hooks/useChatRoomFocus.ts +49 -9
  221. package/src/hooks/useChatWrapperInit.ts +12 -7
  222. package/src/hooks/useLogout.tsx +14 -0
  223. package/src/i18n/strings.ts +0 -18
  224. package/src/networking/api-requests/rooms.api.ts +0 -9
  225. package/src/networking/stanzaHandlers.ts +20 -6
  226. package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +65 -18
  227. package/src/networking/xmppClient.ts +41 -2
  228. package/src/roomStore/index.ts +40 -4
  229. package/src/roomStore/persistence.ts +197 -7
  230. package/src/roomStore/roomsSlice.ts +158 -28
  231. package/src/types/models/room.model.ts +0 -1
  232. package/src/types/types.ts +14 -1
@@ -2,7 +2,6 @@
2
2
 
3
3
  import AsyncStorage from '@react-native-async-storage/async-storage';
4
4
  import { encryptForPersist, decryptFromPersist } from '../helpers/persistCrypto';
5
-
6
5
  // -------------------------------------------------------------------
7
6
  // Lightweight RN persistence layer. Mirrors what redux-persist gives
8
7
  // the web component:
@@ -21,11 +20,23 @@ import { encryptForPersist, decryptFromPersist } from '../helpers/persistCrypto'
21
20
 
22
21
  const KEY_CHAT = '@ethora/persist:chatSettingStore';
23
22
  const KEY_ROOMS = '@ethora/persist:rooms';
23
+ // bug #39: a failed send needs to survive a relaunch as "failed / tap
24
+ // to retry", not silently render as delivered. `roomHeapStore.failedMessages`
25
+ // is the map the bubble reads (`isFailed = failedMessages[id]`, see
26
+ // Message.tsx / useMessageHeapState): it lives in a separate, previously
27
+ // non-persisted slice from `rooms`, so it gets its own key here.
28
+ const KEY_HEAP = '@ethora/persist:roomHeap';
24
29
 
25
30
  // Per-room message cap on disk. Mirror this value in `roomsSlice`'s
26
31
  // in-memory cap (see `enforceMessageCap`) so the runtime and persisted
27
32
  // shapes stay aligned.
28
33
  export const MESSAGE_LIMIT = 100;
34
+
35
+ // Defensive cap on the number of failed-message payloads written to
36
+ // disk. In practice this map only grows while the user is actively
37
+ // sending offline, but a runaway sender (or a bug that never clears an
38
+ // entry) must not turn this key into an unbounded write.
39
+ export const MAX_PERSISTED_FAILED_MESSAGES = 200;
29
40
  const sanitizeUser = user => {
30
41
  if (!user) {
31
42
  return null;
@@ -50,7 +61,14 @@ const sanitizeUser = user => {
50
61
  // on reload before MAM catches up. That needs the fields the bubbles and
51
62
  // room-list previews actually read, nothing else. Everything outside this
52
63
  // list is either re-derived on render or re-fetched from the server.
53
- const PERSISTED_MESSAGE_FIELDS = ['id', 'xmppId', 'xmppFrom', 'body', 'date', 'timestamp', 'roomJid', 'isSystemMessage', 'isMediafile', 'isDeleted', 'isEdited', 'isReply', 'showInChannel', 'mainMessage', 'mimetype', 'location', 'locationPreview', 'fileName', 'originalName', 'size', 'langSource', 'callLog'];
64
+ const PERSISTED_MESSAGE_FIELDS = ['id', 'xmppId', 'xmppFrom', 'body', 'date', 'timestamp', 'roomJid', 'isSystemMessage', 'isMediafile', 'isDeleted', 'isEdited', 'isReply', 'showInChannel', 'mainMessage', 'mimetype', 'location', 'locationPreview', 'fileName', 'originalName', 'size', 'langSource', 'callLog',
65
+ // bug #39: without this, a message that was still in flight (no
66
+ // server echo yet) at kill time comes back from disk indistinguishable
67
+ // from a normal delivered message, so the bubble renders it as sent.
68
+ // Persisting `pending` lets the boot-time scan (see
69
+ // computeBootTimeFailures below) find exactly the messages that never
70
+ // got confirmed and flip them to failed instead.
71
+ 'pending'];
54
72
 
55
73
  // The sender identity that rides along on every message over the wire
56
74
  // (senderFirstName / senderLastName / photo, which createMessageFromXml
@@ -141,6 +159,167 @@ const sanitizeRooms = rooms => {
141
159
  }
142
160
  return out;
143
161
  };
162
+
163
+ // Only the fields retryMessage actually needs to replay the send (see
164
+ // FailedMessagePayload / useSendMessage.retryMessage). In particular, the
165
+ // media `data` blob is trimmed to `uri/type/name/size` (the picker never
166
+ // hands us auth material, but this stays defensive against a future
167
+ // picker change smuggling extra fields, or a huge inline payload, into
168
+ // `data`).
169
+ const sanitizeMediaData = data => {
170
+ if (!data || typeof data !== 'object') {
171
+ return {};
172
+ }
173
+ const out = {};
174
+ if (typeof data.uri === 'string') {
175
+ out.uri = data.uri;
176
+ }
177
+ if (typeof data.type === 'string') {
178
+ out.type = data.type;
179
+ }
180
+ if (typeof data.name === 'string') {
181
+ out.name = data.name;
182
+ }
183
+ if (typeof data.size === 'number') {
184
+ out.size = data.size;
185
+ }
186
+ return out;
187
+ };
188
+ const sanitizeFailedMessagePayload = payload => {
189
+ if (!payload || typeof payload !== 'object') {
190
+ return null;
191
+ }
192
+ if (!payload.id || typeof payload.id !== 'string') {
193
+ return null;
194
+ }
195
+ if (!payload.roomJID || typeof payload.roomJID !== 'string') {
196
+ return null;
197
+ }
198
+ if (payload.kind === 'media') {
199
+ return {
200
+ kind: 'media',
201
+ id: payload.id,
202
+ roomJID: payload.roomJID,
203
+ data: sanitizeMediaData(payload.data),
204
+ type: typeof payload.type === 'string' ? payload.type : '',
205
+ isReply: payload.isReply,
206
+ isChecked: payload.isChecked,
207
+ mainMessage: payload.mainMessage
208
+ };
209
+ }
210
+ if (payload.kind === 'text') {
211
+ return {
212
+ kind: 'text',
213
+ id: payload.id,
214
+ roomJID: payload.roomJID,
215
+ body: typeof payload.body === 'string' ? payload.body : '',
216
+ isReply: payload.isReply,
217
+ isChecked: payload.isChecked,
218
+ mainMessage: payload.mainMessage
219
+ };
220
+ }
221
+ return null;
222
+ };
223
+ const sanitizeFailedMessages = failedMessages => {
224
+ if (!failedMessages || typeof failedMessages !== 'object') {
225
+ return {};
226
+ }
227
+ const out = {};
228
+ let count = 0;
229
+ for (const [id, payload] of Object.entries(failedMessages)) {
230
+ if (count >= MAX_PERSISTED_FAILED_MESSAGES) {
231
+ break;
232
+ }
233
+ const clean = sanitizeFailedMessagePayload(payload);
234
+ if (clean) {
235
+ out[id] = clean;
236
+ count++;
237
+ }
238
+ }
239
+ return out;
240
+ };
241
+
242
+ // -------------------------------------------------------------------
243
+ // Boot-time pending -> failed conversion (bug #39).
244
+ //
245
+ // `messageHeap` (the "still sending" buffer) is intentionally NOT
246
+ // persisted: it's a process-lifetime thing. That means on a cold start
247
+ // there is no such thing as "still sending": a persisted message that
248
+ // carries `pending: true` never got a server echo NOR got the chance to
249
+ // trip the in-session watchdog (see PENDING_WATCHDOG_MS in
250
+ // useSendMessage.tsx) before the process died. It must render as failed
251
+ // with a working retry, not as delivered and not as stuck "sending...".
252
+ //
253
+ // Reconstructs the same FailedMessagePayload shape the live send path
254
+ // builds in useSendMessage (markMessageFailed calls), from the fields we
255
+ // persist per message.
256
+ // -------------------------------------------------------------------
257
+
258
+ export function reconstructFailedMessagePayload(message, roomJID) {
259
+ if (!message?.id || !roomJID) {
260
+ return null;
261
+ }
262
+ const isReply = !!message.isReply;
263
+ const isChecked = message.showInChannel === 'true';
264
+ const mainMessage = message.mainMessage;
265
+ if (message.isMediafile === 'true') {
266
+ return {
267
+ kind: 'media',
268
+ id: message.id,
269
+ roomJID,
270
+ data: sanitizeMediaData({
271
+ uri: message.location,
272
+ name: message.fileName,
273
+ type: message.mimetype,
274
+ size: message.size !== undefined && message.size !== null ? Number(message.size) : undefined
275
+ }),
276
+ type: message.mimetype || '',
277
+ isReply,
278
+ isChecked,
279
+ mainMessage
280
+ };
281
+ }
282
+ return {
283
+ kind: 'text',
284
+ id: message.id,
285
+ roomJID,
286
+ body: message.body || '',
287
+ isReply,
288
+ isChecked,
289
+ mainMessage
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Scan every persisted room for messages still `pending: true` that
295
+ * DON'T already have a restored failure entry (those are handled by the
296
+ * caller separately, from the persisted `failedMessages` map), and
297
+ * synthesize a retry-able FailedMessagePayload for each. Pure function:
298
+ * the caller (roomStore/index.ts) is responsible for dispatching
299
+ * `markMessageFailed` for the results.
300
+ */
301
+ export function computeBootTimeFailures(rooms, alreadyFailed) {
302
+ if (!rooms || typeof rooms !== 'object') {
303
+ return [];
304
+ }
305
+ const out = [];
306
+ for (const [jid, room] of Object.entries(rooms)) {
307
+ const messages = Array.isArray(room?.messages) ? room.messages : [];
308
+ for (const message of messages) {
309
+ if (!message || !message.pending) {
310
+ continue;
311
+ }
312
+ if (alreadyFailed && alreadyFailed[message.id]) {
313
+ continue;
314
+ }
315
+ const payload = reconstructFailedMessagePayload(message, jid);
316
+ if (payload) {
317
+ out.push(payload);
318
+ }
319
+ }
320
+ }
321
+ return out;
322
+ }
144
323
  let writeTimer = null;
145
324
 
146
325
  /**
@@ -150,7 +329,7 @@ let writeTimer = null;
150
329
  export const persistenceMiddleware = storeAPI => next => action => {
151
330
  const result = next(action);
152
331
  const type = action?.type || '';
153
- if (!type.startsWith('roomMessages/') && !type.startsWith('chat/setUser') && !type.startsWith('chat/updateUser') && !type.startsWith('chat/refreshTokens') && !type.startsWith('chat/logout')) {
332
+ if (!type.startsWith('roomMessages/') && !type.startsWith('roomHeapStore/') && !type.startsWith('chat/setUser') && !type.startsWith('chat/updateUser') && !type.startsWith('chat/refreshTokens') && !type.startsWith('chat/logout')) {
154
333
  return result;
155
334
  }
156
335
  if (writeTimer) {
@@ -166,8 +345,11 @@ export const persistenceMiddleware = storeAPI => next => action => {
166
345
  const roomsPayload = {
167
346
  rooms: sanitizeRooms(state.rooms?.rooms || {})
168
347
  };
169
- const [chatCipher, roomsCipher] = await Promise.all([encryptForPersist(JSON.stringify(chatPayload)), encryptForPersist(JSON.stringify(roomsPayload))]);
170
- await AsyncStorage.multiSet([[KEY_CHAT, chatCipher], [KEY_ROOMS, roomsCipher]]);
348
+ const heapPayload = {
349
+ failedMessages: sanitizeFailedMessages(state.roomHeapSlice?.failedMessages)
350
+ };
351
+ const [chatCipher, roomsCipher, heapCipher] = await Promise.all([encryptForPersist(JSON.stringify(chatPayload)), encryptForPersist(JSON.stringify(roomsPayload)), encryptForPersist(JSON.stringify(heapPayload))]);
352
+ await AsyncStorage.multiSet([[KEY_CHAT, chatCipher], [KEY_ROOMS, roomsCipher], [KEY_HEAP, heapCipher]]);
171
353
  } catch (e) {
172
354
  console.warn('persist write failed', e);
173
355
  }
@@ -183,8 +365,8 @@ export const persistenceMiddleware = storeAPI => next => action => {
183
365
  */
184
366
  export async function readPersistedState() {
185
367
  try {
186
- const [chatRaw, roomsRaw] = await AsyncStorage.multiGet([KEY_CHAT, KEY_ROOMS]);
187
- const [chatPlain, roomsPlain] = await Promise.all([chatRaw[1] ? decryptFromPersist(chatRaw[1]) : Promise.resolve(null), roomsRaw[1] ? decryptFromPersist(roomsRaw[1]) : Promise.resolve(null)]);
368
+ const [chatRaw, roomsRaw, heapRaw] = await AsyncStorage.multiGet([KEY_CHAT, KEY_ROOMS, KEY_HEAP]);
369
+ const [chatPlain, roomsPlain, heapPlain] = await Promise.all([chatRaw[1] ? decryptFromPersist(chatRaw[1]) : Promise.resolve(null), roomsRaw[1] ? decryptFromPersist(roomsRaw[1]) : Promise.resolve(null), heapRaw[1] ? decryptFromPersist(heapRaw[1]) : Promise.resolve(null)]);
188
370
  // `decryptFromPersist` returns null both for "nothing stored" and
189
371
  // for "stored value isn't a valid envelope for the current key" —
190
372
  // notably a plaintext blob left over from a pre-encryption install.
@@ -192,27 +374,31 @@ export async function readPersistedState() {
192
374
  // as a cold start and re-hydrates from the server.
193
375
  const chat = chatPlain ? JSON.parse(chatPlain) : null;
194
376
  const rooms = roomsPlain ? JSON.parse(roomsPlain) : null;
377
+ const heap = heapPlain ? JSON.parse(heapPlain) : null;
195
378
  return {
196
379
  chat,
197
- rooms
380
+ rooms,
381
+ heap
198
382
  };
199
383
  } catch (e) {
200
384
  console.warn('persist read failed', e);
201
385
  return {
202
386
  chat: null,
203
- rooms: null
387
+ rooms: null,
388
+ heap: null
204
389
  };
205
390
  }
206
391
  }
207
392
  export async function clearPersistedState() {
208
393
  try {
209
- await AsyncStorage.multiRemove([KEY_CHAT, KEY_ROOMS]);
394
+ await AsyncStorage.multiRemove([KEY_CHAT, KEY_ROOMS, KEY_HEAP]);
210
395
  } catch (e) {
211
396
  console.warn('persist clear failed', e);
212
397
  }
213
398
  }
214
399
  export const PERSIST_KEYS = {
215
400
  KEY_CHAT,
216
- KEY_ROOMS
401
+ KEY_ROOMS,
402
+ KEY_HEAP
217
403
  };
218
404
  //# sourceMappingURL=persistence.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["AsyncStorage","encryptForPersist","decryptFromPersist","KEY_CHAT","KEY_ROOMS","MESSAGE_LIMIT","sanitizeUser","user","token","_token","refreshToken","_refreshToken","xmppPassword","_xmppPassword","rest","PERSISTED_MESSAGE_FIELDS","PERSISTED_MESSAGE_USER_FIELDS","REFETCHED_ROOM_FIELDS","pickDefined","source","keys","out","key","value","undefined","compactMessageForPersist","message","compact","sanitizeMessages","messages","Array","isArray","capped","length","slice","map","sanitizeRooms","rooms","jid","room","Object","entries","includes","compactRoom","usersCnt","members","field","composing","composingList","isLoading","historyPreloadState","writeTimer","persistenceMiddleware","storeAPI","next","action","result","type","startsWith","clearTimeout","setTimeout","state","getState","chatPayload","chatSettingStore","roomsPayload","chatCipher","roomsCipher","Promise","all","JSON","stringify","multiSet","e","console","warn","readPersistedState","chatRaw","roomsRaw","multiGet","chatPlain","roomsPlain","resolve","chat","parse","clearPersistedState","multiRemove","PERSIST_KEYS"],"sourceRoot":"../../../src","sources":["roomStore/persistence.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["AsyncStorage","encryptForPersist","decryptFromPersist","KEY_CHAT","KEY_ROOMS","KEY_HEAP","MESSAGE_LIMIT","MAX_PERSISTED_FAILED_MESSAGES","sanitizeUser","user","token","_token","refreshToken","_refreshToken","xmppPassword","_xmppPassword","rest","PERSISTED_MESSAGE_FIELDS","PERSISTED_MESSAGE_USER_FIELDS","REFETCHED_ROOM_FIELDS","pickDefined","source","keys","out","key","value","undefined","compactMessageForPersist","message","compact","sanitizeMessages","messages","Array","isArray","capped","length","slice","map","sanitizeRooms","rooms","jid","room","Object","entries","includes","compactRoom","usersCnt","members","field","composing","composingList","isLoading","historyPreloadState","sanitizeMediaData","data","uri","type","name","size","sanitizeFailedMessagePayload","payload","id","roomJID","kind","isReply","isChecked","mainMessage","body","sanitizeFailedMessages","failedMessages","count","clean","reconstructFailedMessagePayload","showInChannel","isMediafile","location","fileName","mimetype","Number","computeBootTimeFailures","alreadyFailed","pending","push","writeTimer","persistenceMiddleware","storeAPI","next","action","result","startsWith","clearTimeout","setTimeout","state","getState","chatPayload","chatSettingStore","roomsPayload","heapPayload","roomHeapSlice","chatCipher","roomsCipher","heapCipher","Promise","all","JSON","stringify","multiSet","e","console","warn","readPersistedState","chatRaw","roomsRaw","heapRaw","multiGet","chatPlain","roomsPlain","heapPlain","resolve","chat","parse","heap","clearPersistedState","multiRemove","PERSIST_KEYS"],"sourceRoot":"../../../src","sources":["roomStore/persistence.ts"],"mappings":"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","ignoreList":[]}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
4
4
  import { insertMessageWithDelimiter } from '../helpers/insertMessageWithDelimiter';
5
+ import { msgSortableMs } from '../helpers/msgSortableMs';
5
6
  // Per-room runtime message cap. Mirrors the persistence layer's
6
7
  // MESSAGE_LIMIT so what's in memory matches what's on disk; otherwise
7
8
  // long sessions grow the array unbounded (memory leak) and reload
@@ -127,7 +128,6 @@ function mergeHistoryIntoCache(existing, fetched) {
127
128
  // fetched MAM page (the live-stream swallow doesn't cover history).
128
129
  const ex = stripCallSignals(existing);
129
130
  const fe = stripCallSignals(fetched);
130
- const pending = ex.filter(m => m?.pending);
131
131
  const realExisting = ex.filter(m => m && !m.pending && m.id !== 'delimiter-new');
132
132
  const realFetched = fe.filter(m => m && m.id !== 'delimiter-new');
133
133
 
@@ -135,6 +135,28 @@ function mergeHistoryIntoCache(existing, fetched) {
135
135
  if (realFetched.length === 0) {
136
136
  return ex;
137
137
  }
138
+
139
+ // Optimistic sends are preserved across a history merge - EXCEPT the
140
+ // ones the fetched page proves the server already has. That happens
141
+ // whenever a send reached the server but its echo did not reach us
142
+ // before the process died: the optimistic bubble is restored from disk
143
+ // still `pending: true` (and, since bug #39, flagged failed at boot),
144
+ // while MAM hands back the very same message under its ARCHIVE id.
145
+ // Keying only on `id` never matched those two, so the room ended up
146
+ // showing the message twice - once for real and once as a bubble stuck
147
+ // on "sending…"/"Failed" that nothing could ever clear. The archived
148
+ // copy carries our original stanza id as `xmppId` (getDataFromXml), so
149
+ // match on that too and drop the local ghost.
150
+ const fetchedKeys = new Set();
151
+ for (const m of realFetched) {
152
+ if (m?.id != null) {
153
+ fetchedKeys.add(String(m.id));
154
+ }
155
+ if (m?.xmppId) {
156
+ fetchedKeys.add(String(m.xmppId));
157
+ }
158
+ }
159
+ const pending = ex.filter(m => m?.pending && !fetchedKeys.has(String(m.id)) && !(m.xmppId && fetchedKeys.has(String(m.xmppId))));
138
160
  const byMs = (a, b) => msgSortableMs(a) - msgSortableMs(b);
139
161
  const capTail = arr => arr.length > RUNTIME_MESSAGE_LIMIT ? arr.slice(-RUNTIME_MESSAGE_LIMIT) : arr;
140
162
  const existingIds = new Set(realExisting.map(m => String(m.id)));
@@ -178,7 +200,8 @@ const initialState = {
178
200
  text: ''
179
201
  },
180
202
  pendingNotificationJid: null,
181
- privateStoreMarkers: {}
203
+ privateStoreMarkers: {},
204
+ readBoundaries: {}
182
205
  };
183
206
  const isValidRoomJid = jid => {
184
207
  if (typeof jid !== 'string' || !jid) return false;
@@ -423,7 +446,7 @@ const reducers = {
423
446
  roomMessages.unshift(message);
424
447
  } else {
425
448
  const lastViewedValue = state.rooms[roomJID].lastViewedTimestamp;
426
- const lastViewedTimestamp = state.visibleRoomJID === roomJID ? null : lastViewedValue ? new Date(lastViewedValue) : null;
449
+ const lastViewedTimestamp = state.visibleRoomJID === roomJID ? null : lastViewedValue ? lastViewedValue : null;
427
450
  insertMessageWithDelimiter(roomMessages, message, lastViewedTimestamp);
428
451
  }
429
452
 
@@ -439,6 +462,7 @@ const reducers = {
439
462
  state.rooms = {};
440
463
  state.visibleRoomJID = null;
441
464
  state.privateStoreMarkers = {};
465
+ state.readBoundaries = {};
442
466
  state.isUnreadSyncing = false;
443
467
  },
444
468
  setComposing(state, action) {
@@ -497,7 +521,19 @@ const reducers = {
497
521
  *
498
522
  * Monotonic: a marker only ever moves a baseline FORWARD, so a stale
499
523
  * server value can't resurrect already-read messages and a more-recent
500
- * local read (tab blur stamping Date.now()) always wins.
524
+ * local read (tab blur stamping the server read timestamp) always wins.
525
+ *
526
+ * Bug #38 self-heal: forward-only is correct for a SANE existing
527
+ * value, but a device whose clock was ahead may have already stamped
528
+ * a marker in the future (either persisted locally from a previous
529
+ * session, or fetched from the server before this fix existed). To
530
+ * "only ever move forward" from a corrupt future value is to never
531
+ * move at all - the room would stay stuck at `unreadMessages: 0`
532
+ * forever. When the room is loaded, detect a marker that sits well
533
+ * past the newest message we actually know it has and waive the
534
+ * forward-only check for that one comparison, so the incoming
535
+ * (already-clamped, see xmppClient.getChatsPrivateStoreRequestStanza)
536
+ * value can correct it even though it's numerically smaller.
501
537
  */
502
538
  applyPrivateStoreMarkers: (state, action) => {
503
539
  const markers = action.payload;
@@ -512,20 +548,73 @@ const reducers = {
512
548
  if (!jid || !Number.isFinite(ts) || ts <= 0) {
513
549
  continue;
514
550
  }
551
+ const room = state.rooms[jid];
552
+ const newestKnownMs = room ? newestAckedMessageMs(room.messages) : 0;
553
+ // Mirrors isCorruptFutureReadMarker in helpers/getServerReadTimestamp.ts
554
+ // (duplicated to avoid a roomStore <-> helpers import cycle):
555
+ // BOTH signals are required. "Further ahead than the newest
556
+ // message this room has locally" happens all the time on a room
557
+ // whose history hasn't synced yet, and healing on that alone
558
+ // would drop a legitimate marker another device wrote after
559
+ // reading newer messages. A real read marker can never be ahead
560
+ // of real time, so that second signal is what distinguishes a
561
+ // stale cache from a wrong clock.
562
+ const nowMs = Date.now();
563
+ const looksCorrupt = existing => newestKnownMs > 0 && existing > newestKnownMs + FUTURE_MARKER_TOLERANCE_MS && existing > nowMs + FUTURE_MARKER_TOLERANCE_MS;
564
+
515
565
  // Remember the marker so rooms that load LATER (via addRoom)
516
566
  // inherit it even though they don't exist in the store yet.
517
- if (ts > (state.privateStoreMarkers[jid] || 0)) {
567
+ const cachedMarker = state.privateStoreMarkers[jid] || 0;
568
+ if (ts > cachedMarker || looksCorrupt(cachedMarker)) {
518
569
  state.privateStoreMarkers[jid] = ts;
519
570
  }
520
571
  // Upgrade an already-loaded room's baseline + recompute its badge.
521
- // Only ever forward, so a later local read isn't clobbered.
522
- const room = state.rooms[jid];
523
- if (room && ts > (room.lastViewedTimestamp || 0)) {
572
+ // Forward-only, unless the room's CURRENT baseline is itself the
573
+ // corrupt value being corrected.
574
+ if (room && (ts > (room.lastViewedTimestamp || 0) || looksCorrupt(room.lastViewedTimestamp || 0))) {
524
575
  room.lastViewedTimestamp = ts;
525
576
  room.unreadMessages = countNewerMessages(room.messages, ts);
526
577
  }
527
578
  }
528
579
  },
580
+ /**
581
+ * Set (or clear) the "read up to here, but not further" boundary for
582
+ * a room - see `readBoundaries` on `RoomMessagesState` for the full
583
+ * contract. `ts` is the msgSortableMs of the newest message the user
584
+ * actually reached before scrolling away from the bottom; `null`/`0`
585
+ * clears it (the user is at the bottom, or nothing was reached yet).
586
+ */
587
+ setReadBoundary: (state, action) => {
588
+ const {
589
+ jid,
590
+ ts
591
+ } = action.payload;
592
+ if (!jid) {
593
+ return;
594
+ }
595
+ if (!state.readBoundaries) {
596
+ state.readBoundaries = {};
597
+ }
598
+ if (typeof ts === 'number' && Number.isFinite(ts) && ts > 0) {
599
+ state.readBoundaries[jid] = ts;
600
+ } else {
601
+ delete state.readBoundaries[jid];
602
+ }
603
+ },
604
+ /**
605
+ * Release a room's read boundary once it has been consumed by a
606
+ * genuine "leave" (ChatRoom unmount, or a tab-navigator focus hook
607
+ * switching to a different room) - see `readBoundaries` for why the
608
+ * background/`isVisible=false` paths deliberately do NOT call this.
609
+ */
610
+ clearReadBoundary: (state, action) => {
611
+ const {
612
+ jid
613
+ } = action.payload;
614
+ if (jid && state.readBoundaries) {
615
+ delete state.readBoundaries[jid];
616
+ }
617
+ },
529
618
  setRoomRole: (state, action) => {
530
619
  const {
531
620
  chatJID,
@@ -605,6 +694,7 @@ const reducers = {
605
694
  state.isLoading = false;
606
695
  state.isUnreadSyncing = false;
607
696
  state.privateStoreMarkers = {};
697
+ state.readBoundaries = {};
608
698
  },
609
699
  setActiveMessage: (state, action) => {
610
700
  const {
@@ -707,31 +797,13 @@ export const roomsStore = createSlice({
707
797
  reducers
708
798
  });
709
799
 
710
- // Count messages strictly newer than the given millisecond timestamp.
711
- // Uses `msg.id` (server-authoritative microsecond timestamp prefixed by
712
- // 13-digit millis — see helpers/dateComparison `getHighResolutionTimestamp`)
713
- // because `msg.date` can be derived client-side (createMessageFromXml
714
- // falls back to `Date.now()` for realtime stanzas without a `date`
715
- // attr), which makes the comparison drift vs what the server assigned.
716
- // Excludes the "delimiter-new" sentinel, pending sends, and the current
717
- // user's own messages (parity with unreadMiddleware's isOwnMessage
718
- // filter — without this, the reducer and the middleware disagree about
719
- // the count and we get a flicker as the badge gets written twice with
720
- // different values on every message).
721
- export const msgSortableMs = msg => {
722
- const id = String(msg?.id || '');
723
- const m = /^(\d{13})/.exec(id);
724
- if (m) {
725
- return Number(m[1]);
726
- }
727
- if (msg?.date) {
728
- const t = new Date(msg.date).getTime();
729
- if (Number.isFinite(t)) {
730
- return t;
731
- }
732
- }
733
- return 0;
734
- };
800
+ // Re-exported for backward compatibility - every existing call site
801
+ // imports `msgSortableMs` from here. The implementation now lives in
802
+ // helpers/msgSortableMs.ts (imported above), so insertMessageWithDelimiter.ts
803
+ // (which this file itself imports) can use the SAME ordering source
804
+ // without a roomStore <-> helpers import cycle. See that file for the
805
+ // full rationale (server id vs. client `date`).
806
+ export { msgSortableMs };
735
807
  const norm = s => {
736
808
  if (s == null) {
737
809
  return '';
@@ -757,6 +829,35 @@ const isOwn = (msg, selfXmpp, selfWallet) => {
757
829
  }
758
830
  return false;
759
831
  };
832
+
833
+ // Bug #38 tolerance: a stored read marker more than this far past the
834
+ // newest message a room actually has is not "the user is a little
835
+ // ahead" - it's a leftover from a device whose clock was wrong when it
836
+ // stamped the marker. Kept in sync with (but duplicated from, to avoid
837
+ // a roomStore ↔ helpers import cycle) FUTURE_READ_MARKER_TOLERANCE_MS
838
+ // in helpers/getServerReadTimestamp.ts.
839
+ const FUTURE_MARKER_TOLERANCE_MS = 5 * 60 * 1000;
840
+
841
+ // Newest message in `messages` this room actually has proof the server
842
+ // accepted - used only to sanity-check an already-stored read marker
843
+ // against reality (see applyPrivateStoreMarkers's self-heal). Excludes
844
+ // pending/optimistic sends via the `pending` flag alone: unlike
845
+ // getServerReadTimestamp, this runs inside a reducer and can't reach
846
+ // across to the roomHeapSlice slice, but `pending` already identifies
847
+ // every optimistic send that matters for this comparison.
848
+ const newestAckedMessageMs = messages => {
849
+ let newest = 0;
850
+ for (const m of messages || []) {
851
+ if (!m || m.id === 'delimiter-new' || m.pending) {
852
+ continue;
853
+ }
854
+ const ms = msgSortableMs(m);
855
+ if (ms > newest) {
856
+ newest = ms;
857
+ }
858
+ }
859
+ return newest;
860
+ };
760
861
  const countNewerMessages = (messages, timestamp, selfXmpp = '', selfWallet = '') => {
761
862
  if (!messages?.length || !timestamp) {
762
863
  return 0;
@@ -789,6 +890,8 @@ export const {
789
890
  setUnreadSyncing,
790
891
  setLastViewedTimestamp,
791
892
  applyPrivateStoreMarkers,
893
+ setReadBoundary,
894
+ clearReadBoundary,
792
895
  setRoomNoMessages,
793
896
  setCurrentRoom,
794
897
  setVisibleRoom,