@ethora/chat-component-rn 26.7.6 → 26.7.7

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 (57) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/lib/commonjs/components/MainComponents/ChatWrapper.js +2 -0
  3. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  4. package/lib/commonjs/context/xmppProvider.js +9 -1
  5. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  6. package/lib/commonjs/helpers/pushPayload.js +71 -0
  7. package/lib/commonjs/helpers/pushPayload.js.map +1 -0
  8. package/lib/commonjs/hooks/usePendingNotification.js +8 -11
  9. package/lib/commonjs/hooks/usePendingNotification.js.map +1 -1
  10. package/lib/commonjs/main.js +13 -0
  11. package/lib/commonjs/main.js.map +1 -1
  12. package/lib/commonjs/networking/xmpp/subscribeToRoomMessages.xmpp.js +5 -1
  13. package/lib/commonjs/networking/xmpp/subscribeToRoomMessages.xmpp.js.map +1 -1
  14. package/lib/commonjs/networking/xmppClient.js +55 -1
  15. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  16. package/lib/commonjs/services/pushSubscriptionService.js +15 -13
  17. package/lib/commonjs/services/pushSubscriptionService.js.map +1 -1
  18. package/lib/module/components/MainComponents/ChatWrapper.js +2 -0
  19. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  20. package/lib/module/context/xmppProvider.js +9 -1
  21. package/lib/module/context/xmppProvider.js.map +1 -1
  22. package/lib/module/helpers/pushPayload.js +63 -0
  23. package/lib/module/helpers/pushPayload.js.map +1 -0
  24. package/lib/module/hooks/usePendingNotification.js +8 -11
  25. package/lib/module/hooks/usePendingNotification.js.map +1 -1
  26. package/lib/module/main.js +1 -0
  27. package/lib/module/main.js.map +1 -1
  28. package/lib/module/networking/xmpp/subscribeToRoomMessages.xmpp.js +5 -1
  29. package/lib/module/networking/xmpp/subscribeToRoomMessages.xmpp.js.map +1 -1
  30. package/lib/module/networking/xmppClient.js +55 -1
  31. package/lib/module/networking/xmppClient.js.map +1 -1
  32. package/lib/module/services/pushSubscriptionService.js +15 -13
  33. package/lib/module/services/pushSubscriptionService.js.map +1 -1
  34. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  35. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  36. package/lib/typescript/helpers/pushPayload.d.ts +7 -0
  37. package/lib/typescript/helpers/pushPayload.d.ts.map +1 -0
  38. package/lib/typescript/hooks/usePendingNotification.d.ts.map +1 -1
  39. package/lib/typescript/main.d.ts +2 -0
  40. package/lib/typescript/main.d.ts.map +1 -1
  41. package/lib/typescript/networking/xmpp/subscribeToRoomMessages.xmpp.d.ts.map +1 -1
  42. package/lib/typescript/networking/xmppClient.d.ts +7 -0
  43. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  44. package/lib/typescript/services/pushSubscriptionService.d.ts +1 -1
  45. package/lib/typescript/services/pushSubscriptionService.d.ts.map +1 -1
  46. package/lib/typescript/types/types.d.ts +1 -0
  47. package/lib/typescript/types/types.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/components/MainComponents/ChatWrapper.tsx +3 -0
  50. package/src/context/xmppProvider.tsx +9 -1
  51. package/src/helpers/pushPayload.ts +62 -0
  52. package/src/hooks/usePendingNotification.ts +5 -11
  53. package/src/main.ts +3 -0
  54. package/src/networking/xmpp/subscribeToRoomMessages.xmpp.ts +12 -1
  55. package/src/networking/xmppClient.ts +58 -1
  56. package/src/services/pushSubscriptionService.ts +18 -13
  57. package/src/types/types.ts +1 -0
@@ -594,9 +594,14 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
594
594
  // be re-marked visible on return. Lives in the effect closure (one per
595
595
  // client) — persists across the listener's many invocations.
596
596
  let visibleBeforeBackground: string | null = null;
597
+ const suspendInBackground = !config?.xmppSettings?.keepAliveInBackground;
597
598
  const sub = AppState.addEventListener('change', (next) => {
598
599
  const c = client;
599
600
  if (next === 'active') {
601
+ if (c?.isSuspended) {
602
+ devPushLog('rn', 'AppState active: resuming suspended XMPP client');
603
+ c.resume();
604
+ }
600
605
  // Returned to foreground. If the socket died while backgrounded
601
606
  // (iOS suspends the WebSocket; long background kills it), the
602
607
  // status may still read 'online' until xmpp.js notices the dead
@@ -624,6 +629,9 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
624
629
  }
625
630
  // Going to background/inactive (sleep / app switcher / tray):
626
631
  if (!c) {return;}
632
+ if (next === 'background' && suspendInBackground) {
633
+ c.suspend().catch(() => undefined);
634
+ }
627
635
  const state = store.getState();
628
636
  const rooms = state.rooms?.rooms;
629
637
  const visibleRoomJID = state.rooms?.visibleRoomJID || null;
@@ -655,7 +663,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
655
663
  );
656
664
  });
