@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
@@ -1,5 +1,35 @@
1
1
  import { IMessage } from '../types/types';
2
2
  import { isDateAfter, isDateBefore } from './dateComparison';
3
+ import { msgSortableMs } from './msgSortableMs';
4
+
5
+ // Robustly resolve a caller-supplied read marker to epoch milliseconds.
6
+ // Callers pass a plain number (the redux read boundary, or a room's
7
+ // lastViewedTimestamp), a Date object (older call sites), or occasionally
8
+ // a numeric string. These are NOT interchangeable through the Date
9
+ // constructor: `new Date(1757000000123)` (a number) reads as epoch-ms,
10
+ // but `new Date("1757000000123")` (a string) is parsed as a date STRING
11
+ // - a bare 13-digit string matches no recognized date format, so the
12
+ // result is silently Invalid Date and every comparison against it is
13
+ // `false`. Try Number() first so a numeric string/number is read as
14
+ // epoch-ms; only fall through to Date parsing for genuine date/ISO
15
+ // strings (e.g. a Date object's own `.toString()`). Never falls back to
16
+ // `Date.now()` - a marker we can't resolve is treated as "no marker",
17
+ // not "now" (bug #38).
18
+ function resolveMarkerMs(
19
+ marker: number | { toString: () => string } | null | undefined
20
+ ): number {
21
+ if (marker == null) {return 0;}
22
+ if (typeof marker === 'number') {
23
+ return Number.isFinite(marker) ? marker : 0;
24
+ }
25
+ const raw = marker.toString();
26
+ if (raw.trim() !== '') {
27
+ const asNumber = Number(raw);
28
+ if (Number.isFinite(asNumber)) {return asNumber;}
29
+ }
30
+ const parsed = Date.parse(raw);
31
+ return Number.isFinite(parsed) ? parsed : 0;
32
+ }
3
33
 
