@ethora/chat-component-rn 26.7.8 → 26.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/lib/commonjs/assets/icons.js +2 -2
  3. package/lib/commonjs/assets/icons.js.map +1 -1
  4. package/lib/commonjs/components/MainComponents/ChatRoom.js +17 -8
  5. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  6. package/lib/commonjs/components/MainComponents/MediaMessage.js +8 -5
  7. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  8. package/lib/commonjs/components/MainComponents/MessageContainer.js +5 -1
  9. package/lib/commonjs/components/MainComponents/MessageContainer.js.map +1 -1
  10. package/lib/commonjs/components/MainComponents/MessageList.js +2 -1
  11. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  12. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +1 -51
  13. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  14. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +51 -12
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  16. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +12 -1
  17. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  18. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
  19. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  20. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +13 -2
  21. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  22. package/lib/commonjs/components/styled/MessageImage.js +3 -1
  23. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  24. package/lib/commonjs/components/styled/UnsupportedType.js +14 -4
  25. package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
  26. package/lib/commonjs/components/styled/VideoMessage.js +3 -1
  27. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  28. package/lib/commonjs/context/xmppProvider.js +46 -31
  29. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  30. package/lib/commonjs/helpers/createMessageFromXml.js +19 -1
  31. package/lib/commonjs/helpers/createMessageFromXml.js.map +1 -1
  32. package/lib/commonjs/helpers/getDisplayFileName.js +79 -0
  33. package/lib/commonjs/helpers/getDisplayFileName.js.map +1 -0
  34. package/lib/commonjs/helpers/getServerReadTimestamp.js +125 -0
  35. package/lib/commonjs/helpers/getServerReadTimestamp.js.map +1 -0
  36. package/lib/commonjs/helpers/initXmppRooms.js +9 -7
  37. package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
  38. package/lib/commonjs/helpers/isOwnMessage.js +75 -0
  39. package/lib/commonjs/helpers/isOwnMessage.js.map +1 -0
  40. package/lib/commonjs/helpers/mimeToExtension.js +6 -14
  41. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
  42. package/lib/commonjs/hooks/useChatRoomFocus.js +17 -8
  43. package/lib/commonjs/hooks/useChatRoomFocus.js.map +1 -1
  44. package/lib/commonjs/hooks/useChatWrapperInit.js +12 -7
  45. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  46. package/lib/commonjs/i18n/strings.js +0 -18
  47. package/lib/commonjs/i18n/strings.js.map +1 -1
  48. package/lib/commonjs/networking/api-requests/rooms.api.js +0 -13
  49. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  50. package/lib/commonjs/networking/stanzaHandlers.js +20 -2
  51. package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
  52. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
  53. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  54. package/lib/commonjs/networking/xmppClient.js +36 -2
  55. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  56. package/lib/commonjs/roomStore/index.js +35 -2
  57. package/lib/commonjs/roomStore/index.js.map +1 -1
  58. package/lib/commonjs/roomStore/persistence.js +200 -11
  59. package/lib/commonjs/roomStore/persistence.js.map +1 -1
  60. package/lib/commonjs/roomStore/roomsSlice.js +83 -6
  61. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  62. package/lib/module/assets/icons.js +1 -1
  63. package/lib/module/assets/icons.js.map +1 -1
  64. package/lib/module/components/MainComponents/ChatRoom.js +17 -8
  65. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  66. package/lib/module/components/MainComponents/MediaMessage.js +9 -6
  67. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  68. package/lib/module/components/MainComponents/MessageContainer.js +5 -1
  69. package/lib/module/components/MainComponents/MessageContainer.js.map +1 -1
  70. package/lib/module/components/MainComponents/MessageList.js +2 -1
  71. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  72. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +3 -53
  73. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  74. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +52 -13
  75. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  76. package/lib/module/components/RoomComponents/styled/LastMessageFile.js +12 -1
  77. package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  78. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
  79. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  80. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +13 -2
  81. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  82. package/lib/module/components/styled/MessageImage.js +3 -1
  83. package/lib/module/components/styled/MessageImage.js.map +1 -1
  84. package/lib/module/components/styled/UnsupportedType.js +14 -4
  85. package/lib/module/components/styled/UnsupportedType.js.map +1 -1
  86. package/lib/module/components/styled/VideoMessage.js +3 -1
  87. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  88. package/lib/module/context/xmppProvider.js +47 -32
  89. package/lib/module/context/xmppProvider.js.map +1 -1
  90. package/lib/module/helpers/createMessageFromXml.js +19 -1
  91. package/lib/module/helpers/createMessageFromXml.js.map +1 -1
  92. package/lib/module/helpers/getDisplayFileName.js +73 -0
  93. package/lib/module/helpers/getDisplayFileName.js.map +1 -0
  94. package/lib/module/helpers/getServerReadTimestamp.js +120 -0
  95. package/lib/module/helpers/getServerReadTimestamp.js.map +1 -0
  96. package/lib/module/helpers/initXmppRooms.js +9 -7
  97. package/lib/module/helpers/initXmppRooms.js.map +1 -1
  98. package/lib/module/helpers/isOwnMessage.js +70 -0
  99. package/lib/module/helpers/isOwnMessage.js.map +1 -0
  100. package/lib/module/helpers/mimeToExtension.js +6 -13
  101. package/lib/module/helpers/mimeToExtension.js.map +1 -1
  102. package/lib/module/hooks/useChatRoomFocus.js +17 -8
  103. package/lib/module/hooks/useChatRoomFocus.js.map +1 -1
  104. package/lib/module/hooks/useChatWrapperInit.js +12 -7
  105. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  106. package/lib/module/i18n/strings.js +0 -18
  107. package/lib/module/i18n/strings.js.map +1 -1
  108. package/lib/module/networking/api-requests/rooms.api.js +0 -12
  109. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  110. package/lib/module/networking/stanzaHandlers.js +20 -2
  111. package/lib/module/networking/stanzaHandlers.js.map +1 -1
  112. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
  113. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  114. package/lib/module/networking/xmppClient.js +36 -2
  115. package/lib/module/networking/xmppClient.js.map +1 -1
  116. package/lib/module/roomStore/index.js +37 -4
  117. package/lib/module/roomStore/index.js.map +1 -1
  118. package/lib/module/roomStore/persistence.js +197 -11
  119. package/lib/module/roomStore/persistence.js.map +1 -1
  120. package/lib/module/roomStore/roomsSlice.js +83 -6
  121. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  122. package/lib/typescript/assets/icons.d.ts +0 -8
  123. package/lib/typescript/assets/icons.d.ts.map +1 -1
  124. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  125. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  126. package/lib/typescript/components/MainComponents/MessageContainer.d.ts.map +1 -1
  127. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  128. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  129. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  130. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts +1 -1
  131. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
  132. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts +1 -1
  133. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
  134. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts +1 -1
  135. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
  136. package/lib/typescript/components/styled/MessageImage.d.ts +6 -0
  137. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  138. package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
  139. package/lib/typescript/components/styled/VideoMessage.d.ts +6 -0
  140. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  141. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  142. package/lib/typescript/helpers/createMessageFromXml.d.ts.map +1 -1
  143. package/lib/typescript/helpers/getAvatarColor.d.ts +1 -1
  144. package/lib/typescript/helpers/getDisplayFileName.d.ts +49 -0
  145. package/lib/typescript/helpers/getDisplayFileName.d.ts.map +1 -0
  146. package/lib/typescript/helpers/getServerReadTimestamp.d.ts +69 -0
  147. package/lib/typescript/helpers/getServerReadTimestamp.d.ts.map +1 -0
  148. package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
  149. package/lib/typescript/helpers/isOwnMessage.d.ts +36 -0
  150. package/lib/typescript/helpers/isOwnMessage.d.ts.map +1 -0
  151. package/lib/typescript/helpers/mimeToExtension.d.ts +0 -15
  152. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
  153. package/lib/typescript/hooks/useChatRoomFocus.d.ts +4 -1
  154. package/lib/typescript/hooks/useChatRoomFocus.d.ts.map +1 -1
  155. package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
  156. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  157. package/lib/typescript/networking/api-requests/rooms.api.d.ts +0 -1
  158. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  159. package/lib/typescript/networking/stanzaHandlers.d.ts.map +1 -1
  160. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts +9 -2
  161. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts.map +1 -1
  162. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  163. package/lib/typescript/roomStore/index.d.ts.map +1 -1
  164. package/lib/typescript/roomStore/persistence.d.ts +18 -1
  165. package/lib/typescript/roomStore/persistence.d.ts.map +1 -1
  166. package/lib/typescript/roomStore/roomsSlice.d.ts +13 -1
  167. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  168. package/lib/typescript/types/models/room.model.d.ts +0 -1
  169. package/lib/typescript/types/models/room.model.d.ts.map +1 -1
  170. package/lib/typescript/types/types.d.ts +0 -1
  171. package/lib/typescript/types/types.d.ts.map +1 -1
  172. package/package.json +1 -1
  173. package/src/assets/icons.tsx +0 -24
  174. package/src/components/MainComponents/ChatRoom.tsx +21 -10
  175. package/src/components/MainComponents/MediaMessage.tsx +8 -5
  176. package/src/components/MainComponents/MessageContainer.tsx +5 -1
  177. package/src/components/MainComponents/MessageList.tsx +2 -1
  178. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +2 -36
  179. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +63 -26
  180. package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
  181. package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +14 -2
  182. package/src/components/RoomComponents/styled/LastMessageVideo.tsx +14 -2
  183. package/src/components/styled/MessageImage.tsx +8 -1
  184. package/src/components/styled/UnsupportedType.tsx +14 -4
  185. package/src/components/styled/VideoMessage.tsx +8 -1
  186. package/src/context/xmppProvider.tsx +47 -24
  187. package/src/helpers/createMessageFromXml.ts +20 -1
  188. package/src/helpers/getDisplayFileName.ts +87 -0
  189. package/src/helpers/getServerReadTimestamp.ts +120 -0
  190. package/src/helpers/initXmppRooms.tsx +9 -9
  191. package/src/helpers/isOwnMessage.ts +90 -0
  192. package/src/helpers/mimeToExtension.ts +6 -22
  193. package/src/hooks/useChatRoomFocus.ts +14 -5
  194. package/src/hooks/useChatWrapperInit.ts +12 -7
  195. package/src/i18n/strings.ts +0 -18
  196. package/src/networking/api-requests/rooms.api.ts +0 -9
  197. package/src/networking/stanzaHandlers.ts +20 -6
  198. package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +65 -18
  199. package/src/networking/xmppClient.ts +41 -2
  200. package/src/roomStore/index.ts +40 -4
  201. package/src/roomStore/persistence.ts +197 -7
  202. package/src/roomStore/roomsSlice.ts +84 -6
  203. package/src/types/models/room.model.ts +0 -1
  204. package/src/types/types.ts +0 -1
