@ethora/chat-component-rn 26.8.8 → 26.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +6 -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/UserProfileModal/UserProfileModal.js +1 -29
  12. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  13. package/lib/commonjs/components/RoomComponents/SwipeableRoomRow.js +133 -0
  14. package/lib/commonjs/components/RoomComponents/SwipeableRoomRow.js.map +1 -0
  15. package/lib/commonjs/context/xmppProvider.js +15 -3
  16. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  17. package/lib/commonjs/hooks/useChatSettingState.js +2 -0
  18. package/lib/commonjs/hooks/useChatSettingState.js.map +1 -1
  19. package/lib/commonjs/hooks/useConnectionLabel.js +29 -0
  20. package/lib/commonjs/hooks/useConnectionLabel.js.map +1 -0
  21. package/lib/commonjs/i18n/strings.js +12 -0
  22. package/lib/commonjs/i18n/strings.js.map +1 -1
  23. package/lib/commonjs/networking/xmppClient.js +29 -1
  24. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  25. package/lib/commonjs/roomStore/chatSettingsSlice.js +7 -2
  26. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  27. package/lib/commonjs/version.js +1 -1
  28. package/lib/module/components/Header/HeaderRoomList.js +34 -6
  29. package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
  30. package/lib/module/components/MainComponents/ChatHeader.js +15 -1
  31. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  32. package/lib/module/components/MainComponents/RoomList.js +64 -12
  33. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  34. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js +3 -2
  35. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -1
  36. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +2 -30
  37. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  38. package/lib/module/components/RoomComponents/SwipeableRoomRow.js +126 -0
  39. package/lib/module/components/RoomComponents/SwipeableRoomRow.js.map +1 -0
  40. package/lib/module/context/xmppProvider.js +16 -4
  41. package/lib/module/context/xmppProvider.js.map +1 -1
  42. package/lib/module/hooks/useChatSettingState.js +2 -0
  43. package/lib/module/hooks/useChatSettingState.js.map +1 -1
  44. package/lib/module/hooks/useConnectionLabel.js +25 -0
  45. package/lib/module/hooks/useConnectionLabel.js.map +1 -0
  46. package/lib/module/i18n/strings.js +12 -0
  47. package/lib/module/i18n/strings.js.map +1 -1
  48. package/lib/module/networking/xmppClient.js +29 -1
  49. package/lib/module/networking/xmppClient.js.map +1 -1
  50. package/lib/module/roomStore/chatSettingsSlice.js +5 -1
  51. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  52. package/lib/module/version.js +1 -1
  53. package/lib/typescript/components/Header/HeaderRoomList.d.ts +1 -0
  54. package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
  55. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  56. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  57. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts +2 -0
  58. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts.map +1 -1
  59. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
  60. package/lib/typescript/components/RoomComponents/SwipeableRoomRow.d.ts +19 -0
  61. package/lib/typescript/components/RoomComponents/SwipeableRoomRow.d.ts.map +1 -0
  62. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  63. package/lib/typescript/hooks/useChatSettingState.d.ts +1 -0
  64. package/lib/typescript/hooks/useChatSettingState.d.ts.map +1 -1
  65. package/lib/typescript/hooks/useConnectionLabel.d.ts +8 -0
  66. package/lib/typescript/hooks/useConnectionLabel.d.ts.map +1 -0
  67. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  68. package/lib/typescript/networking/xmppClient.d.ts +11 -1
  69. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  70. package/lib/typescript/roomStore/chatSettingsSlice.d.ts +11 -1
  71. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  72. package/lib/typescript/types/types.d.ts +2 -0
  73. package/lib/typescript/types/types.d.ts.map +1 -1
  74. package/lib/typescript/version.d.ts +1 -1
  75. package/package.json +1 -1
  76. package/src/components/Header/HeaderRoomList.tsx +31 -6
  77. package/src/components/MainComponents/ChatHeader.tsx +16 -1
  78. package/src/components/MainComponents/RoomList.tsx +78 -12
  79. package/src/components/Modals/ChatProfileModal/ReportChatModal.tsx +4 -1
  80. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +0 -28
  81. package/src/components/RoomComponents/SwipeableRoomRow.tsx +133 -0
  82. package/src/context/xmppProvider.tsx +21 -4
  83. package/src/hooks/useChatSettingState.tsx +4 -0
  84. package/src/hooks/useConnectionLabel.ts +16 -0
  85. package/src/i18n/strings.ts +12 -0
  86. package/src/networking/xmppClient.ts +33 -1
  87. package/src/roomStore/chatSettingsSlice.ts +17 -0
  88. package/src/types/types.ts +2 -0
  89. package/src/version.ts +1 -1
@@ -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);
@@ -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': '设置',
@@ -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;
@@ -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.8.9';