@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.
- package/CHANGELOG.md +3 -0
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +2 -0
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +9 -1
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/pushPayload.js +71 -0
- package/lib/commonjs/helpers/pushPayload.js.map +1 -0
- package/lib/commonjs/hooks/usePendingNotification.js +8 -11
- package/lib/commonjs/hooks/usePendingNotification.js.map +1 -1
- package/lib/commonjs/main.js +13 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/xmpp/subscribeToRoomMessages.xmpp.js +5 -1
- package/lib/commonjs/networking/xmpp/subscribeToRoomMessages.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +55 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/services/pushSubscriptionService.js +15 -13
- package/lib/commonjs/services/pushSubscriptionService.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +2 -0
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/context/xmppProvider.js +9 -1
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/pushPayload.js +63 -0
- package/lib/module/helpers/pushPayload.js.map +1 -0
- package/lib/module/hooks/usePendingNotification.js +8 -11
- package/lib/module/hooks/usePendingNotification.js.map +1 -1
- package/lib/module/main.js +1 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/xmpp/subscribeToRoomMessages.xmpp.js +5 -1
- package/lib/module/networking/xmpp/subscribeToRoomMessages.xmpp.js.map +1 -1
- package/lib/module/networking/xmppClient.js +55 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/services/pushSubscriptionService.js +15 -13
- package/lib/module/services/pushSubscriptionService.js.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/pushPayload.d.ts +7 -0
- package/lib/typescript/helpers/pushPayload.d.ts.map +1 -0
- package/lib/typescript/hooks/usePendingNotification.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +2 -0
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/subscribeToRoomMessages.xmpp.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +7 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/services/pushSubscriptionService.d.ts +1 -1
- package/lib/typescript/services/pushSubscriptionService.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +1 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/MainComponents/ChatWrapper.tsx +3 -0
- package/src/context/xmppProvider.tsx +9 -1
- package/src/helpers/pushPayload.ts +62 -0
- package/src/hooks/usePendingNotification.ts +5 -11
- package/src/main.ts +3 -0
- package/src/networking/xmpp/subscribeToRoomMessages.xmpp.ts +12 -1
- package/src/networking/xmppClient.ts +58 -1
- package/src/services/pushSubscriptionService.ts +18 -13
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(`
|
|
123
|
+
console.log(`[PushService] MucSub: ${successful} rooms subscribed, ${failed} failed`);
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
async reset(): Promise<void> {
|
package/src/types/types.ts
CHANGED