657
665
  return () => sub.remove();
658
- }, [client]);
666
+ }, [client, config?.xmppSettings?.keepAliveInBackground]);
659
667
 
660
668
  // -----------------------------------------------------------
661
669
  // Consumer visibility signal (`isVisible` prop). For hosts that keep
@@ -0,0 +1,62 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ import { store } from '../roomStore';
3
+ import {
4
+ clearPendingNotificationJid,
5
+ setCurrentRoom,
6
+ setPendingNotificationJid,
7
+ } from '../roomStore/roomsSlice';
8
+ import { handleCallPush } from './incomingCallFromPush';
9
+ import { normalizeRoomJid } from './normalizeRoomJid';
10
+
11
+ export type PushPayloadOutcome = 'call' | 'room' | 'pending' | 'unknown';
12
+
13
+ export const PENDING_NOTIFICATION_JID_KEY = 'ethora_pending_notification_jid';
14
+ export const PENDING_NOTIFICATION_TTL_MS = 5 * 60 * 1000;
15
+
16
+ export const readPendingJid = async (): Promise<string | null> => {
17
+ try {
18
+ const raw = await AsyncStorage.getItem(PENDING_NOTIFICATION_JID_KEY);
19
+ if (!raw) {return null;}
20
+ if (raw.startsWith('{')) {
21
+ const { jid, at } = JSON.parse(raw) as { jid?: string; at?: number };
22
+ if (!jid || !at || Date.now() - at > PENDING_NOTIFICATION_TTL_MS) {
23
+ await AsyncStorage.removeItem(PENDING_NOTIFICATION_JID_KEY);
24
+ return null;
25
+ }
26
+ return jid;
27
+ }
28
+ return raw;
29
+ } catch {
30
+ return null;
31
+ }
32
+ };
33
+
34
+ export const openRoomFromPush = (jid: string): 'room' | 'pending' => {
35
+ const state = store.getState();
36
+ const conference = state.chatSettingStore?.config?.xmppSettings?.conference;
37
+ const full = normalizeRoomJid(jid, conference);
38
+
39
+ AsyncStorage.setItem(
40
+ PENDING_NOTIFICATION_JID_KEY,
41
+ JSON.stringify({ jid: full, at: Date.now() })
42
+ ).catch(() => undefined);
43
+ if (state.rooms?.rooms?.[full]) {
44
+ store.dispatch(clearPendingNotificationJid());
45
+ store.dispatch(setCurrentRoom({ roomJID: full }));
46
+ return 'room';
47
+ }
48
+ store.dispatch(setPendingNotificationJid(full));
49
+ return 'pending';
50
+ };
51
+
52
+ export const handlePushPayload = (
53
+ data: Record<string, unknown> | null | undefined
54
+ ): PushPayloadOutcome => {
55
+ if (!data) {return 'unknown';}
56
+ if (handleCallPush(data as Record<string, any>)) {return 'call';}
57
+ const jid = data.jid ?? data.chatJid ?? data.roomJid ?? data.roomJID;
58
+ if (typeof jid === 'string' && jid) {
59
+ return openRoomFromPush(jid);
60
+ }
61
+ return 'unknown';
62
+ };
@@ -4,7 +4,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
4
4
  import { RootState } from '../roomStore';
5
5
  import { setCurrentRoom, clearPendingNotificationJid } from '../roomStore/roomsSlice';
6
6
 
7
- const PENDING_NOTIFICATION_JID_KEY = 'ethora_pending_notification_jid';
7
+ import { PENDING_NOTIFICATION_JID_KEY, readPendingJid } from '../helpers/pushPayload';
8
8
 
