@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,6 +2,7 @@ import { createAsyncThunk, createSlice, PayloadAction, type Slice } from '@redux
2
2
  import type { WritableDraft } from 'immer';
3
3
  import { EditAction, HistoryPreloadState, IMessage, IRoom } from '../types/types';
4
4
  import { insertMessageWithDelimiter } from '../helpers/insertMessageWithDelimiter';
5
+ import { msgSortableMs } from '../helpers/msgSortableMs';
5
6
  import type XmppClient from '../networking/xmppClient';
6
7
 
7
8
  // Per-room runtime message cap. Mirrors the persistence layer's
@@ -142,7 +143,6 @@ function mergeHistoryIntoCache(
142
143
  const ex = stripCallSignals(existing);
143
144
  const fe = stripCallSignals(fetched);
144
145
 
145
- const pending = ex.filter((m) => m?.pending);
146
146
  const realExisting = ex.filter(
147
147
  (m) => m && !m.pending && m.id !== 'delimiter-new'
148
148
  );
@@ -151,6 +151,29 @@ function mergeHistoryIntoCache(
151
151
  // Nothing usable came back → keep the cache exactly as it was.
152
152
  if (realFetched.length === 0) {return ex;}
153
153
 
154
+ // Optimistic sends are preserved across a history merge - EXCEPT the
155
+ // ones the fetched page proves the server already has. That happens
156
+ // whenever a send reached the server but its echo did not reach us
157
+ // before the process died: the optimistic bubble is restored from disk
158
+ // still `pending: true` (and, since bug #39, flagged failed at boot),
159
+ // while MAM hands back the very same message under its ARCHIVE id.
160
+ // Keying only on `id` never matched those two, so the room ended up
161
+ // showing the message twice - once for real and once as a bubble stuck
162
+ // on "sending…"/"Failed" that nothing could ever clear. The archived
163
+ // copy carries our original stanza id as `xmppId` (getDataFromXml), so
164
+ // match on that too and drop the local ghost.
165
+ const fetchedKeys = new Set<string>();
166
+ for (const m of realFetched) {
167
+ if (m?.id != null) {fetchedKeys.add(String(m.id));}
168
+ if ((m as any)?.xmppId) {fetchedKeys.add(String((m as any).xmppId));}
169
+ }
170
+ const pending = ex.filter(
171
+ (m) =>
172
+ m?.pending &&
173
+ !fetchedKeys.has(String(m.id)) &&
174
+ !(m.xmppId && fetchedKeys.has(String(m.xmppId)))
175
+ );
176
+
154
177
  const byMs = (a: IMessage, b: IMessage) =>
155
178
  msgSortableMs(a) - msgSortableMs(b);
156
179
  const capTail = (arr: IMessage[]) =>
@@ -214,6 +237,33 @@ export interface RoomMessagesState {
214
237
  // — see `applyPrivateStoreMarkers`. Re-fetched every init/reconnect,
215
238
  // so it is intentionally NOT persisted.
216
239
  privateStoreMarkers: Record<string, number>;
240
+ // The single source of truth for "read up to here, but the user
241
+ // hasn't reached the bottom yet" (`{ roomJID: boundaryMs }`).
242
+ // Set by MessageList (via ChatRoom's `onReadBoundaryChange`) the
243
+ // moment the user first scrolls away from the bottom, to the
244
+ // msgSortableMs of the newest message they'd actually seen. Every
245
+ // path that stamps a read marker for a room the user is leaving
246
+ // (ChatRoom unmount, xmppProvider's AppState background handler and
247
+ // `isVisible=false` handler, the live `advance()` effect, and
248
+ // `useChatRoomFocus`'s `leaveRoom`) must consult this instead of
249
+ // unconditionally stamping "the newest acked message" - otherwise
250
+ // leaving a room while scrolled up marks messages the user never
251
+ // reached as read (customer #42, a regression of #33 reintroduced by
252
+ // #38's server-timestamp marker). See `getReadMarkerTimestamp` in
253
+ // helpers/getServerReadTimestamp.ts.
254
+ //
255
+ // Cleared when the boundary no longer applies: the user scrolls back
256
+ // to the bottom, the visible room changes, or the room is truly left
257
+ // (ChatRoom unmounts, or a tab-navigator focus hook releases the
258
+ // room). NOT cleared merely because the app backgrounds or a host's
259
+ // `isVisible` flips false - MessageList typically stays mounted
260
+ // through those, so its own scroll-tracking ref (the only thing that
261
+ // could re-derive this value) is still intact and the boundary must
262
+ // stay in sync with it. Deliberately NOT persisted (this key isn't
263
+ // read by `persistence.ts`, which only ever picks `rooms` back out of
264
+ // this slice) - it's meaningless across a process restart, where
265
+ // MessageList always mounts fresh at the bottom.
266
+ readBoundaries: Record<string, number>;
217
267
  }
218
268
 
219
269
  const initialState: RoomMessagesState = {
@@ -230,6 +280,7 @@ const initialState: RoomMessagesState = {
230
280
  },
231
281
  pendingNotificationJid: null,
232
282
  privateStoreMarkers: {},
283
+ readBoundaries: {},
233
284
  };
234
285
 
235
286
  const isValidRoomJid = (jid: unknown): jid is string => {
@@ -496,7 +547,7 @@ const reducers = {
496
547
  state.visibleRoomJID === roomJID
497
548
  ? null
498
549
  : lastViewedValue
499
- ? new Date(lastViewedValue)
550
+ ? lastViewedValue
500
551
  : null;
501
552
 
502
553
  insertMessageWithDelimiter(roomMessages, message, lastViewedTimestamp);
@@ -514,6 +565,7 @@ const reducers = {
514
565
  state.rooms = {};
515
566
  state.visibleRoomJID = null;
516
567
  state.privateStoreMarkers = {};
568
+ state.readBoundaries = {};
517
569
  state.isUnreadSyncing = false;
518
570
  },
519
571
  setComposing(
@@ -580,7 +632,19 @@ const reducers = {
580
632
  *
581
633
  * Monotonic: a marker only ever moves a baseline FORWARD, so a stale
582
634
  * server value can't resurrect already-read messages and a more-recent
583
- * local read (tab blur stamping Date.now()) always wins.
635
+ * local read (tab blur stamping the server read timestamp) always wins.
636
+ *
637
+ * Bug #38 self-heal: forward-only is correct for a SANE existing
638
+ * value, but a device whose clock was ahead may have already stamped
639
+ * a marker in the future (either persisted locally from a previous
640
+ * session, or fetched from the server before this fix existed). To
641
+ * "only ever move forward" from a corrupt future value is to never
642
+ * move at all - the room would stay stuck at `unreadMessages: 0`
643
+ * forever. When the room is loaded, detect a marker that sits well
644
+ * past the newest message we actually know it has and waive the
645
+ * forward-only check for that one comparison, so the incoming
646
+ * (already-clamped, see xmppClient.getChatsPrivateStoreRequestStanza)
647
+ * value can correct it even though it's numerically smaller.
584
648
  */
585
649
  applyPrivateStoreMarkers: (
586
650
  state: WritableDraft<RoomMessagesState>,
@@ -592,20 +656,72 @@ const reducers = {
592
656
  for (const jid of Object.keys(markers)) {
593
657
  const ts = Number(markers[jid]);
594
658
  if (!jid || !Number.isFinite(ts) || ts <= 0) {continue;}
659
+
660
+ const room = state.rooms[jid];
661
+ const newestKnownMs = room ? newestAckedMessageMs(room.messages) : 0;
662
+ // Mirrors isCorruptFutureReadMarker in helpers/getServerReadTimestamp.ts
663
+ // (duplicated to avoid a roomStore <-> helpers import cycle):
664
+ // BOTH signals are required. "Further ahead than the newest
665
+ // message this room has locally" happens all the time on a room
666
+ // whose history hasn't synced yet, and healing on that alone
667
+ // would drop a legitimate marker another device wrote after
668
+ // reading newer messages. A real read marker can never be ahead
669
+ // of real time, so that second signal is what distinguishes a
670
+ // stale cache from a wrong clock.
671
+ const nowMs = Date.now();
672
+ const looksCorrupt = (existing: number) =>
673
+ newestKnownMs > 0 &&
674
+ existing > newestKnownMs + FUTURE_MARKER_TOLERANCE_MS &&
675
+ existing > nowMs + FUTURE_MARKER_TOLERANCE_MS;
676
+
595
677
  // Remember the marker so rooms that load LATER (via addRoom)
596
678
  // inherit it even though they don't exist in the store yet.
597
- if (ts > (state.privateStoreMarkers[jid] || 0)) {
679
+ const cachedMarker = state.privateStoreMarkers[jid] || 0;
680
+ if (ts > cachedMarker || looksCorrupt(cachedMarker)) {
598
681
  state.privateStoreMarkers[jid] = ts;
599
682
  }
600
683
  // Upgrade an already-loaded room's baseline + recompute its badge.
601
- // Only ever forward, so a later local read isn't clobbered.
602
- const room = state.rooms[jid];
603
- if (room && ts > (room.lastViewedTimestamp || 0)) {
684
+ // Forward-only, unless the room's CURRENT baseline is itself the
685
+ // corrupt value being corrected.
686
+ if (room && (ts > (room.lastViewedTimestamp || 0) || looksCorrupt(room.lastViewedTimestamp || 0))) {
604
687
  room.lastViewedTimestamp = ts;
605
688
  room.unreadMessages = countNewerMessages(room.messages, ts);
606
689
  }
607
690
  }
608
691
  },
692
+ /**
693
+ * Set (or clear) the "read up to here, but not further" boundary for
694
+ * a room - see `readBoundaries` on `RoomMessagesState` for the full
695
+ * contract. `ts` is the msgSortableMs of the newest message the user
696
+ * actually reached before scrolling away from the bottom; `null`/`0`
697
+ * clears it (the user is at the bottom, or nothing was reached yet).
698
+ */
699
+ setReadBoundary: (
700
+ state: WritableDraft<RoomMessagesState>,
701
+ action: PayloadAction<{ jid: string; ts: number | null }>
702
+ ) => {
703
+ const { jid, ts } = action.payload;
704
+ if (!jid) {return;}
705
+ if (!state.readBoundaries) {state.readBoundaries = {};}
706
+ if (typeof ts === 'number' && Number.isFinite(ts) && ts > 0) {
707
+ state.readBoundaries[jid] = ts;
708
+ } else {
709
+ delete state.readBoundaries[jid];
710
+ }
711
+ },
712
+ /**
713
+ * Release a room's read boundary once it has been consumed by a
714
+ * genuine "leave" (ChatRoom unmount, or a tab-navigator focus hook
715
+ * switching to a different room) - see `readBoundaries` for why the
716
+ * background/`isVisible=false` paths deliberately do NOT call this.
717
+ */
718
+ clearReadBoundary: (
719
+ state: WritableDraft<RoomMessagesState>,
720
+ action: PayloadAction<{ jid: string }>
721
+ ) => {
722
+ const { jid } = action.payload;
723
+ if (jid && state.readBoundaries) {delete state.readBoundaries[jid];}
724
+ },
609
725
  setRoomRole: (
610
726
  state: WritableDraft<RoomMessagesState>,
611
727
  action: PayloadAction<{ chatJID: string; role: string }>
@@ -699,6 +815,7 @@ const reducers = {
699
815
  state.isLoading = false;
700
816
  state.isUnreadSyncing = false;
701
817
  state.privateStoreMarkers = {};
818
+ state.readBoundaries = {};
702
819
  },
703
820
  setActiveMessage: (
704
821
  state: WritableDraft<RoomMessagesState>,
@@ -820,27 +937,13 @@ export const roomsStore: Slice<RoomMessagesState, typeof reducers, 'roomMessages
820
937
  reducers,
821
938
  });
822
939
 
823
- // Count messages strictly newer than the given millisecond timestamp.
824
- // Uses `msg.id` (server-authoritative microsecond timestamp prefixed by
825
- // 13-digit millis — see helpers/dateComparison `getHighResolutionTimestamp`)
826
- // because `msg.date` can be derived client-side (createMessageFromXml
827
- // falls back to `Date.now()` for realtime stanzas without a `date`
828
- // attr), which makes the comparison drift vs what the server assigned.
829
- // Excludes the "delimiter-new" sentinel, pending sends, and the current
830
- // user's own messages (parity with unreadMiddleware's isOwnMessage
831
- // filter — without this, the reducer and the middleware disagree about
832
- // the count and we get a flicker as the badge gets written twice with
833
- // different values on every message).
834
- export const msgSortableMs = (msg: any): number => {
835
- const id = String(msg?.id || '');
836
- const m = /^(\d{13})/.exec(id);
837
- if (m) {return Number(m[1]);}
838
- if (msg?.date) {
839
- const t = new Date(msg.date as any).getTime();
840
- if (Number.isFinite(t)) {return t;}
841
- }
842
- return 0;
843
- };
940
+ // Re-exported for backward compatibility - every existing call site
941
+ // imports `msgSortableMs` from here. The implementation now lives in
942
+ // helpers/msgSortableMs.ts (imported above), so insertMessageWithDelimiter.ts
943
+ // (which this file itself imports) can use the SAME ordering source
944
+ // without a roomStore <-> helpers import cycle. See that file for the
945
+ // full rationale (server id vs. client `date`).
946
+ export { msgSortableMs };
844
947
 
845
948
  const norm = (s: any): string => {
846
949
  if (s == null) {return '';}
@@ -872,6 +975,31 @@ const isOwn = (
872
975
  return false;
873
976
  };
874
977
 
978
+ // Bug #38 tolerance: a stored read marker more than this far past the
979
+ // newest message a room actually has is not "the user is a little
980
+ // ahead" - it's a leftover from a device whose clock was wrong when it
981
+ // stamped the marker. Kept in sync with (but duplicated from, to avoid
982
+ // a roomStore ↔ helpers import cycle) FUTURE_READ_MARKER_TOLERANCE_MS
983
+ // in helpers/getServerReadTimestamp.ts.
984
+ const FUTURE_MARKER_TOLERANCE_MS = 5 * 60 * 1000;
985
+
986
+ // Newest message in `messages` this room actually has proof the server
987
+ // accepted - used only to sanity-check an already-stored read marker
988
+ // against reality (see applyPrivateStoreMarkers's self-heal). Excludes
989
+ // pending/optimistic sends via the `pending` flag alone: unlike
990
+ // getServerReadTimestamp, this runs inside a reducer and can't reach
991
+ // across to the roomHeapSlice slice, but `pending` already identifies
992
+ // every optimistic send that matters for this comparison.
993
+ const newestAckedMessageMs = (messages: IMessage[] | undefined): number => {
994
+ let newest = 0;
995
+ for (const m of messages || []) {
996
+ if (!m || m.id === 'delimiter-new' || m.pending) {continue;}
997
+ const ms = msgSortableMs(m);
998
+ if (ms > newest) {newest = ms;}
999
+ }
1000
+ return newest;
1001
+ };
1002
+
875
1003
  const countNewerMessages = (
876
1004
  messages: IMessage[],
877
1005
  timestamp: number,
@@ -902,6 +1030,8 @@ export const {
902
1030
  setUnreadSyncing,
903
1031
  setLastViewedTimestamp,
904
1032
  applyPrivateStoreMarkers,
1033
+ setReadBoundary,
1034
+ clearReadBoundary,
905
1035
  setRoomNoMessages,
906
1036
  setCurrentRoom,
907
1037
  setVisibleRoom,
@@ -79,7 +79,6 @@ export interface ApiRoom {
79
79
  jid?: string;
80
80
  participants?: number;
81
81
  icon?: string;
82
- muted?: boolean;
83
82
  }
84
83
 
85
84
  export interface PostRoom {
@@ -34,7 +34,6 @@ export interface IRoom {
34
34
  lastViewedTimestamp?: number;
35
35
  unreadMessages?: number;
36
36
  noMessages?: boolean;
37
- muted?: boolean;
38
37
  role?: string;
39
38
  createdAt?: string | number | Date;
40
39
  updatedAt?: string | number | Date;
@@ -586,6 +585,20 @@ export interface IConfig {
586
585
  // `disableKeyboardAvoidingView` is set. Needs on-device tuning of any
587
586
  // `keyboardVerticalOffset`.
588
587
  keyboardStickyInput?: boolean;
588
+ // Bottom padding applied to the input dock (the composer's outer
589
+ // wrapper), on both iOS and Android. When a number is provided (0 is
590
+ // allowed) it is used verbatim on both platforms, overriding the
591
+ // built-in default below.
592
+ //
593
+ // Left unset, the default is:
594
+ // - 0 when `disableKeyboardAvoidingView` is set, since the host owns
595
+ // layout entirely and typically already places the chat above its
596
+ // own chrome (e.g. a tab bar) - the built-in padding would just add
597
+ // an extra gap between the composer and that chrome.
598
+ // - otherwise unchanged: the iOS safe-area bottom inset, or a fixed
599
+ // 12dp gap on Android (`ANDROID_INPUT_DOCK_GAP`).
600
+ // Customer-reported #44.
601
+ inputDockPaddingBottom?: number;
589
602
  // The message list's FlatList uses keyboardDismissMode="interactive" by
590
603
  // default, so a drag can be used to pull the keyboard closed. Because
591
604
  // the list is `inverted`, an ordinary scroll gesture through history can