@ethora/chat-component-rn 26.7.2 → 26.7.4

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 (263) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +9 -8
  3. package/lib/commonjs/assets/icons.js +7 -2
  4. package/lib/commonjs/assets/icons.js.map +1 -1
  5. package/lib/commonjs/components/Header/HeaderRoomList.js +20 -6
  6. package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/ChatRoom.js +86 -7
  8. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/ChatWrapper.js +18 -0
  10. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  11. package/lib/commonjs/components/MainComponents/MessageList.js +11 -4
  12. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  13. package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js +5 -5
  14. package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
  15. package/lib/commonjs/components/MainComponents/RoomList.js +62 -32
  16. package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
  17. package/lib/commonjs/components/Menu/HeaderRoomListMenu.js +208 -70
  18. package/lib/commonjs/components/Menu/HeaderRoomListMenu.js.map +1 -1
  19. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +307 -99
  20. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  21. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +544 -200
  22. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  23. package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js +226 -0
  24. package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -0
  25. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +6 -2
  26. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  27. package/lib/commonjs/components/Modals/Modal/Modal.js +8 -0
  28. package/lib/commonjs/components/Modals/Modal/Modal.js.map +1 -1
  29. package/lib/commonjs/components/Modals/NewChatModal/NewChatModal.js +202 -104
  30. package/lib/commonjs/components/Modals/NewChatModal/NewChatModal.js.map +1 -1
  31. package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js +556 -0
  32. package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -0
  33. package/lib/commonjs/components/Modals/ProfileHeader/ProfileMenu.js +210 -0
  34. package/lib/commonjs/components/Modals/ProfileHeader/ProfileMenu.js.map +1 -0
  35. package/lib/commonjs/components/Modals/SelectUsersModal/SelectUsersModal.js +4 -2
  36. package/lib/commonjs/components/Modals/SelectUsersModal/SelectUsersModal.js.map +1 -1
  37. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +507 -244
  38. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  39. package/lib/commonjs/components/Modals/UserSettingsModal/UserSettingsModal.js +74 -46
  40. package/lib/commonjs/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
  41. package/lib/commonjs/components/Modals/styledModalComponents.js +9 -2
  42. package/lib/commonjs/components/Modals/styledModalComponents.js.map +1 -1
  43. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +17 -7
  44. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
  45. package/lib/commonjs/components/styled/SendInput.js +80 -1
  46. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  47. package/lib/commonjs/components/styled/StyledComponents.js +9 -3
  48. package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
  49. package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js +11 -4
  50. package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
  51. package/lib/commonjs/context/xmppProvider.js +12 -0
  52. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  53. package/lib/commonjs/helpers/getChatBackground.js +23 -0
  54. package/lib/commonjs/helpers/getChatBackground.js.map +1 -0
  55. package/lib/commonjs/helpers/historyPreloadScheduler.js +65 -2
  56. package/lib/commonjs/helpers/historyPreloadScheduler.js.map +1 -1
  57. package/lib/commonjs/helpers/mediaLibraryRuntime.js +31 -0
  58. package/lib/commonjs/helpers/mediaLibraryRuntime.js.map +1 -0
  59. package/lib/commonjs/helpers/shallowEqual.js +36 -0
  60. package/lib/commonjs/helpers/shallowEqual.js.map +1 -0
  61. package/lib/commonjs/helpers/sheetGestures.js +32 -0
  62. package/lib/commonjs/helpers/sheetGestures.js.map +1 -0
  63. package/lib/commonjs/hooks/useChatWrapperInit.js +15 -0
  64. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  65. package/lib/commonjs/hooks/useComposing.js +27 -0
  66. package/lib/commonjs/hooks/useComposing.js.map +1 -1
  67. package/lib/commonjs/hooks/useSendMessage.js +27 -1
  68. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  69. package/lib/commonjs/hooks/useUnreadMessagesCounter.js +6 -1
  70. package/lib/commonjs/hooks/useUnreadMessagesCounter.js.map +1 -1
  71. package/lib/commonjs/i18n/strings.js +121 -1
  72. package/lib/commonjs/i18n/strings.js.map +1 -1
  73. package/lib/commonjs/networking/api-requests/user.api.js +59 -0
  74. package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
  75. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js +6 -2
  76. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  77. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  78. package/lib/commonjs/roomStore/chatSettingsSlice.js +3 -1
  79. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  80. package/lib/commonjs/roomStore/roomsSlice.js +2 -2
  81. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  82. package/lib/module/assets/icons.js +6 -1
  83. package/lib/module/assets/icons.js.map +1 -1
  84. package/lib/module/components/Header/HeaderRoomList.js +20 -6
  85. package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
  86. package/lib/module/components/MainComponents/ChatRoom.js +87 -8
  87. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  88. package/lib/module/components/MainComponents/ChatWrapper.js +19 -1
  89. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  90. package/lib/module/components/MainComponents/MessageList.js +11 -4
  91. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  92. package/lib/module/components/MainComponents/ProfileImagePlaceholder.js +5 -5
  93. package/lib/module/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
  94. package/lib/module/components/MainComponents/RoomList.js +62 -32
  95. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  96. package/lib/module/components/Menu/HeaderRoomListMenu.js +211 -73
  97. package/lib/module/components/Menu/HeaderRoomListMenu.js.map +1 -1
  98. package/lib/module/components/Modals/AttachSheet/AttachSheet.js +298 -102
  99. package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  100. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +547 -204
  101. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  102. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js +223 -0
  103. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -0
  104. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +6 -2
  105. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  106. package/lib/module/components/Modals/Modal/Modal.js +9 -1
  107. package/lib/module/components/Modals/Modal/Modal.js.map +1 -1
  108. package/lib/module/components/Modals/NewChatModal/NewChatModal.js +205 -106
  109. package/lib/module/components/Modals/NewChatModal/NewChatModal.js.map +1 -1
  110. package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js +548 -0
  111. package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -0
  112. package/lib/module/components/Modals/ProfileHeader/ProfileMenu.js +205 -0
  113. package/lib/module/components/Modals/ProfileHeader/ProfileMenu.js.map +1 -0
  114. package/lib/module/components/Modals/SelectUsersModal/SelectUsersModal.js +4 -2
  115. package/lib/module/components/Modals/SelectUsersModal/SelectUsersModal.js.map +1 -1
  116. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +509 -248
  117. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  118. package/lib/module/components/Modals/UserSettingsModal/UserSettingsModal.js +76 -48
  119. package/lib/module/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
  120. package/lib/module/components/Modals/styledModalComponents.js +9 -2
  121. package/lib/module/components/Modals/styledModalComponents.js.map +1 -1
  122. package/lib/module/components/RoomComponents/ChatRoomItem.js +17 -7
  123. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  124. package/lib/module/components/styled/SendInput.js +80 -1
  125. package/lib/module/components/styled/SendInput.js.map +1 -1
  126. package/lib/module/components/styled/StyledComponents.js +9 -3
  127. package/lib/module/components/styled/StyledComponents.js.map +1 -1
  128. package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js +11 -4
  129. package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
  130. package/lib/module/context/xmppProvider.js +12 -0
  131. package/lib/module/context/xmppProvider.js.map +1 -1
  132. package/lib/module/helpers/getChatBackground.js +18 -0
  133. package/lib/module/helpers/getChatBackground.js.map +1 -0
  134. package/lib/module/helpers/historyPreloadScheduler.js +66 -3
  135. package/lib/module/helpers/historyPreloadScheduler.js.map +1 -1
  136. package/lib/module/helpers/mediaLibraryRuntime.js +25 -0
  137. package/lib/module/helpers/mediaLibraryRuntime.js.map +1 -0
  138. package/lib/module/helpers/shallowEqual.js +31 -0
  139. package/lib/module/helpers/shallowEqual.js.map +1 -0
  140. package/lib/module/helpers/sheetGestures.js +26 -0
  141. package/lib/module/helpers/sheetGestures.js.map +1 -0
  142. package/lib/module/hooks/useChatWrapperInit.js +15 -0
  143. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  144. package/lib/module/hooks/useComposing.js +28 -1
  145. package/lib/module/hooks/useComposing.js.map +1 -1
  146. package/lib/module/hooks/useSendMessage.js +28 -2
  147. package/lib/module/hooks/useSendMessage.js.map +1 -1
  148. package/lib/module/hooks/useUnreadMessagesCounter.js +6 -1
  149. package/lib/module/hooks/useUnreadMessagesCounter.js.map +1 -1
  150. package/lib/module/i18n/strings.js +121 -1
  151. package/lib/module/i18n/strings.js.map +1 -1
  152. package/lib/module/networking/api-requests/user.api.js +56 -0
  153. package/lib/module/networking/api-requests/user.api.js.map +1 -1
  154. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js +6 -2
  155. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  156. package/lib/module/networking/xmppClient.js.map +1 -1
  157. package/lib/module/roomStore/chatSettingsSlice.js +3 -1
  158. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  159. package/lib/module/roomStore/roomsSlice.js +1 -1
  160. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  161. package/lib/typescript/assets/icons.d.ts +12 -0
  162. package/lib/typescript/assets/icons.d.ts.map +1 -1
  163. package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
  164. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  165. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  166. package/lib/typescript/components/MainComponents/MessageList.d.ts +10 -1
  167. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  168. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  169. package/lib/typescript/components/Menu/HeaderRoomListMenu.d.ts.map +1 -1
  170. package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts +9 -0
  171. package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
  172. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts +10 -0
  173. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  174. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts +9 -0
  175. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts.map +1 -0
  176. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  177. package/lib/typescript/components/Modals/Modal/Modal.d.ts.map +1 -1
  178. package/lib/typescript/components/Modals/NewChatModal/NewChatModal.d.ts +1 -1
  179. package/lib/typescript/components/Modals/NewChatModal/NewChatModal.d.ts.map +1 -1
  180. package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts +75 -0
  181. package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts.map +1 -0
  182. package/lib/typescript/components/Modals/ProfileHeader/ProfileMenu.d.ts +25 -0
  183. package/lib/typescript/components/Modals/ProfileHeader/ProfileMenu.d.ts.map +1 -0
  184. package/lib/typescript/components/Modals/SelectUsersModal/SelectUsersModal.d.ts +6 -1
  185. package/lib/typescript/components/Modals/SelectUsersModal/SelectUsersModal.d.ts.map +1 -1
  186. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts +16 -0
  187. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
  188. package/lib/typescript/components/Modals/UserSettingsModal/UserSettingsModal.d.ts +1 -0
  189. package/lib/typescript/components/Modals/UserSettingsModal/UserSettingsModal.d.ts.map +1 -1
  190. package/lib/typescript/components/Modals/styledModalComponents.d.ts.map +1 -1
  191. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  192. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  193. package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
  194. package/lib/typescript/components/styled/StyledInputComponents/StyledInputComponents.d.ts.map +1 -1
  195. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  196. package/lib/typescript/helpers/getChatBackground.d.ts +15 -0
  197. package/lib/typescript/helpers/getChatBackground.d.ts.map +1 -0
  198. package/lib/typescript/helpers/historyPreloadScheduler.d.ts.map +1 -1
  199. package/lib/typescript/helpers/mediaLibraryRuntime.d.ts +3 -0
  200. package/lib/typescript/helpers/mediaLibraryRuntime.d.ts.map +1 -0
  201. package/lib/typescript/helpers/shallowEqual.d.ts +11 -0
  202. package/lib/typescript/helpers/shallowEqual.d.ts.map +1 -0
  203. package/lib/typescript/helpers/sheetGestures.d.ts +20 -0
  204. package/lib/typescript/helpers/sheetGestures.d.ts.map +1 -0
  205. package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
  206. package/lib/typescript/hooks/useComposing.d.ts.map +1 -1
  207. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  208. package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts +1 -0
  209. package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts.map +1 -1
  210. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  211. package/lib/typescript/networking/api-requests/user.api.d.ts +27 -0
  212. package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
  213. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts +1 -0
  214. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts.map +1 -1
  215. package/lib/typescript/networking/xmppClient.d.ts +1 -0
  216. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  217. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  218. package/lib/typescript/roomStore/roomsSlice.d.ts +1 -0
  219. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  220. package/lib/typescript/types/types.d.ts +9 -5
  221. package/lib/typescript/types/types.d.ts.map +1 -1
  222. package/package.json +6 -1
  223. package/src/assets/icons.tsx +29 -9
  224. package/src/components/Header/HeaderRoomList.tsx +20 -5
  225. package/src/components/MainComponents/ChatRoom.tsx +92 -8
  226. package/src/components/MainComponents/ChatWrapper.tsx +19 -1
  227. package/src/components/MainComponents/MessageList.tsx +21 -3
  228. package/src/components/MainComponents/ProfileImagePlaceholder.tsx +5 -5
  229. package/src/components/MainComponents/RoomList.tsx +65 -34
  230. package/src/components/Menu/HeaderRoomListMenu.tsx +209 -64
  231. package/src/components/Modals/AttachSheet/AttachSheet.tsx +318 -86
  232. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +599 -278
  233. package/src/components/Modals/ChatProfileModal/ReportChatModal.tsx +244 -0
  234. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +4 -2
  235. package/src/components/Modals/Modal/Modal.tsx +7 -0
  236. package/src/components/Modals/NewChatModal/NewChatModal.tsx +219 -117
  237. package/src/components/Modals/ProfileHeader/ProfileHeader.tsx +615 -0
  238. package/src/components/Modals/ProfileHeader/ProfileMenu.tsx +217 -0
  239. package/src/components/Modals/SelectUsersModal/SelectUsersModal.tsx +14 -4
  240. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +617 -307
  241. package/src/components/Modals/UserSettingsModal/UserSettingsModal.tsx +75 -58
  242. package/src/components/Modals/styledModalComponents.tsx +9 -2
  243. package/src/components/RoomComponents/ChatRoomItem.tsx +14 -7
  244. package/src/components/styled/SendInput.tsx +83 -2
  245. package/src/components/styled/StyledComponents.ts +9 -3
  246. package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +11 -4
  247. package/src/context/xmppProvider.tsx +14 -0
  248. package/src/helpers/getChatBackground.ts +18 -0
  249. package/src/helpers/historyPreloadScheduler.ts +80 -3
  250. package/src/helpers/mediaLibraryRuntime.ts +25 -0
  251. package/src/helpers/shallowEqual.ts +33 -0
  252. package/src/helpers/sheetGestures.ts +25 -0
  253. package/src/hooks/useChatWrapperInit.ts +15 -0
  254. package/src/hooks/useComposing.tsx +37 -1
  255. package/src/hooks/useSendMessage.tsx +32 -2
  256. package/src/hooks/useUnreadMessagesCounter.ts +11 -0
  257. package/src/i18n/strings.ts +121 -1
  258. package/src/networking/api-requests/user.api.ts +71 -0
  259. package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +17 -3
  260. package/src/networking/xmppClient.ts +5 -1
  261. package/src/roomStore/chatSettingsSlice.ts +3 -1
  262. package/src/roomStore/roomsSlice.ts +1 -1
  263. package/src/types/types.ts +16 -5