@@ -7,6 +7,7 @@ import {
7
7
  setVisibleRoom,
8
8
  } from '../roomStore/roomsSlice';
9
9
  import { store } from '../roomStore';
10
+ import { getServerReadTimestamp } from '../helpers/getServerReadTimestamp';
10
11
 
11
12
  interface UseChatRoomFocusOptions {
12
13
  /** The room JID that the consumer's tab/screen is currently showing. */
@@ -32,7 +33,10 @@ interface UseChatRoomFocusOptions {
32
33
  * Without this hook, `useUnread()` will always return 0 for the chat
33
34
  * room because the SDK assumes "mounted == active". With this hook,
34
35
  * focus marks the room visible (clearing the badge) and blur stamps
35
- * `lastViewedTimestamp = Date.now()` so future messages count as unread.
36
+ * `lastViewedTimestamp` to the newest SERVER-acknowledged message so
37
+ * future messages count as unread. Deliberately not `Date.now()`: a
38
+ * device clock running ahead would write a future marker that the
39
+ * forward-only private-store merge could never correct again (bug #38).
36
40
  *
37
41
  * Usage with React Navigation:
38
42
  *
@@ -59,11 +63,16 @@ export const useChatRoomFocus = ({
59
63
  });
60
64
 
61
65
  const leaveRoom = (jid: string) => {
62
- const timestamp = Date.now();
63
- dispatch(setLastViewedTimestamp({ chatJID: jid, timestamp }));
66
+ const state = store.getState();
67
+ const rooms = state.rooms?.rooms;
68
+ const timestamp = getServerReadTimestamp(rooms?.[jid], state.roomHeapSlice);
69
+ // Only stamp when we actually have something to anchor to - skip
70
+ // rather than fall back to the device clock (bug #38).
71
+ if (timestamp > 0) {
72
+ dispatch(setLastViewedTimestamp({ chatJID: jid, timestamp }));
73
+ }
64
74
  dispatch(clearVisibleRoom());
65
- const rooms = store.getState().rooms?.rooms;
66
- (store.getState().chatSettingStore as any)?.client
75
+ (state.chatSettingStore as any)?.client
67
76
  ?.flushLastViewedToPrivateStoreStanza(rooms, { visibleRoomJID: jid })
68
77
  .catch(() => {});
69
78
  };
@@ -8,7 +8,6 @@ import { AppDispatch, RootState } from '../roomStore';
8
8
  import { useXmppClient } from '../context/xmppProvider';
9
9
  import { chatAutoEnterer } from '../helpers/chatAutoEnterer';
10
10
  import { initRoomsPresence } from '../helpers/initRoomsPresence';
11
- import { updatedChatLastTimestamps } from '../helpers/updatedChatLastTimestamps';
12
11
  import { updateMessagesTillLast } from '../helpers/updateMessagesTillLast';
13
12
  import { refreshAuthTokensQuietly } from '../networking/authRefresh';
14
13
  import { setLangSource, setConfig } from '../roomStore/chatSettingsSlice';
@@ -170,15 +169,21 @@ const useChatWrapperInit = ({
170
169
  }
171
170
  }
172
171
  try {
172
+ // getChatsPrivateStoreRequestStanza hydrates redux itself
173
+ // (dispatch(applyPrivateStoreMarkers(...)) internally,
174
+ // clamped + forward-only) - do not also apply the raw
175
+ // return value here. That used to double-dispatch via
176
+ // updatedChatLastTimestamps' unconditional
177
+ // setLastViewedTimestamp, which has neither the
178
+ // forward-only guard nor the future-marker clamp, so on
179
+ // every reconnect it could silently re-clobber a value
180
+ // the clamp had just corrected (bug #38).
173
181
  const roomTimestampObject: any =
174
182
  await newClient.getChatsPrivateStoreRequestStanza();
175
183
  console.log(
176
184
  'Got chats private store',
177
185
  roomTimestampObject?.length || 0
178
186
  );
179
- if (roomTimestampObject) {
180
- updatedChatLastTimestamps(roomTimestampObject, dispatch);
181
- }
182
187
  if (!hasSyncedHistoryRef.current) {
183
188
  console.log('Updating messages till last');
184
189
  await updateMessagesTillLast(rooms, newClient);
@@ -220,15 +225,15 @@ const useChatWrapperInit = ({
220
225
  setInited(true);
221
226
  console.log(' Getting chats private store for existing client');
222
227
  try {
228
+ // See the "new client" branch above: getChatsPrivateStoreRequestStanza
229
+ // already hydrates redux (clamped + forward-only); do not
230
+ // also apply the raw return value here.
223
231
  const roomTimestampObject: any =
224
232
  await client.getChatsPrivateStoreRequestStanza();
225
233
  console.log(
226
234
  'Got chats private store for existing client',
227
235
  roomTimestampObject?.length || 0
228
236
  );
229
- if (roomTimestampObject) {
230
- updatedChatLastTimestamps(roomTimestampObject, dispatch);
231
- }
232
237
  if (!hasSyncedHistoryRef.current) {
233
238
  console.log(
234
239
  'Updating messages till last for existing client'
@@ -81,9 +81,6 @@ const en: UiStringTable = {
81
81
  'action.save': 'Save',
82
82
  'action.delete': 'Delete',
83
83
  'action.leave': 'Leave',
84
- 'action.muted': 'Muted',
85
- 'action.unmuted': 'Unmuted',
86
- 'toast.muteFailed': 'Could not update notifications for this chat',
87
84
  'action.create': 'Create',
88
85
  'action.submit': 'Submit',
89
86
  'action.translate': 'Translate',
@@ -312,9 +309,6 @@ const fr: UiStringTable = {
312
309
  'action.save': 'Enregistrer',
313
310
  'action.delete': 'Supprimer',
314
311
  'action.leave': 'Quitter',
315
- 'action.muted': 'En sourdine',
316
- 'action.unmuted': 'Activé',
317
- 'toast.muteFailed': 'Impossible de modifier les notifications de ce chat',
318
312
  'action.create': 'Créer',
319
313
  'action.submit': 'Soumettre',
320
314
  'action.translate': 'Traduire',
@@ -543,9 +537,6 @@ const es: UiStringTable = {
543
537
  'action.save': 'Guardar',
544
538
  'action.delete': 'Eliminar',
545
539
  'action.leave': 'Salir',
546
- 'action.muted': 'Silenciado',
547
- 'action.unmuted': 'Activado',
548
- 'toast.muteFailed': 'No se pudieron cambiar las notificaciones de este chat',
549
540
  'action.create': 'Crear',
550
541
  'action.submit': 'Enviar',
551
542
  'action.translate': 'Traducir',
@@ -774,9 +765,6 @@ const pt: UiStringTable = {
774
765
  'action.save': 'Guardar',
775
766
  'action.delete': 'Eliminar',
776
767
  'action.leave': 'Sair',
777
- 'action.muted': 'Muted',
778
- 'action.unmuted': 'Unmuted',
779
- 'toast.muteFailed': 'Could not update notifications for this chat',
780
768
  'action.create': 'Criar',
781
769
  'action.submit': 'Enviar',
782
770
  'action.translate': 'Traduzir',
@@ -1005,9 +993,6 @@ const ht: UiStringTable = {
1005
993
  'action.save': 'Anrejistre',
1006
994
  'action.delete': 'Efase',
1007
995
  'action.leave': 'Kite',
1008
- 'action.muted': 'Muted',
1009
- 'action.unmuted': 'Unmuted',
1010
- 'toast.muteFailed': 'Could not update notifications for this chat',
1011
996
  'action.create': 'Kreye',
1012
997
  'action.submit': 'Soumèt',
1013
998
  'action.translate': 'Tradui',
@@ -1236,9 +1221,6 @@ const zh: UiStringTable = {
1236
1221
  'action.save': '保存',
1237
1222
  'action.delete': '删除',
1238
1223
  'action.leave': '退出',
1239
- 'action.muted': 'Muted',
1240
- 'action.unmuted': 'Unmuted',
1241
- 'toast.muteFailed': 'Could not update notifications for this chat',
1242
1224
  'action.create': '创建',
1243
1225
  'action.submit': '提交',
1244
1226
  'action.translate': '翻译',
@@ -130,7 +130,6 @@ function dispatchRoomsFromRestItems(items: ApiRoom[]): void {
130
130
  icon: item.picture || item.icon,
131
131
  createdAt: item.createdAt,
132
132
  updatedAt: item.updatedAt,
133
- muted: item.muted === true,
134
133
  description: (item as any).description,
135
134
  type: (item as any).type,
136
135
  // ChatProfileModal renders this list under the description/type
@@ -255,14 +254,6 @@ export async function postPrivateRoom(
255
254
  }
256
255
  }
257
256
 
258
- export async function setRoomMuted(chatName: string, muted: boolean): Promise<boolean> {
259
- const token = store.getState().chatSettingStore.user.token || '';
260
- const url = `/v1/chats/my/${encodeURIComponent(chatName)}/mute`;
261
- const config = { headers: { Authorization: token } };
262
- const response = muted ? await http.put(url, {}, config) : await http.delete(url, config);
263
- return response.data?.result?.muted ?? muted;
264
- }
265
-
266
257
  export async function postReportRoom(data: PostReportRoom) {
267
258
  const { chatName, category, text } = data;
268
259
  const token = store.getState().chatSettingStore.user.token || '';
@@ -227,11 +227,11 @@ const onMessageHistory = async (stanza: any) => {
227
227
  // what senderJID would have held — fall back to it. createMessageFromXml
228
228
  // already derives a display name from the jid when senderFirstName/
229
229
  // senderLastName are absent.
230
- const innerFrom =
231
- stanza
232
- .getChild('result')
233
- ?.getChild('forwarded')
234
- ?.getChild('message')?.attrs?.from || stanza.attrs.from;
230
+ const forwardedMessage = stanza
231
+ .getChild('result')
232
+ ?.getChild('forwarded')
233
+ ?.getChild('message');
234
+ const innerFrom = forwardedMessage?.attrs?.from || stanza.attrs.from;
235
235
  const senderJID = data?.attrs?.senderJID || innerFrom;
236
236
  if (!data?.attrs || !senderJID) {
237
237
  // console.log(
@@ -242,11 +242,25 @@ const onMessageHistory = async (stanza: any) => {
242
242
  }
243
243
  const mergedAttrs = { ...data.attrs };
244
244
  if (!mergedAttrs.senderJID) {mergedAttrs.senderJID = senderJID;}
245
+ // The forwarded <message>'s own stanza id is our original send id
246
+ // (what `xmppId` holds on the live/wrapped paths) - capture it so
247
+ // this MAM-replayed message dedupes against its live echo /
248
+ // optimistic pending bubble the same way, instead of never carrying
249
+ // an xmppId at all.
250
+ if (forwardedMessage?.attrs?.id) {mergedAttrs.xmppId = forwardedMessage.attrs.id;}
245
251
  const rawMessage = await createMessageFromXml(
246
252
  mergedAttrs,
247
253
  body,
248
254
  id,
249
- stanza.attrs.from,
255
+ // Bug #41: pass the resource-bearing occupant `from`
256
+ // (room@conference/sender-id) here, not the outer envelope's
257
+ // `stanza.attrs.from` (which has no resource for a MAM result).
258
+ // createMessageFromXml derives `user.id` from this field's
259
+ // resource - the same authoritative source getDataFromXml uses
260
+ // for live messages - so catch-up rows resolve the correct
261
+ // sender from the very first render instead of needing a later
262
+ // correction once the "official" history fetch lands.
263
+ innerFrom,
250
264
  !!deleted
251
265
  );
252
266
 
@@ -1,22 +1,32 @@
1
1
  import { getChatsPrivateStoreRequest } from './getChatsPrivateStoreRequest.xmpp';
2
2
  import { setChatsPrivateStoreRequest } from './setChatsPrivateStoreRequest.xmpp';
3
+ import {
4
+ getServerReadTimestamp,
5
+ isCorruptFutureReadMarker,
6
+ type ReadTimestampHeapState,
7
+ } from '../../helpers/getServerReadTimestamp';
8
+ import type { IMessage } from '../../types/types';
3
9
 
4
10
  interface RoomLike {
5
11
  jid: string;
6
12
  lastViewedTimestamp?: number;
7
13
  unreadMessages?: number;
14
+ // Needed to resolve a SERVER-timestamp read marker via
15
+ // getServerReadTimestamp instead of the device clock - see bug #38.
16
+ messages?: IMessage[];
8
17
  }
9
18
 
10
19
  interface FlushOpts {
11
20
  // When visibleRoomJID is set, treat it as "user just viewed everything
12
- // up to now" and stamp it with Date.now() — even if its in-memory
13
- // lastViewedTimestamp has not been updated yet.
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
- // Date.now(). ChatRoom passes the timestamp of the newest message the
17
- // user actually reached when they leave a room while scrolled up —
18
- // without it, the Date.now() default writes "read everything" to 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
- * still uses actionSetTimestampToPrivateStore for the single-room case.
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
- // For the visible room, persist "now" — read state is driven by
66
- // room visibility, not by a sentinel timestamp. The exception is an
67
- // explicit `visibleRoomTs`: the caller knows the user only read up
68
- // to a certain point (left the room while scrolled up), so stamping
69
- // "now" here would mark genuinely-unread messages as read.
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
- : nowMs;
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
- if (Number.isFinite(prevNum) && prevNum >= ts) {continue;}
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 && Number.isFinite(n) && n > 0) {normalized[jid] = n;}
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
- return await flushLastViewedToPrivateStore(this, rooms, opts);
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
  }
@@ -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 { persistenceMiddleware, readPersistedState } from './persistence';
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 — read persisted slices and replay them as standard
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
  })());