4
34
  function deepMerge(target: any, source: any): any {
5
35
  for (const key in source) {
@@ -19,7 +49,7 @@ function deepMerge(target: any, source: any): any {
19
49
  export function insertMessageWithDelimiter(
20
50
  roomMessages: Partial<IMessage>[],
21
51
  message: IMessage,
22
- lastViewedTimestamp: { toString: () => string } | null,
52
+ lastViewedTimestamp: number | { toString: () => string } | null,
23
53
  ) {
24
54
  const existingIndex = roomMessages.findIndex(
25
55
  (msg) =>
@@ -62,33 +92,39 @@ export function insertMessageWithDelimiter(
62
92
  roomMessages.push(message);
63
93
  }
64
94
 
95
+ const lastViewedMs = resolveMarkerMs(lastViewedTimestamp);
96
+ const newMessageMs = msgSortableMs(message);
65
97
  if (
66
- lastViewedTimestamp &&
67
- !roomMessages.some((msg) => msg.id === 'delimiter-new') &&
68
- isDateAfter(newMessageDate.toString(), lastViewedTimestamp.toString())
98
+ lastViewedMs > 0 &&
99
+ newMessageMs > lastViewedMs &&
100
+ !roomMessages.some((msg) => msg.id === 'delimiter-new')
69
101
  ) {
70
- const delimiterIndex = roomMessages.findIndex((msg) =>
71
- isDateAfter(msg.date?.toString() ?? '', lastViewedTimestamp.toString())
102
+ // Find the first message STRICTLY newer than the marker, using the
103
+ // SAME timestamp source (msgSortableMs - the server id-encoded ms)
104
+ // as the unread middleware and countNewerMessages use to decide
105
+ // what counts as unread. Comparing against a DIFFERENT source (the
106
+ // old code compared `msg.date` strings) risks the boundary message
107
+ // itself landing on the wrong side of the cut: since bug #38 the
108
+ // marker can equal a real message's own timestamp exactly, and any
109
+ // drift between `date` and the id-encoded ms - or a raw numeric
110
+ // marker silently producing an Invalid Date via the Date
111
+ // constructor's string-parsing path - could count that message as
112
+ // unread, pushing the divider one message too early (ABOVE the
113
+ // last read message instead of directly below it). Customer #42.
114
+ const delimiterIndex = roomMessages.findIndex(
115
+ (msg) => msg.id !== 'delimiter-new' && msgSortableMs(msg) > lastViewedMs
72
116
  );
73
117
 
74
118
  if (delimiterIndex !== -1) {
75
119
  // The divider's date is what ORDERS it: roomsSlice re-sorts every
76
120
  // merged page by timestamp (see `byMs`), so a divider stamped with
77
- // `new Date()` — i.e. now — sorted past every real message and
121
+ // `new Date()` (i.e. now) sorted past every real message and
78
122
  // rendered at the very bottom, BELOW the new messages it is
79
123
  // supposed to introduce. Anchor it a hair before the first unread
80
124
  // instead, which is where it belongs and where any later sort keeps
81
- // it. Derived from that message rather than from
82
- // lastViewedTimestamp so it works whatever format the caller passes.
83
- const firstUnreadMs = Date.parse(
84
- String(roomMessages[delimiterIndex]?.date ?? '')
85
- );
86
- const rawLastViewed = lastViewedTimestamp.toString();
87
- const lastViewedMs =
88
- Date.parse(rawLastViewed) || Number(rawLastViewed) || Date.now();
89
- const anchorMs = Number.isFinite(firstUnreadMs)
90
- ? firstUnreadMs - 1
91
- : lastViewedMs;
125
+ // it.
126
+ const firstUnreadMs = msgSortableMs(roomMessages[delimiterIndex]);
127
+ const anchorMs = firstUnreadMs > 0 ? firstUnreadMs - 1 : lastViewedMs;
92
128
 
93
129
  roomMessages.splice(delimiterIndex, 0, {
94
130
  id: 'delimiter-new',
@@ -103,7 +139,7 @@ export function insertMessageWithDelimiter(
103
139
  roomJid: '',
104
140
  });
105
141
  }
106
- }
142
+ }
107
143
  } else if (
108
144
  isDateBefore(newMessageDate.toString(), firstMessage?.date?.toString() ?? '')
109
145
  ) {
@@ -0,0 +1,90 @@
1
+ import { IMessage } from '../types/types';
2
+
3
+ /**
4
+ * Bug #41: during reconnect catch-up (app was backgrounded/offline, then
5
+ * comes back online), messages replayed from MAM could transiently render
6
+ * on the RIGHT (as if sent by the current user) before flipping to the
7
+ * LEFT a moment later. Root cause: `message.user.id === walletAddress`
8
+ * treats `undefined === undefined` (or `'' === ''`) as a match, and the
9
+ * two sides could independently be empty for one frame -- the message's
10
+ * sender id before the MAM-derived value settled, and/or the current
11
+ * user's id before it rehydrated. Two blank ids must never compare equal.
12
+ *
13
+ * A second, related issue: the same logical sender id can show up in
14
+ * different shapes depending on which code path produced it -- a bare
15
+ * id ("wallet123") or a bare JID with an incidental session resource
16
+ * ("wallet123@host/sessionResource", as `senderJID` attrs carry on the
17
+ * wire -- see src/assets/test.xml). In every shape used in this
18
+ * codebase the identifying segment is the part BEFORE the first '@' (or
19
+ * before '/' when there's no '@'); anything after that -- domain,
20
+ * resource -- is noise. Comparing raw strings with `===` fails whenever
21
+ * one side still carries that noise and the other doesn't. Normalizing
22
+ * both sides (strip resource, strip domain, lowercase) before comparing
23
+ * makes the check tolerant of that drift.
24
+ */
25
+
26
+ export interface OwnMessageUserLike {
27
+ id?: string | null;
28
+ xmppUsername?: string | null;
29
+ walletAddress?: string | null;
30
+ }
31
+
32
+ export type OwnMessageCurrentUser =
33
+ | OwnMessageUserLike
34
+ | string
35
+ | null
36
+ | undefined;
37
+
38
+ /**
39
+ * Strip a MUC resource (`room@host/resource` -> `room@host`) and then a
40
+ * JID domain (`local@host` -> `local`), lowercasing the result. Applies
41
+ * to both plain ids and full/partial JIDs so either shape normalizes to
42
+ * the same comparable value.
43
+ */
44
+ const normalizeId = (raw: unknown): string => {
45
+ const value = String(raw ?? '').trim();
46
+ if (!value) {
47
+ return '';
48
+ }
49
+ const withoutResource = value.split('/')[0];
50
+ const withoutDomain = withoutResource.includes('@')
51
+ ? withoutResource.split('@')[0]
52
+ : withoutResource;
53
+ return withoutDomain.toLowerCase();
54
+ };
55
+
56
+ // Same precedence the rest of the app uses when it needs "who am I" as a
57
+ // single id: xmppUsername first, walletAddress as the fallback.
58
+ const resolveCurrentUserId = (currentUser: OwnMessageCurrentUser): string => {
59
+ if (typeof currentUser === 'string') {
60
+ return currentUser;
61
+ }
62
+ if (!currentUser) {
63
+ return '';
64
+ }
65
+ return (
66
+ currentUser.xmppUsername || currentUser.walletAddress || currentUser.id || ''
67
+ );
68
+ };
69
+
70
+ /**
71
+ * Robustly determine whether `message` was sent by `currentUser`. Never
72
+ * returns true from two blank/undefined ids, and tolerates bare-id vs.
73
+ * full-JID mismatches between the sender id and the current user id.
74
+ */
75
+ export const isOwnMessage = (
76
+ message: Pick<IMessage, 'user'> | null | undefined,
77
+ currentUser: OwnMessageCurrentUser
78
+ ): boolean => {
79
+ const messageUserId = normalizeId(message?.user?.id);
80
+ if (!messageUserId) {
81
+ return false;
82
+ }
83
+
84
+ const currentUserId = normalizeId(resolveCurrentUserId(currentUser));
85
+ if (!currentUserId) {
86
+ return false;
87
+ }
88
+
89
+ return messageUserId === currentUserId;
90
+ };
@@ -14,9 +14,30 @@ export const getKeyboardVerticalOffset = ({
14
14
  }: KeyboardLayoutArgs): number =>
15
15
  configuredOffset + (platform === 'ios' ? bottomInset : 0);
16
16
 
17
+ type InputDockPaddingArgs = Omit<KeyboardLayoutArgs, 'configuredOffset'> & {
18
+ // Explicit `inputDockPaddingBottom` config value, verbatim on both
19
+ // platforms when provided (0 allowed). Takes priority over every other
20
+ // rule below, including `hostOwnsLayout`.
21
+ configuredPadding?: number;
22
+ // True when the host app owns keyboard layout entirely (i.e.
23
+ // `disableKeyboardAvoidingView` is set) and `configuredPadding` was not
24
+ // given. The dock then gets no built-in bottom padding of its own,
25
+ // since it already sits above the host's own chrome (e.g. its tab bar).
26
+ hostOwnsLayout?: boolean;
27
+ };
28
+
17
29
  export const getInputDockPaddingBottom = ({
18
30
  platform,
19
31
  bottomInset = 0,
20
32
  keyboardVisible = false,
21
- }: Omit<KeyboardLayoutArgs, 'configuredOffset'>): number =>
22
- platform === 'ios' ? bottomInset : ANDROID_INPUT_DOCK_GAP;
33
+ configuredPadding,
34
+ hostOwnsLayout = false,
35
+ }: InputDockPaddingArgs): number => {
36
+ if (typeof configuredPadding === 'number') {
37
+ return configuredPadding;
38
+ }
39
+ if (hostOwnsLayout) {
40
+ return 0;
41
+ }
42
+ return platform === 'ios' ? bottomInset : ANDROID_INPUT_DOCK_GAP;
43
+ };
@@ -289,25 +289,9 @@ export function ensureFilenameHasExtension(
289
289
  return `${base}${getExtensionForMime(mime)}`;
290
290
  }
291
291
 
292
- /**
293
- * Best-effort filename derivation for a message bubble:
294
- * - server-provided `fileName`
295
- * - server-provided `originalName`
296
- * - URL pathname
297
- * - generic `media_<timestamp>` so we never render an empty label
298
- *
299
- * Always returns a string with a valid extension.
300
- */
301
- export function deriveDisplayFilename(opts: {
302
- fileName?: string | null;
303
- originalName?: string | null;
304
- url?: string | null;
305
- mime?: string | null;
306
- }): string {
307
- const candidate =
308
- (opts.fileName && opts.fileName.trim()) ||
309
- (opts.originalName && opts.originalName.trim()) ||
310
- filenameFromUrl(opts.url) ||
311
- '';
312
- return ensureFilenameHasExtension(candidate, opts.mime);
313
- }
292
+ // NOTE: the "which name wins" decision (originalName vs the stored
293
+ // fileName vs the URL) lives in helpers/getDisplayFileName.ts - that is
294
+ // the single source of truth used across display, save, share and
295
+ // download call sites (bug #40: this used to be duplicated here with
296
+ // the priority backwards, so attachments showed/saved under the
297
+ // server's hash name instead of the sender's original one).
@@ -0,0 +1,25 @@
1
+ // Count messages strictly newer than a given millisecond timestamp using
2
+ // `msg.id` (the server-authoritative microsecond timestamp prefixed by a
3
+ // 13-digit millis, see `helpers/dateComparison`'s
4
+ // `getHighResolutionTimestamp`) rather than `msg.date`, which can be
5
+ // derived client-side (`createMessageFromXml` falls back to `Date.now()`
6
+ // for realtime stanzas without a `date` attr) and drift from what the
7
+ // server actually assigned.
8
+ //
9
+ // Extracted to its own module (rather than living in roomsSlice.ts, its
10
+ // original home) so helpers that need the SAME ordering source - notably
11
+ // `insertMessageWithDelimiter.ts`, which decides where the "New messages"
12
+ // divider sits - can import it without creating a roomStore <-> helpers
13
+ // import cycle (roomsSlice.ts already imports insertMessageWithDelimiter).
14
+ // roomsSlice.ts re-exports this for backward compatibility with existing
15
+ // call sites.
16
+ export const msgSortableMs = (msg: any): number => {
17
+ const id = String(msg?.id || '');
18
+ const m = /^(\d{13})/.exec(id);
19
+ if (m) {return Number(m[1]);}
20
+ if (msg?.date) {
21
+ const t = new Date(msg.date as any).getTime();
22
+ if (Number.isFinite(t)) {return t;}
23
+ }
24
+ return 0;
25
+ };
@@ -1,12 +1,17 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { useDispatch } from 'react-redux';
3
3
  import {
4
+ clearReadBoundary,
4
5
  clearVisibleRoom,
5
6
  setCurrentRoom,
6
7
  setLastViewedTimestamp,
7
8
  setVisibleRoom,
8
9
  } from '../roomStore/roomsSlice';
9
10
  import { store } from '../roomStore';
11
+ import {
12
+ getFlushBoundaryTs,
13
+ getReadMarkerTimestamp,
14
+ } from '../helpers/getServerReadTimestamp';
10
15
 
11
16
  interface UseChatRoomFocusOptions {
12
17
  /** The room JID that the consumer's tab/screen is currently showing. */
@@ -32,7 +37,10 @@ interface UseChatRoomFocusOptions {
32
37
  * Without this hook, `useUnread()` will always return 0 for the chat
33
38
  * room because the SDK assumes "mounted == active". With this hook,
34
39
  * focus marks the room visible (clearing the badge) and blur stamps
35
- * `lastViewedTimestamp = Date.now()` so future messages count as unread.
40
+ * `lastViewedTimestamp` to the newest SERVER-acknowledged message so
41
+ * future messages count as unread. Deliberately not `Date.now()`: a
42
+ * device clock running ahead would write a future marker that the
43
+ * forward-only private-store merge could never correct again (bug #38).
36
44
  *
37
45
  * Usage with React Navigation:
38
46
  *
@@ -58,20 +66,52 @@ export const useChatRoomFocus = ({
58
66
  isFocused: false,
59
67
  });
60
68
 
61
- const leaveRoom = (jid: string) => {
62
- const timestamp = Date.now();
63
- dispatch(setLastViewedTimestamp({ chatJID: jid, timestamp }));
69
+ // `clearBoundary`: pass true only when the room the user is leaving is
70
+ // genuinely being released (switching to a different room, or this
71
+ // hook unmounting) - not on a mere same-room blur. `<ChatRoom>`'s
72
+ // MessageList typically stays mounted across a same-room focus loss
73
+ // (the whole point of this hook is that the host keeps `<Chat>`
74
+ // mounted in a hidden tab), so its own scroll-tracking ref is still
75
+ // intact and the redux boundary must stay in sync with it - clearing
76
+ // it here would desync the two.
77
+ const leaveRoom = (jid: string, clearBoundary: boolean) => {
78
+ const state = store.getState();
79
+ const rooms = state.rooms?.rooms;
80
+ const boundaryTs = state.rooms?.readBoundaries?.[jid] ?? null;
81
+ // getReadMarkerTimestamp: honour the boundary (the newest message the
82
+ // user actually reached) when the user left this room scrolled up,
83
+ // otherwise fall back to the newest server-acked message. Only stamp
84
+ // when we actually have something to anchor to - skip rather than
85
+ // fall back to the device clock (bug #38). Ignoring the boundary here
86
+ // would stamp "everything read" regardless of scroll position (bug
87
+ // #42, a regression of #33 reintroduced by #38).
88
+ const timestamp = getReadMarkerTimestamp(rooms?.[jid], state.roomHeapSlice, boundaryTs);
89
+ if (timestamp > 0) {
90
+ dispatch(setLastViewedTimestamp({ chatJID: jid, timestamp }));
91
+ }
64
92
  dispatch(clearVisibleRoom());
65
- const rooms = store.getState().rooms?.rooms;
66
- (store.getState().chatSettingStore as any)?.client
67
- ?.flushLastViewedToPrivateStoreStanza(rooms, { visibleRoomJID: jid })
93
+ (state.chatSettingStore as any)?.client
94
+ ?.flushLastViewedToPrivateStoreStanza(rooms, {
95
+ visibleRoomJID: jid,
96
+ // Carry the same boundary to the SERVER marker - without it the
97
+ // flush defaults to "everything" for the visible room. Clamped
98
+ // through the same helper as the local stamp above.
99
+ visibleRoomTs: getFlushBoundaryTs(
100
+ rooms?.[jid],
101
+ state.roomHeapSlice,
102
+ boundaryTs
103
+ ),
104
+ })
68
105
  .catch(() => {});
106
+ if (clearBoundary) {
107
+ dispatch(clearReadBoundary({ jid }));
108
+ }
69
109
  };
70
110
 
71
111
  useEffect(() => {
72
112
  const prev = prevRef.current;
73
113
  if (prev.isFocused && prev.roomJID && (prev.roomJID !== roomJID || !isFocused)) {
74
- leaveRoom(prev.roomJID);
114
+ leaveRoom(prev.roomJID, prev.roomJID !== roomJID);
75
115
  }
76
116
 
77
117
  if (roomJID && isFocused) {
@@ -86,7 +126,7 @@ export const useChatRoomFocus = ({
86
126
  return () => {
87
127
  const prev = prevRef.current;
88
128
  if (prev.isFocused && prev.roomJID) {
89
- leaveRoom(prev.roomJID);
129
+ leaveRoom(prev.roomJID, true);
90
130
  }
91
131
  };
92
132
  }, []);
@@ -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'
@@ -10,6 +10,7 @@ import { pushSubscriptionService } from '../services/pushSubscriptionService';
10
10
  import { clearRoomsRestCache } from '../networking/api-requests/rooms.api';
11
11
  import { clearPersistedState } from '../roomStore/persistence';
12
12
  import { secureUserStorage } from '../helpers/secureUserStorage';
13
+ import { getFlushBoundaryTs } from '../helpers/getServerReadTimestamp';
13
14
 
14
15
  // AsyncStorage keys the library writes but that aren't cleared by any
15
16
  // slice reducer. Listed here so a single logout call wipes the full
@@ -59,10 +60,23 @@ const logoutService = {
59
60
  const client = (state.chatSettingStore as any)?.client;
60
61
  const rooms = state.rooms?.rooms;
61
62
  const visibleRoomJID = state.rooms?.visibleRoomJID || null;
63
+ // The visible room is exempt from `onlyIfNoUnread`'s skip (it's
64
+ // meant to write "read up to now" for whatever's open), so if the
65
+ // user is logging out while scrolled up with unread messages
66
+ // above, the boundary must still be carried through here - same
67
+ // reasoning as every other "leaving a room" path (bug #42).
68
+ const boundaryTs = visibleRoomJID
69
+ ? getFlushBoundaryTs(
70
+ rooms?.[visibleRoomJID],
71
+ state.roomHeapSlice,
72
+ state.rooms?.readBoundaries?.[visibleRoomJID] ?? null
73
+ )
74
+ : undefined;
62
75
  if (client?.flushLastViewedToPrivateStoreStanza) {
63
76
  await Promise.race([
64
77
  client.flushLastViewedToPrivateStoreStanza(rooms, {
65
78
  visibleRoomJID,
79
+ visibleRoomTs: boundaryTs,
66
80
  onlyIfNoUnread: true,
67
81
  }),
68
82
  new Promise((res) => setTimeout(res, 2000)),
@@ -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