@ethora/chat-component-rn 26.8.8 → 26.9.0

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 (127) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/lib/commonjs/components/Header/HeaderRoomList.js +34 -6
  4. package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatHeader.js +15 -1
  6. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/RoomList.js +63 -11
  8. package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
  9. package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js +3 -2
  10. package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -1
  11. package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js +2 -0
  12. package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
  13. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +1 -29
  14. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  15. package/lib/commonjs/components/RoomComponents/SwipeableRoomRow.js +133 -0
  16. package/lib/commonjs/components/RoomComponents/SwipeableRoomRow.js.map +1 -0
  17. package/lib/commonjs/context/xmppProvider.js +15 -3
  18. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  19. package/lib/commonjs/helpers/updateMessagesTillLast.js +8 -0
  20. package/lib/commonjs/helpers/updateMessagesTillLast.js.map +1 -1
  21. package/lib/commonjs/hooks/useChatSettingState.js +2 -0
  22. package/lib/commonjs/hooks/useChatSettingState.js.map +1 -1
  23. package/lib/commonjs/hooks/useConnectionLabel.js +29 -0
  24. package/lib/commonjs/hooks/useConnectionLabel.js.map +1 -0
  25. package/lib/commonjs/i18n/strings.js +12 -0
  26. package/lib/commonjs/i18n/strings.js.map +1 -1
  27. package/lib/commonjs/networking/api-requests/rooms.api.js +32 -7
  28. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  29. package/lib/commonjs/networking/stanzaHandlers.js +12 -0
  30. package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
  31. package/lib/commonjs/networking/xmppClient.js +29 -1
  32. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  33. package/lib/commonjs/roomStore/chatSettingsSlice.js +7 -2
  34. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  35. package/lib/commonjs/roomStore/roomsSlice.js +4 -1
  36. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  37. package/lib/commonjs/services/pushSubscriptionService.js +19 -1
  38. package/lib/commonjs/services/pushSubscriptionService.js.map +1 -1
  39. package/lib/commonjs/version.js +1 -1
  40. package/lib/module/components/Header/HeaderRoomList.js +34 -6
  41. package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
  42. package/lib/module/components/MainComponents/ChatHeader.js +15 -1
  43. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  44. package/lib/module/components/MainComponents/RoomList.js +64 -12
  45. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  46. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js +3 -2
  47. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -1
  48. package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js +2 -0
  49. package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
  50. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +2 -30
  51. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  52. package/lib/module/components/RoomComponents/SwipeableRoomRow.js +126 -0
  53. package/lib/module/components/RoomComponents/SwipeableRoomRow.js.map +1 -0
  54. package/lib/module/context/xmppProvider.js +16 -4
  55. package/lib/module/context/xmppProvider.js.map +1 -1
  56. package/lib/module/helpers/updateMessagesTillLast.js +9 -1
  57. package/lib/module/helpers/updateMessagesTillLast.js.map +1 -1
  58. package/lib/module/hooks/useChatSettingState.js +2 -0
  59. package/lib/module/hooks/useChatSettingState.js.map +1 -1
  60. package/lib/module/hooks/useConnectionLabel.js +25 -0
  61. package/lib/module/hooks/useConnectionLabel.js.map +1 -0
  62. package/lib/module/i18n/strings.js +12 -0
  63. package/lib/module/i18n/strings.js.map +1 -1
  64. package/lib/module/networking/api-requests/rooms.api.js +32 -7
  65. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  66. package/lib/module/networking/stanzaHandlers.js +12 -0
  67. package/lib/module/networking/stanzaHandlers.js.map +1 -1
  68. package/lib/module/networking/xmppClient.js +29 -1
  69. package/lib/module/networking/xmppClient.js.map +1 -1
  70. package/lib/module/roomStore/chatSettingsSlice.js +5 -1
  71. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  72. package/lib/module/roomStore/roomsSlice.js +5 -1
  73. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  74. package/lib/module/services/pushSubscriptionService.js +17 -0
  75. package/lib/module/services/pushSubscriptionService.js.map +1 -1
  76. package/lib/module/version.js +1 -1
  77. package/lib/typescript/components/Header/HeaderRoomList.d.ts +1 -0
  78. package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
  79. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  80. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  81. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts +2 -0
  82. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts.map +1 -1
  83. package/lib/typescript/components/Modals/NewChatModal/UpdNewChatModal.d.ts.map +1 -1
  84. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
  85. package/lib/typescript/components/RoomComponents/SwipeableRoomRow.d.ts +19 -0
  86. package/lib/typescript/components/RoomComponents/SwipeableRoomRow.d.ts.map +1 -0
  87. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  88. package/lib/typescript/helpers/updateMessagesTillLast.d.ts.map +1 -1
  89. package/lib/typescript/hooks/useChatSettingState.d.ts +1 -0
  90. package/lib/typescript/hooks/useChatSettingState.d.ts.map +1 -1
  91. package/lib/typescript/hooks/useConnectionLabel.d.ts +8 -0
  92. package/lib/typescript/hooks/useConnectionLabel.d.ts.map +1 -0
  93. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  94. package/lib/typescript/networking/api-requests/rooms.api.d.ts +11 -1
  95. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  96. package/lib/typescript/networking/stanzaHandlers.d.ts.map +1 -1
  97. package/lib/typescript/networking/xmppClient.d.ts +11 -1
  98. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  99. package/lib/typescript/roomStore/chatSettingsSlice.d.ts +11 -1
  100. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  101. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  102. package/lib/typescript/services/pushSubscriptionService.d.ts +8 -0
  103. package/lib/typescript/services/pushSubscriptionService.d.ts.map +1 -1
  104. package/lib/typescript/types/types.d.ts +2 -0
  105. package/lib/typescript/types/types.d.ts.map +1 -1
  106. package/lib/typescript/version.d.ts +1 -1
  107. package/package.json +1 -1
  108. package/src/components/Header/HeaderRoomList.tsx +31 -6
  109. package/src/components/MainComponents/ChatHeader.tsx +16 -1
  110. package/src/components/MainComponents/RoomList.tsx +78 -12
  111. package/src/components/Modals/ChatProfileModal/ReportChatModal.tsx +4 -1
  112. package/src/components/Modals/NewChatModal/UpdNewChatModal.tsx +2 -0
  113. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +0 -28
  114. package/src/components/RoomComponents/SwipeableRoomRow.tsx +133 -0
  115. package/src/context/xmppProvider.tsx +21 -4
  116. package/src/helpers/updateMessagesTillLast.tsx +8 -1
  117. package/src/hooks/useChatSettingState.tsx +4 -0
  118. package/src/hooks/useConnectionLabel.ts +16 -0
  119. package/src/i18n/strings.ts +12 -0
  120. package/src/networking/api-requests/rooms.api.ts +33 -7
  121. package/src/networking/stanzaHandlers.ts +10 -0
  122. package/src/networking/xmppClient.ts +33 -1
  123. package/src/roomStore/chatSettingsSlice.ts +17 -0
  124. package/src/roomStore/roomsSlice.ts +4 -1
  125. package/src/services/pushSubscriptionService.ts +17 -0
  126. package/src/types/types.ts +2 -0
  127. package/src/version.ts +1 -1