@@ -1,4 +1,5 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import { AppState, AppStateStatus } from 'react-native';
2
3
  import { useXmppClient } from '../context/xmppProvider';
3
4
  import { useSelector } from 'react-redux';
4
5
  import { RootState } from '../roomStore';
@@ -9,8 +10,16 @@ const useComposing = (config?: IConfig) => {
9
10
  const { client } = useXmppClient();
10
11
  const { activeRoomJID } = useSelector((state: RootState) => state.rooms);
11
12
  const { user } = useChatSettingState();
13
+ // RN keeps a focused TextInput focused when the app is backgrounded or
14
+ // the device is locked — nothing fires `onBlur`, so without this the
15
+ // other participant sees "typing…" for as long as the user is away.
16
+ // Tracks whether the input is *currently* focused, independent of the
17
+ // foreground/background transition, so we know whether to resume
18
+ // composing on return.
19
+ const isFocusedRef = useRef(false);
12
20
 
13
21
  const sendStartComposing = useCallback(() => {
22
+ isFocusedRef.current = true;
14
23
  if (config?.disableTypingIndicator) {
15
24
  return;
16
25
  }
@@ -24,6 +33,7 @@ const useComposing = (config?: IConfig) => {
24
33
  }, [activeRoomJID, client, config?.disableTypingIndicator, user.firstName, user.lastName]);
25
34
 
26
35
  const sendEndComposing = useCallback(() => {
36
+ isFocusedRef.current = false;
27
37
  if (config?.disableTypingIndicator) {
28
38
  return;
29
39
  }
@@ -44,6 +54,32 @@ const useComposing = (config?: IConfig) => {
44
54
  return () => clearTimeout(timerId);
45
55
  }, [sendEndComposing]);
46
56
 
57
+ // Foreground/background transition only — a pause in typing must NOT
58
+ // clear the indicator (that would flicker for a slowly-composing user),
59
+ // so this deliberately does not hook keystroke/idle timing at all.
60
+ useEffect(() => {
61
+ const handleAppStateChange = (nextState: AppStateStatus) => {
62
+ if (nextState !== 'active') {
63
+ if (isFocusedRef.current && !config?.disableTypingIndicator && client) {
64
+ client.sendTypingRequestStanza(
65
+ activeRoomJID || '',
66
+ `${user.firstName} ${user.lastName}`,
67
+ false
68
+ );
69
+ }
70
+ } else if (isFocusedRef.current && !config?.disableTypingIndicator && client) {
71
+ client.sendTypingRequestStanza(
72
+ activeRoomJID || '',
73
+ `${user.firstName} ${user.lastName}`,
74
+ true
75
+ );
76
+ }
77
+ };
78
+
79
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
80
+ return () => subscription.remove();
81
+ }, [activeRoomJID, client, config?.disableTypingIndicator, user.firstName, user.lastName]);
82
+
47
83
  return { sendStartComposing, sendEndComposing };
48
84
  };
49
85
 
@@ -10,7 +10,7 @@ import {
10
10
  markMessageFailed,
11
11
  } from '../roomStore/roomHeapSlice';
12
12
  import { uploadFileV2 } from '../networking/api-requests/auth.api';
13
- import { enqueueOutboundSend } from '../networking/outboundQueue';
13
+ import { enqueueOutboundSend, removeOutboundSend } from '../networking/outboundQueue';
14
14
  import { RootState, store } from '../roomStore';
15
15
  import { getGlobalXmppClient } from '../utils/clientRegistry';
16
16
  import { useEventHandlers } from './useEventHandlers';
@@ -214,6 +214,13 @@ export const useSendMessage = (_configOverride?: IConfig) => {
214
214
  console.warn(
215
215
  `Send watchdog fired — message ${optimisticId} stuck pending > ${PENDING_WATCHDOG_MS}ms; marking failed`
216
216
  );
217
+ // Drop the buffered replay now that the bubble is showing
218
+ // "failed / tap to retry" — a later onOnline flush must not
219
+ // silently resend it behind the user's back. If the original
220
+ // send actually lands after this point, the server echo still
221
+ // flips the bubble back to delivered (newMessageMidlleware);
222
+ // only an explicit tap-to-retry should trigger another send.
223
+ removeOutboundSend(optimisticId);
217
224
  dispatch(
218
225
  markMessageFailed({
219
226
  kind: 'text',
@@ -308,6 +315,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
308
315
  });
309
316
  } catch (error) {
310
317
  clearTimeout(watchdogTimer);
318
+ // Same reasoning as the watchdog: once the bubble shows failed,
319
+ // only an explicit tap-to-retry should resend it.
320
+ removeOutboundSend(optimisticId);
311
321
  dispatch(
312
322
  markMessageFailed({
313
323
  kind: 'text',
@@ -454,6 +464,10 @@ export const useSendMessage = (_configOverride?: IConfig) => {
454
464
  console.warn(
455
465
  `Media-send watchdog fired — ${id} stuck pending > ${PENDING_WATCHDOG_MS}ms; marking failed`
456
466
  );
467
+ // See the text-send watchdog above: drop the buffered replay so
468
+ // a later onOnline flush can't silently resend behind the
469
+ // "failed / tap to retry" bubble.
470
+ removeOutboundSend(id);
457
471
  dispatch(
458
472
  markMessageFailed({
459
473
  kind: 'media',
@@ -661,6 +675,22 @@ export const useSendMessage = (_configOverride?: IConfig) => {
661
675
  async (failedId: string) => {
662
676
  const payload = failedMessages[failedId];
663
677
  if (!payload) {return;}
678
+
679
+ // The server may have accepted the original send even though its
680
+ // echo arrived too late to beat the watchdog — the bubble shows
681
+ // failed, but a confirmed (non-pending) copy is already sitting in
682
+ // the room. Resending here would deliver a genuine duplicate; just
683
+ // clear the failure and let the existing copy stand.
684
+ const roomMsgs = rooms?.[payload.roomJID]?.messages || [];
685
+ const alreadyDelivered = roomMsgs.some(
686
+ (m: any) =>
687
+ !m.pending && (m.id === failedId || m.xmppId === failedId)
688
+ );
689
+ if (alreadyDelivered) {
690
+ dispatch(clearMessageFailure(failedId));
691
+ return;
692
+ }
693
+
664
694
  dispatch(clearMessageFailure(failedId));
665
695
  handleMessageRetry({
666
696
  messageId: failedId,
@@ -690,7 +720,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
690
720
  );
691
721
  }
692
722
  },
693
- [failedMessages, dispatch, handleMessageRetry, sendMessage, sendMedia]
723
+ [failedMessages, rooms, dispatch, handleMessageRetry, sendMessage, sendMedia]
694
724
  );
695
725
 
696
726
  // ChatRoom/ThreadWrapper consume this as the "edit branch" of send.
@@ -11,6 +11,12 @@ interface UnreadMessagesStats {
11
11
  totalCount: number;
12
12
  unreadByRoom: UnreadMessagesMap;
13
13
  isLoading: boolean;
14
+ // True while at least one room's unread count could still be a floor
15
+ // rather than the true total — the history-preload scheduler paged back
16
+ // as far as its catch-up budget allows without finding the read
17
+ // boundary. Lets a host show "99+"-style affordances honestly instead of
18
+ // implying an exact count it can't back up.
19
+ isCapped: boolean;
14
20
  }
15
21
 
16
22
  export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
@@ -35,6 +41,7 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
35
41
  const rooms = roomsState.rooms;
36
42
  const unreadByRoom: UnreadMessagesMap = {};
37
43
  let totalCount = 0;
44
+ let isCapped = false;
38
45
 
39
46
  Object.entries(rooms).forEach(([roomJid, room]: [string, IRoom]) => {
40
47
  const unreadCount = room.unreadMessages || 0;
@@ -42,6 +49,9 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
42
49
  unreadByRoom[roomJid] = unreadCount;
43
50
  totalCount += unreadCount;
44
51
  }
52
+ if ((room as any)?.unreadCapped) {
53
+ isCapped = true;
54
+ }
45
55
  });
46
56
 
47
57
  return {
@@ -49,6 +59,7 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
49
59
  totalCount,
50
60
  unreadByRoom,
51
61
  isLoading: !!roomsState.isUnreadSyncing || !!roomsState.isLoading,
62
+ isCapped,
52
63
  };
53
64
  };
54
65
 
@@ -15,6 +15,8 @@ export type UiStringTable = Record<string, string>;
15
15
 
16
16
  const en: UiStringTable = {
17
17
  'search.placeholder': 'Search...',
18
+ 'search.members': 'Search members',
19
+ 'search.noResults': 'Nobody found',
18
20
  'input.placeholder': 'Type message',
19
21
  'room.created': 'Room created',
20
22
  'room.empty': 'This chat is empty',
@@ -89,6 +91,10 @@ const en: UiStringTable = {
89
91
  'action.add': 'Add',
90
92
  'action.backToCreation': 'Back to creation',
91
93
  'action.report': 'Report',
94
+ 'action.search': 'Search',
95
+ 'action.edit': 'Edit',
96
+ 'action.deleteAndLeave': 'Delete and Leave',
97
+ 'status.banned': 'banned',
92
98
  'status.connecting': 'Connecting…',
93
99
  'status.noInternet': 'No internet connection',
94
100
  'translation.translating': 'Translating…',
@@ -100,7 +106,10 @@ const en: UiStringTable = {
100
106
  'translation.modeManual': 'Manual',
101
107
  'language.select': 'Select language',
102
108
  'modal.newChat.title': 'Create New Chat',
103
- 'modal.newChat.roomNamePlaceholder': 'Enter Room Name',
109
+ 'modal.newChat.roomNamePlaceholder': 'Chat name',
110
+ 'modal.newChat.descriptionPlaceholder': 'Description (optional)',
111
+ 'modal.newChat.createButton': 'Create new chat',
112
+ 'modal.newChat.nameTooShort': 'Chat name must be at least 3 characters.',
104
113
  'modal.newChat.chatTypePlaceholder': 'Select chat type',
105
114
  'modal.newChat.selectUsersTitle': 'Select users to add to Chat',
106
115
  'modal.selectUsers.title': 'Select Users (max 20)',
@@ -142,6 +151,8 @@ const en: UiStringTable = {
142
151
  'action.call': 'Call',
143
152
  'action.audioCall': 'Audio call',
144
153
  'action.copyUserId': 'Copy User Id',
154
+ 'action.account': 'Account',
155
+ 'action.share': 'Share',
145
156
  'action.deleteChat': 'Delete chat',
146
157
  'action.appointAsAdmin': 'Appoint as an admin',
147
158
  'action.moreOptions': 'More Options',
@@ -160,12 +171,21 @@ const en: UiStringTable = {
160
171
  'auth.anonymousLoginMessage': 'You are logged in anonymously!',
161
172
  'modal.profile.title': 'Profile',
162
173
  'modal.profile.about': 'About',
174
+ 'modal.profile.language': 'Language',
175
+ 'modal.profile.media': 'Media',
176
+ 'modal.profile.documents': 'Documents',
177
+ 'modal.profile.noFiles': 'Nothing here yet',
163
178
  'modal.profile.noDescription': 'No description',
164
179
  'modal.chatProfile.title': 'Chat Profile',
165
180
  'modal.chatProfile.description': 'Description',
166
181
  'modal.chatProfile.chatType': 'Chat type',
167
182
  'modal.chatProfile.memberCountSingular': '{count} member',
168
183
  'modal.chatProfile.memberCountPlural': '{count} members',
184
+ 'modal.chatProfile.addMembers': 'Add Members',
185
+ 'modal.chatProfile.removePhoto': 'Remove photo',
186
+ 'modal.chatProfile.membersLabel': 'Members: {count}',
187
+ 'modal.chatProfile.membersUnavailable': 'Member list unavailable',
188
+ 'modal.chatProfile.removeMember': 'Remove from this chat?',
169
189
  'modal.deleteChat.title': 'Delete this chat ?',
170
190
  'modal.addMembers.title': 'Add New Member',
171
191
  'modal.addMembers.userIdPlaceholder': 'Enter User Id',
@@ -223,6 +243,8 @@ const en: UiStringTable = {
223
243
 
224
244
  const fr: UiStringTable = {
225
245
  'search.placeholder': 'Rechercher...',
246
+ 'search.members': 'Rechercher des membres',
247
+ 'search.noResults': 'Personne trouvée',
226
248
  'input.placeholder': 'Écrire un message',
227
249
  'room.created': 'Salon créé',
228
250
  'room.empty': 'Ce salon est vide',
@@ -297,6 +319,10 @@ const fr: UiStringTable = {
297
319
  'action.add': 'Ajouter',
298
320
  'action.backToCreation': 'Retour à la création',
299
321
  'action.report': 'Signaler',
322
+ 'action.search': 'Rechercher',
323
+ 'action.edit': 'Modifier',
324
+ 'action.deleteAndLeave': 'Supprimer et quitter',
325
+ 'status.banned': 'banni',
300
326
  'status.connecting': 'Connexion…',
301
327
  'status.noInternet': 'Pas de connexion internet',
302
328
  'translation.translating': 'Traduction…',
@@ -309,6 +335,9 @@ const fr: UiStringTable = {
309
335
  'language.select': 'Choisir la langue',
310
336
  'modal.newChat.title': 'Créer une discussion',
311
337
  'modal.newChat.roomNamePlaceholder': 'Nom du salon',
338
+ 'modal.newChat.descriptionPlaceholder': 'Description (facultatif)',
339
+ 'modal.newChat.createButton': 'Créer la discussion',
340
+ 'modal.newChat.nameTooShort': 'Le nom doit contenir au moins 3 caractères.',
312
341
  'modal.newChat.chatTypePlaceholder': 'Choisir le type de discussion',
313
342
  'modal.newChat.selectUsersTitle': 'Sélectionner des utilisateurs à ajouter',
314
343
  'modal.selectUsers.title': 'Sélectionner des utilisateurs (max 20)',
@@ -350,6 +379,8 @@ const fr: UiStringTable = {
350
379
  'action.call': 'Appeler',
351
380
  'action.audioCall': 'Appel audio',
352
381
  'action.copyUserId': "Copier l'ID utilisateur",
382
+ 'action.account': 'Compte',
383
+ 'action.share': 'Partager',
353
384
  'action.deleteChat': 'Supprimer la discussion',
354
385
  'action.appointAsAdmin': 'Nommer administrateur',
355
386
  'action.moreOptions': "Plus d'options",
@@ -368,12 +399,21 @@ const fr: UiStringTable = {
368
399
  'auth.anonymousLoginMessage': 'Vous êtes connecté anonymement !',
369
400
  'modal.profile.title': 'Profil',
370
401
  'modal.profile.about': 'À propos',
402
+ 'modal.profile.language': 'Langue',
403
+ 'modal.profile.media': 'Médias',
404
+ 'modal.profile.documents': 'Documents',
405
+ 'modal.profile.noFiles': 'Rien pour le moment',
371
406
  'modal.profile.noDescription': 'Aucune description',
372
407
  'modal.chatProfile.title': 'Profil de la discussion',
373
408
  'modal.chatProfile.description': 'Description',
374
409
  'modal.chatProfile.chatType': 'Type de discussion',
375
410
  'modal.chatProfile.memberCountSingular': '{count} membre',
376
411
  'modal.chatProfile.memberCountPlural': '{count} membres',
412
+ 'modal.chatProfile.addMembers': 'Ajouter des membres',
413
+ 'modal.chatProfile.removePhoto': 'Supprimer la photo',
414
+ 'modal.chatProfile.membersLabel': 'Membres : {count}',
415
+ 'modal.chatProfile.membersUnavailable': 'Liste des membres indisponible',
416
+ 'modal.chatProfile.removeMember': 'Retirer de cette discussion ?',
377
417
  'modal.deleteChat.title': 'Supprimer cette discussion ?',
378
418
  'modal.addMembers.title': 'Ajouter un nouveau membre',
379
419
  'modal.addMembers.userIdPlaceholder': "Entrez l'ID utilisateur",
@@ -431,6 +471,8 @@ const fr: UiStringTable = {
431
471
 
432
472
  const es: UiStringTable = {
433
473
  'search.placeholder': 'Buscar...',
474
+ 'search.members': 'Buscar miembros',
475
+ 'search.noResults': 'No se encontró a nadie',
434
476
  'input.placeholder': 'Escribe un mensaje',
435
477
  'room.created': 'Sala creada',
436
478
  'room.empty': 'Este chat está vacío',
@@ -505,6 +547,10 @@ const es: UiStringTable = {
505
547
  'action.add': 'Añadir',
506
548
  'action.backToCreation': 'Volver a la creación',
507
549
  'action.report': 'Reportar',
550
+ 'action.search': 'Buscar',
551
+ 'action.edit': 'Editar',
552
+ 'action.deleteAndLeave': 'Eliminar y salir',
553
+ 'status.banned': 'bloqueado',
508
554
  'status.connecting': 'Conectando…',
509
555
  'status.noInternet': 'Sin conexión a internet',
510
556
  'translation.translating': 'Traduciendo…',
@@ -517,6 +563,9 @@ const es: UiStringTable = {
517
563
  'language.select': 'Seleccionar idioma',
518
564
  'modal.newChat.title': 'Crear nuevo chat',
519
565
  'modal.newChat.roomNamePlaceholder': 'Nombre de la sala',
566
+ 'modal.newChat.descriptionPlaceholder': 'Descripción (opcional)',
567
+ 'modal.newChat.createButton': 'Crear chat',
568
+ 'modal.newChat.nameTooShort': 'El nombre debe tener al menos 3 caracteres.',
520
569
  'modal.newChat.chatTypePlaceholder': 'Selecciona el tipo de chat',
521
570
  'modal.newChat.selectUsersTitle': 'Selecciona usuarios para añadir al chat',
522
571
  'modal.selectUsers.title': 'Seleccionar usuarios (máx. 20)',
@@ -558,6 +607,8 @@ const es: UiStringTable = {
558
607
  'action.call': 'Llamar',
559
608
  'action.audioCall': 'Llamada de audio',
560
609
  'action.copyUserId': 'Copiar ID de usuario',
610
+ 'action.account': 'Cuenta',
611
+ 'action.share': 'Compartir',
561
612
  'action.deleteChat': 'Eliminar chat',
562
613
  'action.appointAsAdmin': 'Nombrar administrador',
563
614
  'action.moreOptions': 'Más opciones',
@@ -576,12 +627,21 @@ const es: UiStringTable = {
576
627
  'auth.anonymousLoginMessage': '¡Has iniciado sesión de forma anónima!',
577
628
  'modal.profile.title': 'Perfil',
578
629
  'modal.profile.about': 'Acerca de',
630
+ 'modal.profile.language': 'Idioma',
631
+ 'modal.profile.media': 'Multimedia',
632
+ 'modal.profile.documents': 'Documentos',
633
+ 'modal.profile.noFiles': 'Todavía no hay nada',
579
634
  'modal.profile.noDescription': 'Sin descripción',
580
635
  'modal.chatProfile.title': 'Perfil del chat',
581
636
  'modal.chatProfile.description': 'Descripción',
582
637
  'modal.chatProfile.chatType': 'Tipo de chat',
583
638
  'modal.chatProfile.memberCountSingular': '{count} miembro',
584
639
  'modal.chatProfile.memberCountPlural': '{count} miembros',
640
+ 'modal.chatProfile.addMembers': 'Añadir miembros',
641
+ 'modal.chatProfile.removePhoto': 'Quitar la foto',
642
+ 'modal.chatProfile.membersLabel': 'Miembros: {count}',
643
+ 'modal.chatProfile.membersUnavailable': 'Lista de miembros no disponible',
644
+ 'modal.chatProfile.removeMember': '¿Quitar de este chat?',
585
645
  'modal.deleteChat.title': '¿Eliminar este chat?',
586
646
  'modal.addMembers.title': 'Añadir nuevo miembro',
587
647
  'modal.addMembers.userIdPlaceholder': 'Introduce el ID de usuario',
@@ -639,6 +699,8 @@ const es: UiStringTable = {
639
699
 
640
700
  const pt: UiStringTable = {
641
701
  'search.placeholder': 'Pesquisar...',
702
+ 'search.members': 'Pesquisar membros',
703
+ 'search.noResults': 'Ninguém encontrado',
642
704
  'input.placeholder': 'Escreva uma mensagem',
643
705
  'room.created': 'Sala criada',
644
706
  'room.empty': 'Este chat está vazio',
@@ -713,6 +775,10 @@ const pt: UiStringTable = {
713
775
  'action.add': 'Adicionar',
714
776
  'action.backToCreation': 'Voltar à criação',
715
777
  'action.report': 'Denunciar',
778
+ 'action.search': 'Pesquisar',
779
+ 'action.edit': 'Editar',
780
+ 'action.deleteAndLeave': 'Eliminar e sair',
781
+ 'status.banned': 'banido',
716
782
  'status.connecting': 'A ligar…',
717
783
  'status.noInternet': 'Sem ligação à internet',
718
784
  'translation.translating': 'A traduzir…',
@@ -725,6 +791,9 @@ const pt: UiStringTable = {
725
791
  'language.select': 'Selecionar idioma',
726
792
  'modal.newChat.title': 'Criar novo chat',
727
793
  'modal.newChat.roomNamePlaceholder': 'Nome da sala',
794
+ 'modal.newChat.descriptionPlaceholder': 'Descrição (opcional)',
795
+ 'modal.newChat.createButton': 'Criar conversa',
796
+ 'modal.newChat.nameTooShort': 'O nome deve ter pelo menos 3 caracteres.',
728
797
  'modal.newChat.chatTypePlaceholder': 'Selecione o tipo de chat',
729
798
  'modal.newChat.selectUsersTitle': 'Selecione utilizadores para adicionar ao chat',
730
799
  'modal.selectUsers.title': 'Selecionar utilizadores (máx. 20)',
@@ -766,6 +835,8 @@ const pt: UiStringTable = {
766
835
  'action.call': 'Ligar',
767
836
  'action.audioCall': 'Chamada de áudio',
768
837
  'action.copyUserId': 'Copiar ID do utilizador',
838
+ 'action.account': 'Conta',
839
+ 'action.share': 'Partilhar',
769
840
  'action.deleteChat': 'Eliminar chat',
770
841
  'action.appointAsAdmin': 'Nomear administrador',
771
842
  'action.moreOptions': 'Mais opções',
@@ -784,12 +855,21 @@ const pt: UiStringTable = {
784
855
  'auth.anonymousLoginMessage': 'Iniciou sessão anonimamente!',
785
856
  'modal.profile.title': 'Perfil',
786
857
  'modal.profile.about': 'Sobre',
858
+ 'modal.profile.language': 'Idioma',
859
+ 'modal.profile.media': 'Multimédia',
860
+ 'modal.profile.documents': 'Documentos',
861
+ 'modal.profile.noFiles': 'Ainda não há nada aqui',
787
862
  'modal.profile.noDescription': 'Sem descrição',
788
863
  'modal.chatProfile.title': 'Perfil do chat',
789
864
  'modal.chatProfile.description': 'Descrição',
790
865
  'modal.chatProfile.chatType': 'Tipo de chat',
791
866
  'modal.chatProfile.memberCountSingular': '{count} membro',
792
867
  'modal.chatProfile.memberCountPlural': '{count} membros',
868
+ 'modal.chatProfile.addMembers': 'Adicionar membros',
869
+ 'modal.chatProfile.removePhoto': 'Remover foto',
870
+ 'modal.chatProfile.membersLabel': 'Membros: {count}',
871
+ 'modal.chatProfile.membersUnavailable': 'Lista de membros indisponível',
872
+ 'modal.chatProfile.removeMember': 'Remover deste chat?',
793
873
  'modal.deleteChat.title': 'Eliminar este chat?',
794
874
  'modal.addMembers.title': 'Adicionar novo membro',
795
875
  'modal.addMembers.userIdPlaceholder': 'Introduza o ID do utilizador',
@@ -847,6 +927,8 @@ const pt: UiStringTable = {
847
927
 
848
928
  const ht: UiStringTable = {
849
929
  'search.placeholder': 'Chèche...',
930
+ 'search.members': 'Chèche manm',
931
+ 'search.noResults': 'Pa jwenn pèsonn',
850
932
  'input.placeholder': 'Ekri yon mesaj',
851
933
  'room.created': 'Sal la kreye',
852
934
  'room.empty': 'Chat sa a vid',
@@ -921,6 +1003,10 @@ const ht: UiStringTable = {
921
1003
  'action.add': 'Ajoute',
922
1004
  'action.backToCreation': 'Retounen nan kreyasyon',
923
1005
  'action.report': 'Rapòte',
1006
+ 'action.search': 'Chèche',
1007
+ 'action.edit': 'Modifye',
1008
+ 'action.deleteAndLeave': 'Efase epi kite',
1009
+ 'status.banned': 'bloke',
924
1010
  'status.connecting': 'Koneksyon…',
925
1011
  'status.noInternet': 'Pa gen koneksyon entènèt',
926
1012
  'translation.translating': 'Ap tradui…',
@@ -933,6 +1019,9 @@ const ht: UiStringTable = {
933
1019
  'language.select': 'Chwazi lang',
934
1020
  'modal.newChat.title': 'Kreye yon nouvo chat',
935
1021
  'modal.newChat.roomNamePlaceholder': 'Antre non sal la',
1022
+ 'modal.newChat.descriptionPlaceholder': 'Deskripsyon (opsyonèl)',
1023
+ 'modal.newChat.createButton': 'Kreye chat la',
1024
+ 'modal.newChat.nameTooShort': 'Non an dwe gen omwen 3 karaktè.',
936
1025
  'modal.newChat.chatTypePlaceholder': 'Chwazi tip chat la',
937
1026
  'modal.newChat.selectUsersTitle': 'Chwazi itilizatè pou ajoute nan chat la',
938
1027
  'modal.selectUsers.title': 'Chwazi itilizatè (maksimòm 20)',
@@ -974,6 +1063,8 @@ const ht: UiStringTable = {
974
1063
  'action.call': 'Rele',
975
1064
  'action.audioCall': 'Apèl odyo',
976
1065
  'action.copyUserId': 'Kopye ID itilizatè a',
1066
+ 'action.account': 'Kont',
1067
+ 'action.share': 'Pataje',
977
1068
  'action.deleteChat': 'Efase chat la',
978
1069
  'action.appointAsAdmin': 'Nonmen kòm administratè',
979
1070
  'action.moreOptions': 'Plis opsyon',
@@ -992,12 +1083,21 @@ const ht: UiStringTable = {
992
1083
  'auth.anonymousLoginMessage': 'Ou konekte anonim!',
993
1084
  'modal.profile.title': 'Pwofil',
994
1085
  'modal.profile.about': 'Konsènan',
1086
+ 'modal.profile.language': 'Lang',
1087
+ 'modal.profile.media': 'Medya',
1088
+ 'modal.profile.documents': 'Dokiman',
1089
+ 'modal.profile.noFiles': 'Poko gen anyen',
995
1090
  'modal.profile.noDescription': 'Pa gen deskripsyon',
996
1091
  'modal.chatProfile.title': 'Pwofil chat la',
997
1092
  'modal.chatProfile.description': 'Deskripsyon',
998
1093
  'modal.chatProfile.chatType': 'Tip chat',
999
1094
  'modal.chatProfile.memberCountSingular': '{count} manm',
1000
1095
  'modal.chatProfile.memberCountPlural': '{count} manm',
1096
+ 'modal.chatProfile.addMembers': 'Ajoute manm',
1097
+ 'modal.chatProfile.removePhoto': 'Retire foto a',
1098
+ 'modal.chatProfile.membersLabel': 'Manm: {count}',
1099
+ 'modal.chatProfile.membersUnavailable': 'Lis manm yo pa disponib',
1100
+ 'modal.chatProfile.removeMember': 'Retire nan chat sa a?',
1001
1101
  'modal.deleteChat.title': 'Efase chat sa a?',
1002
1102
  'modal.addMembers.title': 'Ajoute yon nouvo manm',
1003
1103
  'modal.addMembers.userIdPlaceholder': 'Antre ID itilizatè a',
@@ -1055,6 +1155,8 @@ const ht: UiStringTable = {
1055
1155
 
1056
1156
  const zh: UiStringTable = {
1057
1157
  'search.placeholder': '搜索...',
1158
+ 'search.members': '搜索成员',
1159
+ 'search.noResults': '未找到成员',
1058
1160
  'input.placeholder': '输入消息',
1059
1161
  'room.created': '聊天室已创建',
1060
1162
  'room.empty': '此聊天为空',
@@ -1129,6 +1231,10 @@ const zh: UiStringTable = {
1129
1231
  'action.add': '添加',
1130
1232
  'action.backToCreation': '返回创建',
1131
1233
  'action.report': '举报',
1234
+ 'action.search': '搜索',
1235
+ 'action.edit': '编辑',
1236
+ 'action.deleteAndLeave': '删除并退出',
1237
+ 'status.banned': '已封禁',
1132
1238
  'status.connecting': '连接中…',
1133
1239
  'status.noInternet': '无网络连接',
1134
1240
  'translation.translating': '翻译中…',
@@ -1141,6 +1247,9 @@ const zh: UiStringTable = {
1141
1247
  'language.select': '选择语言',
1142
1248
  'modal.newChat.title': '创建新聊天',
1143
1249
  'modal.newChat.roomNamePlaceholder': '输入聊天室名称',
1250
+ 'modal.newChat.descriptionPlaceholder': '描述(可选)',
1251
+ 'modal.newChat.createButton': '创建聊天',
1252
+ 'modal.newChat.nameTooShort': '名称至少需要 3 个字符。',
1144
1253
  'modal.newChat.chatTypePlaceholder': '选择聊天类型',
1145
1254
  'modal.newChat.selectUsersTitle': '选择要添加到聊天的用户',
1146
1255
  'modal.selectUsers.title': '选择用户(最多 20 个)',
@@ -1182,6 +1291,8 @@ const zh: UiStringTable = {
1182
1291
  'action.call': '通话',
1183
1292
  'action.audioCall': '语音通话',
1184
1293
  'action.copyUserId': '复制用户 ID',
1294
+ 'action.account': '账户',
1295
+ 'action.share': '分享',
1185
1296
  'action.deleteChat': '删除聊天',
1186
1297
  'action.appointAsAdmin': '设为管理员',
1187
1298
  'action.moreOptions': '更多选项',
@@ -1200,12 +1311,21 @@ const zh: UiStringTable = {
1200
1311
  'auth.anonymousLoginMessage': '您已匿名登录!',
1201
1312
  'modal.profile.title': '个人资料',
1202
1313
  'modal.profile.about': '简介',
1314
+ 'modal.profile.language': '语言',
1315
+ 'modal.profile.media': '媒体',
1316
+ 'modal.profile.documents': '文件',
1317
+ 'modal.profile.noFiles': '这里还没有内容',
1203
1318
  'modal.profile.noDescription': '暂无简介',
1204
1319
  'modal.chatProfile.title': '聊天资料',
1205
1320
  'modal.chatProfile.description': '描述',
1206
1321
  'modal.chatProfile.chatType': '聊天类型',
1207
1322
  'modal.chatProfile.memberCountSingular': '{count} 位成员',
1208
1323
  'modal.chatProfile.memberCountPlural': '{count} 位成员',
1324
+ 'modal.chatProfile.addMembers': '添加成员',
1325
+ 'modal.chatProfile.removePhoto': '移除图片',
1326
+ 'modal.chatProfile.membersLabel': '成员:{count}',
1327
+ 'modal.chatProfile.membersUnavailable': '成员列表不可用',
1328
+ 'modal.chatProfile.removeMember': '将其移出该聊天?',
1209
1329
  'modal.deleteChat.title': '删除此聊天?',
1210
1330
  'modal.addMembers.title': '添加新成员',
1211
1331
  'modal.addMembers.userIdPlaceholder': '输入用户 ID',
@@ -74,3 +74,74 @@ export async function updateProfile(fd: FormData): Promise<{ user: User }> {
74
74
  throw new Error('Error updating profile');
75
75
  }
76
76
  }
77
+
78
+ /** One entry of `GET /v2/files/`, normalised for the UI. */
79
+ export interface UserFile {
80
+ id: string;
81
+ name: string;
82
+ url: string;
83
+ mimetype: string;
84
+ createdAt?: string;
85
+ size?: number;
86
+ }
87
+
88
+ /**
89
+ * Files uploaded by the signed-in user (the JWT owner) — the profile
90
+ * screen's Media and Documents tabs.
91
+ *
92
+ * v2 answers `{ results, pagination }` and, for backward compatibility,
93
+ * also `{ limit, offset, total, items }`; older deployments only have the
94
+ * legacy shape. Read whichever is there instead of betting on one. Field
95
+ * names vary the same way between deployments (`location` vs `locations[]`,
96
+ * `originalname` vs `filename`), so each one falls through its aliases and
97
+ * anything without a usable URL is dropped rather than rendered as a dead
98
+ * row.
99
+ */
100
+ export async function getUserFiles(options?: {
101
+ limit?: number;
102
+ offset?: number;
103
+ }): Promise<UserFile[]> {
104
+ const token = store.getState().chatSettingStore.user.token || '';
105
+ const response = await http.get('/v2/files/', {
106
+ params: {
107
+ limit: options?.limit ?? 100,
108
+ offset: options?.offset ?? 0,
109
+ },
110
+ headers: { Authorization: token },
111
+ });
112
+
113
+ const payload = response?.data ?? {};
114
+ const rows: any[] = payload.results ?? payload.items ?? [];
115
+
116
+ return rows
117
+ .map((row: any): UserFile | null => {
118
+ const url = row?.location || row?.locations?.[0] || row?.url || '';
119
+ if (!url) {return null;}
120
+ return {
121
+ id: String(row?._id ?? row?.id ?? url),
122
+ name:
123
+ row?.originalname ||
124
+ row?.filename ||
125
+ row?.documentName ||
126
+ row?.name ||
127
+ url.split('/').pop() ||
128
+ 'file',
129
+ url,
130
+ mimetype: row?.mimetype || row?.mimeType || row?.type || '',
131
+ createdAt: row?.createdAt || row?.updatedAt,
132
+ size: typeof row?.size === 'number' ? row.size : undefined,
133
+ };
134
+ })
135
+ .filter(Boolean) as UserFile[];
136
+ }
137
+
138
+ /** Images and video go to the Media tab, everything else to Documents. */
139
+ export const isMediaFile = (file: UserFile): boolean => {
140
+ const mime = (file.mimetype || '').toLowerCase();
141
+ if (mime.startsWith('image/') || mime.startsWith('video/')) {return true;}
142
+ if (mime) {return false;}
143
+ // Deployments that don't store a mimetype still name the file.
144
+ return /\.(jpe?g|png|gif|webp|heic|heif|mp4|mov|m4v|3gp|avi|mkv)$/i.test(
145
+ file.name
146
+ );
147
+ };
@@ -12,6 +12,14 @@ interface FlushOpts {
12
12
  // up to now" and stamp it with Date.now() — even if its in-memory
13
13
  // lastViewedTimestamp has not been updated yet.
14
14
  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
20
+ // read on the next login, even though the local count was correct.
21
+ // Customer-reported #33 (server half).
22
+ visibleRoomTs?: number | null;
15
23
  // If true, only write entries for rooms where unreadMessages === 0
16
24
  // (or the visible room). Used at logout time: rooms with outstanding
17
25
  // unread keep their old marker so the next login still surfaces them.
@@ -34,7 +42,7 @@ export async function flushLastViewedToPrivateStore(
34
42
  opts: FlushOpts = {}
35
43
  ): Promise<boolean> {
36
44
  if (!client?.client) {return false;}
37
- const { visibleRoomJID, onlyIfNoUnread } = opts;
45
+ const { visibleRoomJID, visibleRoomTs, onlyIfNoUnread } = opts;
38
46
  const roomList = Object.values(rooms || {});
39
47
  if (roomList.length === 0) {return false;}
40
48
 
@@ -55,10 +63,16 @@ export async function flushLastViewedToPrivateStore(
55
63
  const hasUnread = Number(room.unreadMessages || 0) > 0;
56
64
 
57
65
  // For the visible room, persist "now" — read state is driven by
58
- // room visibility, not by a sentinel timestamp.
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.
59
70
  let ts: number | undefined;
60
71
  if (isVisible) {
61
- ts = nowMs;
72
+ ts =
73
+ typeof visibleRoomTs === 'number' && visibleRoomTs > 0
74
+ ? visibleRoomTs
75
+ : nowMs;
62
76
  } else if (room.lastViewedTimestamp && room.lastViewedTimestamp > 0) {
63
77
  ts = room.lastViewedTimestamp;
64
78
  }
@@ -1039,7 +1039,11 @@ export class XmppClient {
1039
1039
  // src/networking/xmpp/flushLastViewedToPrivateStore.ts for details.
1040
1040
  async flushLastViewedToPrivateStoreStanza(
1041
1041
  rooms: Record<string, any> | null | undefined,
1042
- opts: { visibleRoomJID?: string | null; onlyIfNoUnread?: boolean } = {}
1042
+ opts: {
1043
+ visibleRoomJID?: string | null;
1044
+ visibleRoomTs?: number | null;
1045
+ onlyIfNoUnread?: boolean;
1046
+ } = {}
1043
1047
  ) {
1044
1048
  if (this.disableLastRead) {return false;}
1045
1049
  try {