9
9
  export function usePendingNotification() {
10
10
  const dispatch = useDispatch();
@@ -21,21 +21,15 @@ export function usePendingNotification() {
21
21
  let jidToOpen = pendingNotificationJid;
22
22
 
23
23
  if (!jidToOpen) {
24
- try {
25
- const storedJid = await AsyncStorage.getItem(PENDING_NOTIFICATION_JID_KEY);
26
- if (storedJid) {
27
- jidToOpen = storedJid;
28
- await AsyncStorage.removeItem(PENDING_NOTIFICATION_JID_KEY);
29
- }
30
- } catch (error) {
31
- console.error('Error reading from AsyncStorage:', error);
32
- }
24
+ jidToOpen = await readPendingJid();
33
25
  }
34
26
 
35
27
  if (jidToOpen) {
36
28
  const room = rooms[jidToOpen];
37
29
  if (room) {
38
- return;
30
+ dispatch(clearPendingNotificationJid());
31
+ AsyncStorage.removeItem(PENDING_NOTIFICATION_JID_KEY).catch(() => undefined);
32
+ dispatch(setCurrentRoom({ roomJID: jidToOpen }));
39
33
  }
40
34
  }
41
35
  };
package/src/main.ts CHANGED
@@ -31,6 +31,9 @@ export { XmppProvider } from './context/xmppProvider';
31
31
 
32
32
  // Hooks that consumers usually need (badge counts, logout, push, etc.).
33
33
  export { useUnread } from './hooks/useUnreadMessagesCounter';
34
+
35
+ export { handlePushPayload, openRoomFromPush } from './helpers/pushPayload';
36
+ export type { PushPayloadOutcome } from './helpers/pushPayload';
34
37
  export { useChatRoomFocus } from './hooks/useChatRoomFocus';
35
38
  export { logoutService, useLogout } from './hooks/useLogout';
36
39
  export { useQRCodeChat, handleQRChatId } from './hooks/useQRCodeChatHandler';
@@ -2,12 +2,14 @@ import { Client, xml } from '@xmpp/client';
2
2
  import { Element } from '@xmpp/xml';
3
3
  import { createTimeoutPromise } from './createTimeoutPromise.xmpp';
4
4
 
5
+ let subscribeIdCounter = 0;
6
+
5
7
  export async function subscribeToRoomMessages(
6
8
  client: Client,
7
9
  roomJID: string,
8
10
  userNick?: string
9
11
  ): Promise<boolean> {
10
- const id = `newSubscription:${Date.now().toString()}`;
12
+ const id = `newSubscription:${Date.now().toString(36)}-${(++subscribeIdCounter).toString(36)}`;
11
13
  let stanzaHandler: (stanza: Element) => void;
12
14
 
13
15
  const unsubscribe = () => {
@@ -33,6 +35,15 @@ export async function subscribeToRoomMessages(
33
35
  if (stanza.attrs.type === 'result') {
34
36
  finish(resolve, true);
35
37
  } else if (stanza.attrs.type === 'error') {
38
+ const errorEl = typeof stanza.getChild === 'function' ? stanza.getChild('error') : null;
39
+ const condition = errorEl?.children?.find(
40
+ (c: any) => c?.name && c.name !== 'text'
41
+ ) as Element | undefined;
42
+ console.warn(
43
+ '[PushService] MucSub subscribe error for',
44
+ roomJID,
45
+ condition?.name ?? 'unknown'
46
+ );
36
47
  finish(resolve, false);
37
48
  }
38
49
  }
@@ -342,6 +342,7 @@ export class XmppClient {
342
342
  }
343
343
 
344
344
  initializeClient() {
345
+ this.clientAlreadyStopped = false;
345
346
  try {
346
347
  const devServer = this.devServer || DEFAULT_DEV_SERVER;
347
348
  const url = `wss://${devServer}/ws`;
@@ -752,7 +753,10 @@ export class XmppClient {
752
753
  // `this.client` still points at the old client — i.e. before
753
754
  // initializeClient() reassigns it.
754
755
  const old = this.client;
755
- if (old) {
756
+ if (old && this.clientAlreadyStopped) {
757
+ this.detachEventListeners();
758
+ this.clientAlreadyStopped = false;
759
+ } else if (old) {
756
760
  this.detachEventListeners();
757
761
  // NEVER await a bare stop(): on a half-dead socket (the exact case
758
762
  // we're reconnecting for) @xmpp/client's stop() can hang forever
@@ -767,6 +771,9 @@ export class XmppClient {
767
771
  await withTimeout(old.stop(), STOP_TIMEOUT_MS);
768
772
  }
769
773
  this.initializeClient();
774
+ if (this.suspendedForBackground) {
775
+ await this.stopClientQuietly();
776
+ }
770
777
  } finally {
771
778
  this.reconnecting = false;
772
779
  }
@@ -804,6 +811,56 @@ export class XmppClient {
804
811
  return this.credentialsRefreshInFlight;
805
812
  }
806
813
 
814
+ private suspendedForBackground = false;
815
+ private clientAlreadyStopped = false;
816
+
817
+ private async stopClientQuietly(): Promise<void> {
818
+ const c: any = this.client;
819
+ if (c) {
820
+ const sock = c.socket;
821
+ if (sock && typeof sock.end === 'function') {
822
+ try {
823
+ sock.end();
824
+ } catch {}
825
+ try {
826
+ if (c.status !== 'offline') c._status?.('offline');
827
+ } catch {}
828
+ } else {
829
+ await withTimeout(
830
+ Promise.resolve(c.stop()).catch(() => undefined),
831
+ STOP_TIMEOUT_MS
832
+ );
833
+ }
834
+ this.clientAlreadyStopped = true;
835
+ }
836
+ this.status = 'offline';
837
+ this.presencesReady = false;
838
+ }
839
+
840
+ async suspend(): Promise<void> {
841
+ if (this.suspendedForBackground) {return;}
842
+ this.suspendedForBackground = true;
843
+ this.suppressReconnect = true;
844
+ if (this.reconnectTimer) {
845
+ clearTimeout(this.reconnectTimer);
846
+ this.reconnectTimer = null;
847
+ }
848
+ await this.stopClientQuietly();
849
+ console.log('XMPP suspended (background).');
850
+ }
851
+
852
+ /** Back in the foreground: allow reconnects again and reconnect at once. */
853
+ resume(): void {
854
+ if (!this.suspendedForBackground) {return;}
855
+ this.suspendedForBackground = false;
856
+ this.suppressReconnect = false;
857
+ this.forceReconnect();
858
+ }
859
+
860
+ get isSuspended(): boolean {
861
+ return this.suspendedForBackground;
862
+ }
863
+
807
864
  async disconnect(options?: { suppressReconnect?: boolean }): Promise<void> {
808
865
  if (options?.suppressReconnect) {this.suppressReconnect = true;}
809
866
  return this.close();
@@ -81,7 +81,9 @@ export class PushSubscriptionService {
81
81
  this.subscribedRooms.add(roomJID);
82
82
  await this.saveSubscribedRoomsToStorage();
83
83
  }
84
- console.log('Successfully Subscribed to room', roomJID);
84
+ if (!result) {
85
+ console.warn('[PushService] MucSub subscribe refused for', roomJID);
86
+ }
85
87
  return result;
86
88
  } catch (error) {
87
89
  console.error(`[PushService] Failed to subscribe to room ${roomJID}:`, error);
@@ -92,30 +94,33 @@ export class PushSubscriptionService {
92
94
  async subscribeToRooms(
93
95
  client: Client,
94
96
  roomJIDs: string[],
95
- userNick?: string
97
+ userNick?: string,
98
+ concurrency = 8
96
99
  ): Promise<void> {
97
100
  let successful = 0;
98
101
  let failed = 0;
99
102
 
100
- console.log('test roomJIDs', roomJIDs);
103
+ await this.loadSubscribedRoomsFromStorage();
104
+ const pending = roomJIDs.filter((jid) => !this.subscribedRooms.has(jid));
105
+ if (!pending.length) {
106
+ return;
107
+ }
101
108
 
102
- for (const roomJID of roomJIDs) {
103
- try {
104
- const result = await this.subscribeToRoom(client, roomJID, userNick);
105
- console.log('test roomJID-1', result);
106
- if (result) {
109
+ for (let i = 0; i < pending.length; i += concurrency) {
110
+ const batch = pending.slice(i, i + concurrency);
111
+ const results = await Promise.allSettled(
112
+ batch.map((roomJID) => this.subscribeToRoom(client, roomJID, userNick))
113
+ );
114
+ for (const r of results) {
115
+ if (r.status === 'fulfilled' && r.value) {
107
116
  successful++;
108
117
  } else {
109
118
  failed++;
110
119
  }
111
- await new Promise(resolve => setTimeout(resolve, 100));
112
- } catch (error) {
113
- failed++;
114
- console.error(`Failed to subscribe to room ${roomJID}:`, error);
115
120
  }
116
121
  }
117
122
 
118
- console.log(`Subscribed to ${successful} rooms, ${failed} failed`);
123
+ console.log(`[PushService] MucSub: ${successful} rooms subscribed, ${failed} failed`);
119
124
  }
120
125
 
121
126
  async reset(): Promise<void> {
@@ -151,6 +151,7 @@ export interface xmppSettingsInterface {
151
151
  disableLastRead?: boolean;
152
152
  xmppPingOnSendEnabled?: boolean;
153
153
  historyQoS?: HistoryQoSConfig;
154
+ keepAliveInBackground?: boolean;
154
155
  }
155
156
 
156
157
  export interface InAppNotificationConfig {