@@ -14,6 +14,7 @@ import { RoomMember } from '../../../types/types';
14
14
  import ModalContent from './ModalContent';
15
15
  import { ModalBackground } from '../styledModalComponents';
16
16
  import { addRoom, setCurrentRoom } from '../../../roomStore/roomsSlice';
17
+ import { subscribeRoomForPush } from '../../../services/pushSubscriptionService';
17
18
 
18
19
  const NewChatModal: React.FC = () => {
19
20
  const config = useSelector(
@@ -101,6 +102,7 @@ const NewChatModal: React.FC = () => {
101
102
  dispatch(addRoom({ roomData: normalizedChat }));
102
103
  dispatch(setCurrentRoom({ roomJID: normalizedChat.jid }));
103
104
  client?.presenceInRoomStanza(normalizedChat.jid);
105
+ subscribeRoomForPush(client, normalizedChat.jid);
104
106
  }
105
107
  };
106
108
 
@@ -16,7 +16,6 @@ import {
16
16
  ChatIcon,
17
17
  EditIcon,
18
18
  IconDoc,
19
- LeaveIcon,
20
19
  LockIcon,
21
20
  LogoutIcon,
22
21
  ProfileIcon,
@@ -541,19 +540,6 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
541
540
  scrollY={scrollY}
542
541
  actions={heroActions}
543
542
  titleStyle={chatTextStyle(config?.typography?.profile?.title)}
544
- titleAccessory={
545
- isOwnProfile ? (
546
- <TouchableOpacity
547
- testID={`${PREFIX}-logout`}
548
- activeOpacity={0.7}
549
- style={styles.leaveButton}
550
- onPress={handleLogout}
551
- >
552
- <LeaveIcon color="#FFFFFF" width={18} height={18} />
553
- <Text style={styles.leaveLabel}>{t('action.leave')}</Text>
554
- </TouchableOpacity>
555
- ) : undefined
556
- }
557
543
  />
558
544
 
559
545
  <View style={styles.body}>
@@ -685,20 +671,6 @@ const createStyles = (theme: ChatTheme) => StyleSheet.create({
685
671
  fontSize: 16,
686
672
  marginTop: 4,
687
673
  },
688
- leaveButton: {
689
- flexDirection: 'row',
690
- alignItems: 'center',
691
- gap: 6,
692
- paddingHorizontal: 12,
693
- paddingVertical: 7,
694
- borderRadius: 18,
695
- backgroundColor: 'rgba(0,0,0,0.35)',
696
- },
697
- leaveLabel: {
698
- color: '#FFFFFF',
699
- fontSize: 14,
700
- fontWeight: '500',
701
- },
702
674
  tabsRow: {
703
675
  flexDirection: 'row',
704
676
  gap: 20,
@@ -0,0 +1,133 @@
1
+ import React, { useCallback, useMemo, useRef } from 'react';
2
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
+ import ReanimatedSwipeable, {
4
+ type SwipeableMethods,
5
+ } from 'react-native-gesture-handler/ReanimatedSwipeable';
6
+ import { LeaveIcon, ReportIcon } from '../../assets/icons';
7
+ import { useTheme } from '../../hooks/useTheme';
8
+ import { useT } from '../../i18n/useT';
9
+ import type { ChatTheme } from '../../theme/theme';
10
+
11
+ /** Width of one action button behind the row. */
12
+ export const ROOM_ACTION_WIDTH = 84;
13
+
14
+ /**
15
+ * Only one row shows its actions at a time, as in the messengers: opening
16
+ * another one, or the list being scrolled, folds the open one back.
17
+ */
18
+ let openRow: SwipeableMethods | null = null;
19
+
20
+ export const closeOpenRoomRow = (): void => {
21
+ openRow?.close();
22
+ openRow = null;
23
+ };
24
+
25
+ interface SwipeableRoomRowProps {
26
+ /** Identifies the row to the host's callbacks. */
27
+ roomJid: string;
28
+ onReport: (roomJid: string) => void;
29
+ onLeave: (roomJid: string) => void;
30
+ children: React.ReactNode;
31
+ }
32
+
33
+ /**
34
+ * A room-list row that slides left to show Report and Leave behind it,
35
+ * the way chats do in the big messengers. A tap on an action folds the
36
+ * row back first, so the list is tidy under whatever the action opens.
37
+ */
38
+ const SwipeableRoomRow: React.FC<SwipeableRoomRowProps> = ({
39
+ roomJid,
40
+ onReport,
41
+ onLeave,
42
+ children,
43
+ }) => {
44
+ const theme = useTheme();
45
+ const t = useT();
46
+ const styles = useMemo(() => createStyles(theme), [theme]);
47
+ const ref = useRef<SwipeableMethods>(null);
48
+
49
+ const onWillOpen = useCallback(() => {
50
+ if (openRow && openRow !== ref.current) {openRow.close();}
51
+ openRow = ref.current;
52
+ }, []);
53
+ const onClose = useCallback(() => {
54
+ if (openRow === ref.current) {openRow = null;}
55
+ }, []);
56
+
57
+ const act = useCallback(
58
+ (what: (jid: string) => void) => {
59
+ ref.current?.close();
60
+ what(roomJid);
61
+ },
62
+ [roomJid]
63
+ );
64
+
65
+ const renderRightActions = useCallback(
66
+ () => (
67
+ <View style={styles.actions}>
68
+ <TouchableOpacity
69
+ testID={`room-action-report-${roomJid.split('@')[0]}`}
70
+ accessibilityRole="button"
71
+ accessibilityLabel={t('action.report')}
72
+ activeOpacity={0.8}
73
+ style={[styles.action, { backgroundColor: theme.textSecondary }]}
74
+ onPress={() => act(onReport)}
75
+ >
76
+ <ReportIcon color={theme.textOnPrimary} width={22} height={22} />
77
+ <Text style={styles.label}>{t('action.report')}</Text>
78
+ </TouchableOpacity>
79
+ <TouchableOpacity
80
+ testID={`room-action-leave-${roomJid.split('@')[0]}`}
81
+ accessibilityRole="button"
82
+ accessibilityLabel={t('action.leave')}
83
+ activeOpacity={0.8}
84
+ style={[styles.action, { backgroundColor: theme.danger }]}
85
+ onPress={() => act(onLeave)}
86
+ >
87
+ <LeaveIcon color={theme.textOnPrimary} width={22} height={22} />
88
+ <Text style={styles.label}>{t('action.leave')}</Text>
89
+ </TouchableOpacity>
90
+ </View>
91
+ ),
92
+ [act, onLeave, onReport, roomJid, styles, t, theme]
93
+ );
94
+
95
+ return (
96
+ <ReanimatedSwipeable
97
+ ref={ref}
98
+ testID={`room-swipe-${roomJid.split('@')[0]}`}
99
+ friction={2}
100
+ rightThreshold={ROOM_ACTION_WIDTH / 2}
101
+ overshootRight={false}
102
+ enableTrackpadTwoFingerGesture
103
+ renderRightActions={renderRightActions}
104
+ onSwipeableWillOpen={onWillOpen}
105
+ onSwipeableClose={onClose}
106
+ // The row must cover the actions until it moves.
107
+ childrenContainerStyle={{ backgroundColor: theme.listBackground }}
108
+ >
109
+ {children}
110
+ </ReanimatedSwipeable>
111
+ );
112
+ };
113
+
114
+ export default SwipeableRoomRow;
115
+
116
+ const createStyles = (theme: ChatTheme) =>
117
+ StyleSheet.create({
118
+ actions: {
119
+ flexDirection: 'row',
120
+ width: ROOM_ACTION_WIDTH * 2,
121
+ },
122
+ action: {
123
+ width: ROOM_ACTION_WIDTH,
124
+ alignItems: 'center',
125
+ justifyContent: 'center',
126
+ gap: 4,
127
+ },
128
+ label: {
129
+ color: theme.textOnPrimary,
130
+ fontSize: 12,
131
+ fontWeight: '600',
132
+ },
133
+ });
@@ -39,7 +39,12 @@ import { getRooms as prefetchRoomsViaRest } from '../networking/api-requests/roo
39
39
  import { allRoomPresences } from '../networking/xmpp/allRoomPresences.xmpp';
40
40
  import { pushSubscriptionService } from '../services/pushSubscriptionService';
41
41
  import { store } from '../roomStore';
42
- import { logout, setStoreClient, setConfig } from '../roomStore/chatSettingsSlice';
42
+ import {
43
+ logout,
44
+ setStoreClient,
45
+ setConfig,
46
+ setConnectionState,
47
+ } from '../roomStore/chatSettingsSlice';
43
48
  import { logoutService } from '../hooks/useLogout';
44
49
  import {
45
50
  setLogoutState,
@@ -260,7 +265,12 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
260
265
  created.setOnOnline(() => {
261
266
  const underlying = (created as any).client;
262
267
  if (!underlying) {return;}
263
- allRoomPresences(underlying)
268
+ // The headers say "Updating…" until the rooms are joined again and
269
+ // the archive caught up - a stream that is up is not yet a chat
270
+ // that is. Same tick as the client's own 'online', so the title
271
+ // never shows the plain state in between.
272
+ store.dispatch(setConnectionState('syncing'));
273
+ const rejoin = allRoomPresences(underlying)
264
274
  .catch((e) =>
265
275
  devPushLog('warn', 'reconnect: allRoomPresences re-join failed', e)
266
276
  )
@@ -268,7 +278,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
268
278
  // Also refresh the private store so unread / lastViewed markers
269
279
  // are accurate after a long reconnect — the MUC re-join above only
270
280
  // restores delivery, not unread state. Idempotent on first connect.
271
- created
281
+ const markers = created
272
282
  .getChatsPrivateStoreRequestStanza()
273
283
  .catch((e: unknown) =>
274
284
  devPushLog('warn', 'reconnect: privateStore refresh failed', e)
@@ -283,9 +293,16 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
283
293
  // point, so this is a no-op until the bootstrap's own history load
284
294
  // populates it. Customer-reported #32.
285
295
  const rooms = store.getState().rooms?.rooms || {};
286
- updateMessagesTillLast(rooms, created).catch((e: unknown) =>
296
+ const catchUp = updateMessagesTillLast(rooms, created).catch((e: unknown) =>
287
297
  devPushLog('warn', 'reconnect: offline catch-up sync failed', e)
288
298
  );
299
+ Promise.allSettled([rejoin, markers, catchUp]).then(() => {
300
+ // Only if this stream is still the live one: a drop meanwhile
301
+ // has already put the headers back to "Connecting…".
302
+ if (created.status === 'online') {
303
+ store.dispatch(setConnectionState('online'));
304
+ }
305
+ });
289
306
  });
290
307
 
291
308
  setClient(created);
@@ -1,6 +1,6 @@
1
1
  import XmppClient from '../networking/xmppClient';
2
2
  import { store } from '../roomStore';
3
- import { setRoomMessages } from '../roomStore/roomsSlice';
3
+ import { addRoomMessages, setRoomMessages } from '../roomStore/roomsSlice';
4
4
  import { IMessage, IRoom } from '../types/types';
5
5
  import { checkUniqueUsers } from './checkUniqueUsers';
6
6
 
@@ -88,6 +88,13 @@ export const updateMessagesTillLast = async (
88
88
  Number(message.id) === Number(lastCachedMessagesTimeStamp)
89
89
  );
90
90
 
91
+ // Merge the page now, whatever comes next: what the cache
92
+ // already holds is skipped, the rest is what arrived while
93
+ // the stream was down.
94
+ store.dispatch(
95
+ addRoomMessages({ roomJID: jid, messages: fetchedMessages })
96
+ );
97
+
91
98
  if (!isMessageFound && !(counter <= maxFetchAttempts - 1)) {
92
99
  // Fire-and-forget — checkUniqueUsers resolves to an
93
100
  // updated RoomMember list, but there's no slice
@@ -12,6 +12,9 @@ export const useChatSettingState = () => {
12
12
  const uiLocale = useSelector(
13
13
  (state: RootState) => state.chatSettingStore.uiLocale
14
14
  );
15
+ const connection = useSelector(
16
+ (state: RootState) => state.chatSettingStore.connection
17
+ );
15
18
  const config = useSelector(
16
19
  (state: RootState) => state.chatSettingStore.config
17
20
  );
@@ -33,6 +36,7 @@ export const useChatSettingState = () => {
33
36
 
34
37
  return {
35
38
  uiLocale,
39
+ connection,
36
40
  user,
37
41
  activeFile,
38
42
  activeModal,
@@ -0,0 +1,16 @@
1
+ import { useChatSettingState } from './useChatSettingState';
2
+ import { useT } from '../i18n/useT';
3
+
4
+ /**
5
+ * What the headers say in place of their usual title while the session is
6
+ * not usable yet: "Connecting…" until the stream is up (first connect and
7
+ * every reconnect), "Updating…" while rooms are re-joined and the archive
8
+ * caught up. Nothing once the chat is live.
9
+ */
10
+ export const useConnectionLabel = (): string | undefined => {
11
+ const { connection } = useChatSettingState();
12
+ const t = useT();
13
+ if (connection === 'connecting') {return t('connection.connecting');}
14
+ if (connection === 'syncing') {return t('connection.updating');}
15
+ return undefined;
16
+ };
@@ -16,6 +16,8 @@ export type UiStringTable = Record<string, string>;
16
16
  const en: UiStringTable = {
17
17
  'search.placeholder': 'Search...',
18
18
  'roomList.title': 'Chats',
19
+ 'connection.connecting': 'Connecting…',
20
+ 'connection.updating': 'Updating…',
19
21
  'menu.newChat': 'New Chat',
20
22
  'menu.profile': 'Profile',
21
23
  'menu.settings': 'Settings',
@@ -335,6 +337,8 @@ const en: UiStringTable = {
335
337
  const fr: UiStringTable = {
336
338
  'search.placeholder': 'Rechercher...',
337
339
  'roomList.title': 'Discussions',
340
+ 'connection.connecting': 'Connexion…',
341
+ 'connection.updating': 'Mise à jour…',
338
342
  'menu.newChat': 'Nouvelle discussion',
339
343
  'menu.profile': 'Profil',
340
344
  'menu.settings': 'Paramètres',
@@ -654,6 +658,8 @@ const fr: UiStringTable = {
654
658
  const es: UiStringTable = {
655
659
  'search.placeholder': 'Buscar...',
656
660
  'roomList.title': 'Conversaciones',
661
+ 'connection.connecting': 'Conectando…',
662
+ 'connection.updating': 'Actualizando…',
657
663
  'menu.newChat': 'Nuevo chat',
658
664
  'menu.profile': 'Perfil',
659
665
  'menu.settings': 'Configuración',
@@ -973,6 +979,8 @@ const es: UiStringTable = {
973
979
  const pt: UiStringTable = {
974
980
  'search.placeholder': 'Pesquisar...',
975
981
  'roomList.title': 'Conversas',
982
+ 'connection.connecting': 'A ligar…',
983
+ 'connection.updating': 'A atualizar…',
976
984
  'menu.newChat': 'Nova conversa',
977
985
  'menu.profile': 'Perfil',
978
986
  'menu.settings': 'Definições',
@@ -1292,6 +1300,8 @@ const pt: UiStringTable = {
1292
1300
  const ht: UiStringTable = {
1293
1301
  'search.placeholder': 'Chèche...',
1294
1302
  'roomList.title': 'Chat yo',
1303
+ 'connection.connecting': 'Ap konekte…',
1304
+ 'connection.updating': 'Ap mete ajou…',
1295
1305
  'menu.newChat': 'Nouvo chat',
1296
1306
  'menu.profile': 'Pwofil',
1297
1307
  'menu.settings': 'Paramèt',
@@ -1611,6 +1621,8 @@ const ht: UiStringTable = {
1611
1621
  const zh: UiStringTable = {
1612
1622
  'search.placeholder': '搜索...',
1613
1623
  'roomList.title': '聊天',
1624
+ 'connection.connecting': '连接中…',
1625
+ 'connection.updating': '更新中…',
1614
1626
  'menu.newChat': '新聊天',
1615
1627
  'menu.profile': '个人资料',
1616
1628
  'menu.settings': '设置',
@@ -212,11 +212,25 @@ function dispatchRoomsFromRestItems(items: ApiRoom[]): void {
212
212
  const ROOM_MEMBERS_TTL_MS = 5 * 60_000;
213
213
  const roomMembersLoads = new Map<string, { at: number; done: Promise<boolean> }>();
214
214
 
215
- export function loadRoomMembers(roomJid: string): Promise<boolean> {
215
+ /**
216
+ * Fetches the room's own REST record (`/chats/my/{name}`) and applies what
217
+ * only the backend knows: the full roster, and whether the room is
218
+ * end-to-end encrypted. Skipped while the roster is already complete,
219
+ * unless `force` - a room that arrived through XMPP (the room list
220
+ * stanza, an invite) was never described by REST at all, so it has no
221
+ * `e2ee` and shows no padlock until this runs.
222
+ */
223
+ export function loadRoomMembers(
224
+ roomJid: string,
225
+ options: { force?: boolean } = {}
226
+ ): Promise<boolean> {
216
227
  const jid = String(roomJid || '').split('/')[0]!;
217
228
  const room = store.getState().rooms.rooms?.[jid];
218
229
  if (!room) {return Promise.resolve(false);}
219
- if ((room.roomMembers?.length ?? 0) >= Number(room.usersCnt || 0)) {
230
+ if (
231
+ !options.force &&
232
+ (room.roomMembers?.length ?? 0) >= Number(room.usersCnt || 0)
233
+ ) {
220
234
  return Promise.resolve(false);
221
235
  }
222
236
  const running = roomMembersLoads.get(jid);
@@ -227,23 +241,35 @@ export function loadRoomMembers(roomJid: string): Promise<boolean> {
227
241
  const response: any = await getRoomByName(jid.split('@')[0]!);
228
242
  const item: ApiRoom = response?.result ?? response;
229
243
  const members = Array.isArray(item?.members) ? item.members : [];
230
- if (members.length === 0) {return false;}
244
+ if (!item || typeof item !== 'object') {return false;}
231
245
  dispatchUsersSetFromRestItems([item]);
232
246
  const current = store.getState().rooms.rooms?.[jid];
233
247
  if (!current) {return false;}
248
+ const e2ee = item.e2ee === true || current.e2ee === true;
234
249
  store.dispatch(
235
250
  updateRoom({
236
251
  jid,
237
252
  updates: {
238
- roomMembers: toRoomMembers(members),
239
- usersCnt: Math.max(memberCount(item), members.length),
240
- ...(current.e2ee
253
+ ...(members.length > 0
254
+ ? {
255
+ roomMembers: toRoomMembers(members),
256
+ usersCnt: Math.max(memberCount(item), members.length),
257
+ }
258
+ : {}),
259
+ ...(e2ee ? { e2ee: true } : {}),
260
+ ...(e2ee && members.length > 0
241
261
  ? { members: toE2eeMembers(members, ownAppId()) }
242
262
  : {}),
263
+ // What the stanza list did not carry.
264
+ ...(!current.title && item.title ? { title: item.title } : {}),
265
+ ...(!current.icon && (item.picture || item.icon)
266
+ ? { icon: item.picture || item.icon }
267
+ : {}),
268
+ ...(!current.type && (item as any).type ? { type: (item as any).type } : {}),
243
269
  },
244
270
  })
245
271
  );
246
- return true;
272
+ return members.length > 0;
247
273
  } catch (error) {
248
274
  roomMembersLoads.delete(jid);
249
275
  console.warn('[ethora-rn] rooms.api: could not load room members', jid, error);
@@ -23,6 +23,8 @@ import { setDeleteModal } from '../roomStore/chatSettingsSlice';
23
23
  import { messageNotificationManager } from '../utils/messageNotificationManager';
24
24
  import { transformCallLogMessage } from '../helpers/callLogMessage';
25
25
  import { translateKey } from '../i18n/strings';
26
+ import { loadRoomMembers } from './api-requests/rooms.api';
27
+ import { subscribeRoomForPush } from '../services/pushSubscriptionService';
26
28
 
27
29
  // TO DO: we are thinking to refactor this code in the following way:
28
30
  // each stanza will be parsed for 'type'
@@ -502,6 +504,14 @@ const onGetChatRooms = (stanza: Element, xmpp: any) => {
502
504
  if (!store.getState().rooms.activeRoomJID) {
503
505
  store.dispatch(setCurrentRoom({ roomJID: fresh[0].jid }));
504
506
  }
507
+ // The stanza says nothing about encryption or the roster: ask REST
508
+ // for each new room, so an encrypted room gets its padlock (and
509
+ // its recipients) without waiting for the next full load.
510
+ for (const room of fresh) {
511
+ loadRoomMembers(room.jid, { force: true }).catch(() => {});
512
+ // ...and pushes for it: the bootstrap's MucSub pass is over.
513
+ subscribeRoomForPush(xmpp, room.jid);
514
+ }
505
515
  }
506
516
  for (const jid of jids) {
507
517
  try {
@@ -29,6 +29,7 @@ import { pushLog as devPushLog, isDevLogActive } from '../utils/devLogger';
29
29
  import { normalizeRoomJid } from '../helpers/normalizeRoomJid';
30
30
  import { store } from '../roomStore';
31
31
  import { applyPrivateStoreMarkers } from '../roomStore/roomsSlice';
32
+ import { setConnectionState } from '../roomStore/chatSettingsSlice';
32
33
  import {
33
34
  clearOutboundSends,
34
35
  enqueueOutboundSend,
@@ -129,7 +130,38 @@ export class XmppClient {
129
130
  service: string = '';
130
131
  conference: string = '';
131
132
  username: string;
132
- status: 'offline' | 'connecting' | 'online' | 'error' = 'offline';
133
+ private _status: 'offline' | 'connecting' | 'online' | 'error' = 'offline';
134
+
135
+ /**
136
+ * The stream's state. Every change is mirrored into the store
137
+ * (`chatSettingStore.connection`) so the headers can say "Connecting…":
138
+ * a class field is nothing React can follow. `offline` and `error` are
139
+ * reported as `connecting` while a retry is scheduled - that is what the
140
+ * user is waiting for - and as `offline` only once nothing will try
141
+ * (logout, unmount).
142
+ */
143
+ get status(): 'offline' | 'connecting' | 'online' | 'error' {
144
+ return this._status;
145
+ }
146
+
147
+ set status(next: 'offline' | 'connecting' | 'online' | 'error') {
148
+ const changed = next !== this._status;
149
+ this._status = next;
150
+ if (!changed) {return;}
151
+ try {
152
+ const reported =
153
+ next === 'online'
154
+ ? 'online'
155
+ : next === 'connecting' || !this.suppressReconnect
156
+ ? 'connecting'
157
+ : 'offline';
158
+ if (store.getState().chatSettingStore.connection !== reported) {
159
+ store.dispatch(setConnectionState(reported));
160
+ }
161
+ } catch {
162
+ /* no store in some test harnesses */
163
+ }
164
+ }
133
165
 
134
166
  password = '';
135
167
  reconnectAttempts = 0;
@@ -44,8 +44,18 @@ export interface ChatState {
44
44
  * `config.i18n.locale`, which is the default for a user who never chose.
45
45
  */
46
46
  uiLocale?: string;
47
+ /**
48
+ * Where the XMPP session stands, for the headers: `connecting` until the
49
+ * stream is up (also while it is being re-established), `syncing` while
50
+ * rooms are re-joined and the archive caught up after a (re)connect,
51
+ * `online` once that is done, `offline` when the stream is down and
52
+ * nothing is trying (logged out).
53
+ */
54
+ connection?: ConnectionState;
47
55
  }
48
56
 
57
+ export type ConnectionState = 'connecting' | 'syncing' | 'online' | 'offline';
58
+
49
59
  export const unpackAndTransform = (input?: User): User => {
50
60
  return {
51
61
  // Was hardcoded to '' — every setUser dropped the user's own bio, so
@@ -231,6 +241,12 @@ const reducers = {
231
241
  ) => {
232
242
  state.uiLocale = action.payload || undefined;
233
243
  },
244
+ setConnectionState: (
245
+ state: WritableDraft<ChatState>,
246
+ action: PayloadAction<ConnectionState>
247
+ ) => {
248
+ state.connection = action.payload;
249
+ },
234
250
  };
235
251
 
236
252
  export const chatSlice: Slice<ChatState, typeof reducers, 'chat'> = createSlice({
@@ -255,6 +271,7 @@ export const {
255
271
  setThemePreference,
256
272
  setPushEnabled,
257
273
  setUiLocale,
274
+ setConnectionState,
258
275
  } = chatSlice.actions;
259
276
 
260
277
  export default chatSlice.reducer;
@@ -4,6 +4,7 @@ import { EditAction, HistoryPreloadState, IMessage, IRoom } from '../types/types
4
4
  import { insertMessageWithDelimiter } from '../helpers/insertMessageWithDelimiter';
5
5
  import { msgSortableMs } from '../helpers/msgSortableMs';
6
6
  import type XmppClient from '../networking/xmppClient';
7
+ import { subscribeRoomForPush } from '../services/pushSubscriptionService';
7
8
 
8
9
  // Per-room runtime message cap. Mirrors the persistence layer's
9
10
  // MESSAGE_LIMIT so what's in memory matches what's on disk; otherwise
@@ -509,11 +510,13 @@ const isValidRoomJid = (jid: unknown): jid is string => {
509
510
  export const addRoomViaApi = createAsyncThunk(
510
511
  'roomMessages/addRoomViaApi',
511
512
  async (
512
- { room, xmpp: _xmpp }: { room: IRoom; xmpp: XmppClient },
513
+ { room, xmpp }: { room: IRoom; xmpp: XmppClient },
513
514
  { dispatch }
514
515
  ) => {
515
516
  if (!room || !room.jid) return;
516
517
  dispatch(roomsStore.actions.addRoomFromApi({ room }));
518
+ // A room made now was not in the bootstrap's MucSub pass.
519
+ subscribeRoomForPush(xmpp, room.jid);
517
520
  }
518
521
  );
519
522
 
@@ -114,3 +114,20 @@ export class PushSubscriptionService {
114
114
 
115
115
  export const pushSubscriptionService = new PushSubscriptionService();
116
116
 
117
+ /**
118
+ * Push for a room that turned up after the bootstrap (created here, an
119
+ * invite, the room list after a reconnect): the bootstrap subscribed only
120
+ * the rooms it had, so without this a new room is silent until the next
121
+ * app start. Best effort, never throws; `wrapper` is the SDK's XmppClient
122
+ * (its `client` the underlying stream).
123
+ */
124
+ export const subscribeRoomForPush = (wrapper: any, roomJid: string): void => {
125
+ const client = wrapper?.client;
126
+ const jid = String(roomJid || '').split('/')[0];
127
+ if (!client || !jid.includes('@')) {return;}
128
+ const nick = client.jid?.getLocal?.();
129
+ pushSubscriptionService
130
+ .subscribeToRoom(client, jid, nick)
131
+ .catch((error) => console.warn('[PushService] late subscribe failed', jid, error));
132
+ };
133
+
@@ -652,6 +652,8 @@ export interface IConfig {
652
652
  };
653
653
  disableProfilesInteractions?: boolean;
654
654
  disableUserCount?: boolean;
655
+ /** No Report / Leave behind a room-list row swiped to the left. */
656
+ disableRoomSwipeActions?: boolean;
655
657
  disableSentLogic?: boolean;
656
658
  disableTypingIndicator?: boolean;
657
659
  // Hide the full-screen "Connection error" overlay (the dark modal with
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/sync-version.mjs on `npm version`.
2
2
  // Edit package.json's version instead of this file.
3
- export const ETHORA_CHAT_COMPONENT_VERSION = '26.8.8';
3
+ export const ETHORA_CHAT_COMPONENT_VERSION = '26.9.0';