@ethora/chat-component-rn 26.10.7 → 26.10.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.
- package/CHANGELOG.md +10 -0
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js +3 -3
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +16 -6
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +16 -2
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/callActions.js +32 -2
- package/lib/commonjs/components/VideoCalls/callActions.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/systemCall.js +83 -0
- package/lib/commonjs/components/VideoCalls/systemCall.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/useCallKeep.js +84 -69
- package/lib/commonjs/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/commonjs/helpers/incomingCallFromPush.js +95 -21
- package/lib/commonjs/helpers/incomingCallFromPush.js.map +1 -1
- package/lib/commonjs/main.js +33 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/callTokenStanza.js +19 -1
- package/lib/commonjs/networking/callTokenStanza.js.map +1 -1
- package/lib/commonjs/networking/xmpp/sendMessageReaction.xmpp.js +22 -6
- package/lib/commonjs/networking/xmpp/sendMessageReaction.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/push/sealedPush.js +13 -2
- package/lib/commonjs/push/sealedPush.js.map +1 -1
- package/lib/commonjs/push/sealedPushTask.js +63 -18
- package/lib/commonjs/push/sealedPushTask.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/components/Menu/HeaderRoomListMenu.js +3 -3
- package/lib/module/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +16 -6
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallOverlay.js +17 -3
- package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/module/components/VideoCalls/callActions.js +29 -0
- package/lib/module/components/VideoCalls/callActions.js.map +1 -1
- package/lib/module/components/VideoCalls/systemCall.js +73 -0
- package/lib/module/components/VideoCalls/systemCall.js.map +1 -0
- package/lib/module/components/VideoCalls/useCallKeep.js +79 -68
- package/lib/module/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/module/helpers/incomingCallFromPush.js +92 -22
- package/lib/module/helpers/incomingCallFromPush.js.map +1 -1
- package/lib/module/main.js +3 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/callTokenStanza.js +19 -1
- package/lib/module/networking/callTokenStanza.js.map +1 -1
- package/lib/module/networking/xmpp/sendMessageReaction.xmpp.js +21 -6
- package/lib/module/networking/xmpp/sendMessageReaction.xmpp.js.map +1 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/push/sealedPush.js +11 -1
- package/lib/module/push/sealedPush.js.map +1 -1
- package/lib/module/push/sealedPushTask.js +62 -18
- package/lib/module/push/sealedPushTask.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/components/Menu/HeaderRoomListMenu.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/callActions.d.ts +6 -0
- package/lib/typescript/components/VideoCalls/callActions.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/systemCall.d.ts +25 -0
- package/lib/typescript/components/VideoCalls/systemCall.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts +8 -23
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts.map +1 -1
- package/lib/typescript/helpers/incomingCallFromPush.d.ts +5 -13
- package/lib/typescript/helpers/incomingCallFromPush.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +6 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/callTokenStanza.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/sendMessageReaction.xmpp.d.ts +13 -1
- package/lib/typescript/networking/xmpp/sendMessageReaction.xmpp.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +2 -4
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/push/sealedPush.d.ts +6 -0
- package/lib/typescript/push/sealedPush.d.ts.map +1 -1
- package/lib/typescript/push/sealedPushTask.d.ts +31 -7
- package/lib/typescript/push/sealedPushTask.d.ts.map +1 -1
- package/lib/typescript/types/models/xmpp.model.d.ts +2 -4
- package/lib/typescript/types/models/xmpp.model.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/notification-service/NotificationService.swift +105 -19
- package/plugins/withNotificationServiceExtension.js +44 -4
- package/src/components/Menu/HeaderRoomListMenu.tsx +13 -8
- package/src/components/MessageBubble/Message.tsx +16 -5
- package/src/components/VideoCalls/VideoCallOverlay.tsx +18 -2
- package/src/components/VideoCalls/callActions.ts +28 -0
- package/src/components/VideoCalls/systemCall.ts +109 -0
- package/src/components/VideoCalls/useCallKeep.ts +91 -101
- package/src/helpers/incomingCallFromPush.ts +97 -26
- package/src/main.ts +9 -1
- package/src/networking/callTokenStanza.ts +20 -1
- package/src/networking/xmpp/sendMessageReaction.xmpp.ts +38 -6
- package/src/networking/xmppClient.ts +2 -2
- package/src/push/sealedPush.ts +11 -0
- package/src/push/sealedPushTask.ts +95 -16
- package/src/types/models/xmpp.model.ts +2 -1
- package/src/version.ts +1 -1
|
@@ -26,6 +26,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
26
26
|
import { RootState } from '../../roomStore';
|
|
27
27
|
import {
|
|
28
28
|
acceptIncomingCall,
|
|
29
|
+
declineIncomingCall,
|
|
29
30
|
resetCall,
|
|
30
31
|
setCallError,
|
|
31
32
|
} from '../../roomStore/callSlice';
|
|
@@ -64,6 +65,12 @@ const ACCEPT = '#10B981';
|
|
|
64
65
|
// PSTN ring window.
|
|
65
66
|
const OUTGOING_CALL_TIMEOUT_MS = 30000;
|
|
66
67
|
|
|
68
|
+
// Stop ringing an incoming call nobody cancelled. The caller gives up after
|
|
69
|
+
// OUTGOING_CALL_TIMEOUT_MS and says so, but that signal is lost when the
|
|
70
|
+
// call came in by push and the socket never came up (the system call
|
|
71
|
+
// screen of a VoIP push would otherwise ring on).
|
|
72
|
+
const INCOMING_CALL_TIMEOUT_MS = 45000;
|
|
73
|
+
|
|
67
74
|
// Ring pattern for an incoming call: wait, buzz, pause, buzz... Android
|
|
68
75
|
// loops this from index 0, iOS ignores the pattern and just vibrates.
|
|
69
76
|
const RING_VIBRATION_PATTERN = [0, 700, 900];
|
|
@@ -507,8 +514,6 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
507
514
|
}, [enabled, canRenderSession, call.token, livekitUrl, call.kind]);
|
|
508
515
|
|
|
509
516
|
// Give up on an unanswered outgoing dial. Unlike web (which only shows
|
|
510
|
-
// the error), also tell the callee, who would otherwise keep ringing:
|
|
511
|
-
// there is no incoming ring timeout on either platform.
|
|
512
517
|
useEffect(() => {
|
|
513
518
|
if (call.phase !== 'requesting' || call.direction !== 'outgoing') {
|
|
514
519
|
return;
|
|
@@ -520,6 +525,17 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
520
525
|
return () => clearTimeout(timer);
|
|
521
526
|
}, [call.phase, call.direction, call.startedAt, dispatch, t]);
|
|
522
527
|
|
|
528
|
+
useEffect(() => {
|
|
529
|
+
if (call.phase !== 'ringing-incoming' || call.direction !== 'incoming') {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const elapsed = Date.now() - (call.startedAt || Date.now());
|
|
533
|
+
const timer = setTimeout(() => {
|
|
534
|
+
dispatch(declineIncomingCall());
|
|
535
|
+
}, Math.max(0, INCOMING_CALL_TIMEOUT_MS - elapsed));
|
|
536
|
+
return () => clearTimeout(timer);
|
|
537
|
+
}, [call.phase, call.direction, call.startedAt, dispatch]);
|
|
538
|
+
|
|
523
539
|
// Buzz while an incoming call is ringing. The ring screen is already on
|
|
524
540
|
// top, so this is the only alert needed when the app is in the
|
|
525
541
|
// foreground; a backgrounded/killed app is handled by the call push.
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
sendCallStateSignal,
|
|
8
8
|
} from '../../networking/callTokenStanza';
|
|
9
9
|
import { translateKey } from '../../i18n/strings';
|
|
10
|
+
import { getGlobalXmppClient } from '../../utils/clientRegistry';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Call teardown that doesn't need a mounted component.
|
|
@@ -51,6 +52,33 @@ export const declineActiveCall = (): void => {
|
|
|
51
52
|
store.dispatch(declineIncomingCall());
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
const DECLINE_SIGNAL_WAIT_MS = 30_000;
|
|
56
|
+
const DECLINE_SIGNAL_POLL_MS = 500;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decline the ringing call, telling the caller as soon as the socket is
|
|
60
|
+
* up. A call declined on the lock screen right after a push woke the app
|
|
61
|
+
* gets here while XMPP is still connecting, and the signal would be lost.
|
|
62
|
+
*/
|
|
63
|
+
export const declineActiveCallWhenOnline = (): void => {
|
|
64
|
+
const call = store.getState().call;
|
|
65
|
+
const signal = {
|
|
66
|
+
peerXmppUsername: call.peerXmppUsername,
|
|
67
|
+
callId: call.callId,
|
|
68
|
+
roomBareName: call.roomBareName,
|
|
69
|
+
};
|
|
70
|
+
store.dispatch(declineIncomingCall());
|
|
71
|
+
const deadline = Date.now() + DECLINE_SIGNAL_WAIT_MS;
|
|
72
|
+
const attempt = () => {
|
|
73
|
+
if (getGlobalXmppClient()?.status === 'online') {
|
|
74
|
+
sendCallStateSignal('declined', signal);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (Date.now() < deadline) {setTimeout(attempt, DECLINE_SIGNAL_POLL_MS);}
|
|
78
|
+
};
|
|
79
|
+
attempt();
|
|
80
|
+
};
|
|
81
|
+
|
|
54
82
|
/** `t` for code outside React, resolved by the same rule as useT. */
|
|
55
83
|
export const translateCallString = (
|
|
56
84
|
key: string,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export interface CallKeepModule {
|
|
5
|
+
setup: (options: any) => Promise<void>;
|
|
6
|
+
displayIncomingCall: (
|
|
7
|
+
uuid: string,
|
|
8
|
+
handle: string,
|
|
9
|
+
localizedCallerName?: string,
|
|
10
|
+
handleType?: string,
|
|
11
|
+
hasVideo?: boolean
|
|
12
|
+
) => void;
|
|
13
|
+
endCall: (uuid: string) => void;
|
|
14
|
+
endAllCalls: () => void;
|
|
15
|
+
startCall?: (
|
|
16
|
+
uuid: string,
|
|
17
|
+
handle: string,
|
|
18
|
+
contactIdentifier?: string,
|
|
19
|
+
handleType?: string,
|
|
20
|
+
hasVideo?: boolean
|
|
21
|
+
) => void;
|
|
22
|
+
answerIncomingCall?: (uuid: string) => void;
|
|
23
|
+
reportConnectingOutgoingCallWithUUID?: (uuid: string) => void;
|
|
24
|
+
reportConnectedOutgoingCallWithUUID?: (uuid: string) => void;
|
|
25
|
+
reportEndCallWithUUID?: (uuid: string, reason: number) => void;
|
|
26
|
+
setMutedCall?: (uuid: string, muted: boolean) => void;
|
|
27
|
+
addEventListener: (event: string, handler: (payload: any) => void) => void;
|
|
28
|
+
removeEventListener: (event: string) => void;
|
|
29
|
+
setAvailable?: (available: boolean) => void;
|
|
30
|
+
backToForeground?: () => void;
|
|
31
|
+
clearInitialEvents?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let cachedCallKeep: CallKeepModule | null | undefined;
|
|
35
|
+
|
|
36
|
+
export const loadCallKeep = (): CallKeepModule | null => {
|
|
37
|
+
if (cachedCallKeep !== undefined) {
|
|
38
|
+
return cachedCallKeep;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const { NativeModules } = require('react-native');
|
|
42
|
+
if (!NativeModules?.RNCallKeep) {
|
|
43
|
+
cachedCallKeep = null;
|
|
44
|
+
return cachedCallKeep;
|
|
45
|
+
}
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const mod = require('react-native-callkeep');
|
|
48
|
+
cachedCallKeep = (mod?.default || mod) as CallKeepModule;
|
|
49
|
+
} catch {
|
|
50
|
+
cachedCallKeep = null;
|
|
51
|
+
}
|
|
52
|
+
return cachedCallKeep;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const callKitUuid = (seed: string): string => {
|
|
56
|
+
let hash = 0x811c9dc5;
|
|
57
|
+
const bytes: number[] = [];
|
|
58
|
+
for (let i = 0; i < 32; i++) {
|
|
59
|
+
const ch = seed.charCodeAt(i % Math.max(1, seed.length)) + i;
|
|
60
|
+
hash ^= ch;
|
|
61
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
62
|
+
bytes.push(hash & 0xff);
|
|
63
|
+
}
|
|
64
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
65
|
+
return [
|
|
66
|
+
hex.slice(0, 8),
|
|
67
|
+
hex.slice(8, 12),
|
|
68
|
+
'4' + hex.slice(13, 16),
|
|
69
|
+
((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16) + hex.slice(17, 20),
|
|
70
|
+
hex.slice(20, 32),
|
|
71
|
+
].join('-');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type SystemCallAction = 'answer' | 'end';
|
|
75
|
+
|
|
76
|
+
const SYSTEM_ACTION_TTL_MS = 2 * 60 * 1000;
|
|
77
|
+
const pendingActions = new Map<string, { action: SystemCallAction; at: number }>();
|
|
78
|
+
|
|
79
|
+
const key = (uuid: string) => uuid.trim().toLowerCase();
|
|
80
|
+
|
|
81
|
+
export const rememberSystemCallAction = (
|
|
82
|
+
uuid: string,
|
|
83
|
+
action: SystemCallAction
|
|
84
|
+
): void => {
|
|
85
|
+
if (!uuid) {return;}
|
|
86
|
+
pendingActions.set(key(uuid), { action, at: Date.now() });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const takeSystemCallAction = (uuid: string): SystemCallAction | null => {
|
|
90
|
+
const entry = pendingActions.get(key(uuid));
|
|
91
|
+
pendingActions.delete(key(uuid));
|
|
92
|
+
if (!entry || Date.now() - entry.at > SYSTEM_ACTION_TTL_MS) {return null;}
|
|
93
|
+
return entry.action;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const endSystemIncomingCall = (callId: string | null | undefined): void => {
|
|
97
|
+
if (Platform.OS !== 'ios' || !callId) {return;}
|
|
98
|
+
const callKeep = loadCallKeep();
|
|
99
|
+
if (!callKeep?.reportEndCallWithUUID) {return;}
|
|
100
|
+
try {
|
|
101
|
+
callKeep.reportEndCallWithUUID(callKitUuid(callId), 3);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const __resetSystemCallForTests = (): void => {
|
|
107
|
+
pendingActions.clear();
|
|
108
|
+
cachedCallKeep = undefined;
|
|
109
|
+
};
|
|
@@ -2,12 +2,17 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
|
4
4
|
import { RootState, store } from '../../roomStore';
|
|
5
|
-
import { acceptIncomingCall
|
|
6
|
-
import {
|
|
7
|
-
import { terminateActiveCall } from './callActions';
|
|
5
|
+
import { acceptIncomingCall } from '../../roomStore/callSlice';
|
|
6
|
+
import { declineActiveCallWhenOnline, terminateActiveCall } from './callActions';
|
|
8
7
|
import { setCallMicEnabled, useCallSession } from './callSessionController';
|
|
9
8
|
import { loadLiveKit } from './livekitRuntime';
|
|
10
9
|
import { setCallSystemRinging } from './callUiStore';
|
|
10
|
+
import {
|
|
11
|
+
callKitUuid,
|
|
12
|
+
loadCallKeep,
|
|
13
|
+
rememberSystemCallAction,
|
|
14
|
+
takeSystemCallAction,
|
|
15
|
+
} from './systemCall';
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Native incoming-call UI (Android ConnectionService / iOS CallKit) via
|
|
@@ -27,95 +32,15 @@ import { setCallSystemRinging } from './callUiStore';
|
|
|
27
32
|
* Platform reality, stated plainly:
|
|
28
33
|
* - Android: full value. A ConnectionService call shows the system
|
|
29
34
|
* incoming-call screen over the lock screen.
|
|
30
|
-
* - iOS: this only
|
|
31
|
-
* a killed app
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* - iOS: this hook only runs while `<Chat>` is mounted. A CallKit screen
|
|
36
|
+
* from a killed app comes from the host: its PushKit handler reports
|
|
37
|
+
* the VoIP push to CallKit natively under `callKitUuid(callId)` and
|
|
38
|
+
* hands the payload to `handlePushPayload`. This hook then adopts that
|
|
39
|
+
* call (same UUID, CallKit answers "CallUUIDAlreadyExists" to our own
|
|
40
|
+
* report) and replays what the user did on it before JS was up.
|
|
35
41
|
*/
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
setup: (options: any) => Promise<void>;
|
|
39
|
-
displayIncomingCall: (
|
|
40
|
-
uuid: string,
|
|
41
|
-
handle: string,
|
|
42
|
-
localizedCallerName?: string,
|
|
43
|
-
handleType?: string,
|
|
44
|
-
hasVideo?: boolean
|
|
45
|
-
) => void;
|
|
46
|
-
endCall: (uuid: string) => void;
|
|
47
|
-
endAllCalls: () => void;
|
|
48
|
-
startCall?: (
|
|
49
|
-
uuid: string,
|
|
50
|
-
handle: string,
|
|
51
|
-
contactIdentifier?: string,
|
|
52
|
-
handleType?: string,
|
|
53
|
-
hasVideo?: boolean
|
|
54
|
-
) => void;
|
|
55
|
-
answerIncomingCall?: (uuid: string) => void;
|
|
56
|
-
reportConnectingOutgoingCallWithUUID?: (uuid: string) => void;
|
|
57
|
-
reportConnectedOutgoingCallWithUUID?: (uuid: string) => void;
|
|
58
|
-
reportEndCallWithUUID?: (uuid: string, reason: number) => void;
|
|
59
|
-
setMutedCall?: (uuid: string, muted: boolean) => void;
|
|
60
|
-
addEventListener: (event: string, handler: (payload: any) => void) => void;
|
|
61
|
-
removeEventListener: (event: string) => void;
|
|
62
|
-
setAvailable?: (available: boolean) => void;
|
|
63
|
-
backToForeground?: () => void;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let cachedCallKeep: CallKeepModule | null | undefined;
|
|
67
|
-
|
|
68
|
-
export const loadCallKeep = (): CallKeepModule | null => {
|
|
69
|
-
if (cachedCallKeep !== undefined) {
|
|
70
|
-
return cachedCallKeep;
|
|
71
|
-
}
|
|
72
|
-
try {
|
|
73
|
-
// The JS package being installed does NOT mean the native side is
|
|
74
|
-
// linked: a host can `npm i react-native-callkeep` and skip
|
|
75
|
-
// `pod install`, or (very common in this repo) node_modules can carry
|
|
76
|
-
// it over from another branch whose build did link it. In that state
|
|
77
|
-
// requiring the package builds a `new NativeEventEmitter(null)` at
|
|
78
|
-
// module scope, which throws an invariant that RN surfaces as an
|
|
79
|
-
// uncaught redbox on every render of the call overlay — the whole app
|
|
80
|
-
// becomes unusable over a feature the host never switched on.
|
|
81
|
-
// Probing NativeModules first keeps the optional dependency genuinely
|
|
82
|
-
// optional: no native module => behave exactly as if it isn't installed.
|
|
83
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
84
|
-
const { NativeModules } = require('react-native');
|
|
85
|
-
if (!NativeModules?.RNCallKeep) {
|
|
86
|
-
cachedCallKeep = null;
|
|
87
|
-
return cachedCallKeep;
|
|
88
|
-
}
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
90
|
-
const mod = require('react-native-callkeep');
|
|
91
|
-
cachedCallKeep = (mod?.default || mod) as CallKeepModule;
|
|
92
|
-
} catch {
|
|
93
|
-
cachedCallKeep = null;
|
|
94
|
-
}
|
|
95
|
-
return cachedCallKeep;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// CallKeep identifies calls by UUID. Our callIds aren't UUIDs, so derive a
|
|
99
|
-
// stable v4-shaped string from the callId: same call always maps to the
|
|
100
|
-
// same UUID, which is what endCall() needs to dismiss the right screen.
|
|
101
|
-
const toUuid = (seed: string): string => {
|
|
102
|
-
let hash = 0x811c9dc5;
|
|
103
|
-
const bytes: number[] = [];
|
|
104
|
-
for (let i = 0; i < 32; i++) {
|
|
105
|
-
const ch = seed.charCodeAt(i % Math.max(1, seed.length)) + i;
|
|
106
|
-
hash ^= ch;
|
|
107
|
-
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
108
|
-
bytes.push(hash & 0xff);
|
|
109
|
-
}
|
|
110
|
-
const hex = bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
111
|
-
return [
|
|
112
|
-
hex.slice(0, 8),
|
|
113
|
-
hex.slice(8, 12),
|
|
114
|
-
'4' + hex.slice(13, 16),
|
|
115
|
-
((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16) + hex.slice(17, 20),
|
|
116
|
-
hex.slice(20, 32),
|
|
117
|
-
].join('-');
|
|
118
|
-
};
|
|
43
|
+
export { loadCallKeep };
|
|
119
44
|
|
|
120
45
|
const trace = (...args: unknown[]) => {
|
|
121
46
|
if (__DEV__) {console.log('[calls] CallKit:', ...args);}
|
|
@@ -192,7 +117,25 @@ export const useCallKeep = (): void => {
|
|
|
192
117
|
useEffect(() => {
|
|
193
118
|
if (!callKeep) {return;}
|
|
194
119
|
|
|
195
|
-
const
|
|
120
|
+
const isOtherCall = (payload: any): boolean => {
|
|
121
|
+
const uuid = String(payload?.callUUID || '').toLowerCase();
|
|
122
|
+
if (!uuid) {return false;}
|
|
123
|
+
const active = activeUuidRef.current?.toLowerCase() || null;
|
|
124
|
+
if (active === uuid) {return false;}
|
|
125
|
+
const current = store.getState().call;
|
|
126
|
+
const ringingUuid =
|
|
127
|
+
current.direction === 'incoming' && current.callId
|
|
128
|
+
? callKitUuid(current.callId)
|
|
129
|
+
: null;
|
|
130
|
+
return ringingUuid !== uuid;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const onAnswer = (payload?: any) => {
|
|
134
|
+
if (isOtherCall(payload)) {
|
|
135
|
+
trace('answer before the call arrived', payload?.callUUID);
|
|
136
|
+
rememberSystemCallAction(String(payload.callUUID), 'answer');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
196
139
|
answeredRef.current = true;
|
|
197
140
|
// The user answered from the system screen, which on Android may
|
|
198
141
|
// mean the app was not even in the foreground.
|
|
@@ -206,15 +149,19 @@ export const useCallKeep = (): void => {
|
|
|
206
149
|
}
|
|
207
150
|
};
|
|
208
151
|
|
|
209
|
-
const onEnd = () => {
|
|
152
|
+
const onEnd = (payload?: any) => {
|
|
153
|
+
if (isOtherCall(payload)) {
|
|
154
|
+
trace('declined before the call arrived', payload?.callUUID);
|
|
155
|
+
rememberSystemCallAction(String(payload.callUUID), 'end');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
210
158
|
const snapshot = store.getState().call;
|
|
211
159
|
if (snapshot.phase === 'idle') {return;}
|
|
212
160
|
if (
|
|
213
161
|
snapshot.direction === 'incoming' &&
|
|
214
162
|
snapshot.phase === 'ringing-incoming'
|
|
215
163
|
) {
|
|
216
|
-
|
|
217
|
-
dispatch(declineIncomingCall());
|
|
164
|
+
declineActiveCallWhenOnline();
|
|
218
165
|
return;
|
|
219
166
|
}
|
|
220
167
|
// Ended from the Dynamic Island, the lock screen or a headset.
|
|
@@ -262,16 +209,46 @@ export const useCallKeep = (): void => {
|
|
|
262
209
|
callKeep.addEventListener('didResetProvider', onReset);
|
|
263
210
|
// The system's verdict on displayIncomingCall ("Unentitled" means the
|
|
264
211
|
// app lacks the voip background mode).
|
|
265
|
-
const onDisplayed = ({
|
|
266
|
-
|
|
212
|
+
const onDisplayed = ({
|
|
213
|
+
error,
|
|
214
|
+
errorCode,
|
|
215
|
+
fromPushKit,
|
|
216
|
+
}: {
|
|
217
|
+
error?: string;
|
|
218
|
+
errorCode?: string;
|
|
219
|
+
fromPushKit?: string;
|
|
220
|
+
}) => {
|
|
221
|
+
trace(
|
|
222
|
+
`incoming call shown${fromPushKit === '1' ? ' (PushKit)' : ''}`,
|
|
223
|
+
error ? `failed: ${errorCode || ''} ${error}` : 'ok'
|
|
224
|
+
);
|
|
225
|
+
if (errorCode === 'CallUUIDAlreadyExists') {return;}
|
|
267
226
|
// CallKit refused it (Do Not Disturb, unentitled...): bring the
|
|
268
|
-
|
|
269
|
-
if (error) {setCallSystemRinging(false);}
|
|
227
|
+
if (error && fromPushKit !== '1') {setCallSystemRinging(false);}
|
|
270
228
|
};
|
|
271
229
|
callKeep.addEventListener('didDisplayIncomingCall', onDisplayed);
|
|
272
230
|
callKeep.addEventListener('didDeactivateAudioSession', onAudioDeactivated);
|
|
273
231
|
|
|
232
|
+
const replay: Record<string, (data: any) => void> = {
|
|
233
|
+
RNCallKeepPerformAnswerCallAction: onAnswer,
|
|
234
|
+
RNCallKeepPerformEndCallAction: onEnd,
|
|
235
|
+
RNCallKeepDidDisplayIncomingCall: onDisplayed,
|
|
236
|
+
RNCallKeepDidActivateAudioSession: onAudioActivated,
|
|
237
|
+
RNCallKeepDidDeactivateAudioSession: onAudioDeactivated,
|
|
238
|
+
RNCallKeepDidPerformSetMutedCallAction: onMuted,
|
|
239
|
+
};
|
|
240
|
+
const onLoadWithEvents = (events: { name: string; data: any }[]) => {
|
|
241
|
+
if (!Array.isArray(events) || events.length === 0) {return;}
|
|
242
|
+
trace('replaying', events.map((e) => e.name).join(', '));
|
|
243
|
+
callKeep.clearInitialEvents?.();
|
|
244
|
+
for (const event of events) {
|
|
245
|
+
replay[event.name]?.(event.data);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
callKeep.addEventListener('didLoadWithEvents', onLoadWithEvents);
|
|
249
|
+
|
|
274
250
|
return () => {
|
|
251
|
+
callKeep.removeEventListener('didLoadWithEvents');
|
|
275
252
|
callKeep.removeEventListener('answerCall');
|
|
276
253
|
callKeep.removeEventListener('endCall');
|
|
277
254
|
callKeep.removeEventListener('didPerformSetMutedCallAction');
|
|
@@ -313,11 +290,23 @@ export const useCallKeep = (): void => {
|
|
|
313
290
|
|
|
314
291
|
if (isRinging) {
|
|
315
292
|
if (activeUuidRef.current) {return;}
|
|
316
|
-
const uuid =
|
|
293
|
+
const uuid = callKitUuid(call.callId || call.roomJid || String(Date.now()));
|
|
317
294
|
activeUuidRef.current = uuid;
|
|
318
295
|
answeredRef.current = false;
|
|
296
|
+
const earlier = takeSystemCallAction(uuid);
|
|
297
|
+
if (earlier === 'answer') {
|
|
298
|
+
trace('answered earlier on the system screen', uuid);
|
|
299
|
+
answeredRef.current = true;
|
|
300
|
+
dispatch(acceptIncomingCall());
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (earlier === 'end') {
|
|
304
|
+
trace('declined earlier on the system screen', uuid);
|
|
305
|
+
activeUuidRef.current = null;
|
|
306
|
+
declineActiveCallWhenOnline();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
319
309
|
// iOS: the system banner is the ring UI (Telegram-style); the in-app
|
|
320
|
-
// card would only cover the screen under it.
|
|
321
310
|
if (isIOS) {setCallSystemRinging(true);}
|
|
322
311
|
try {
|
|
323
312
|
callKeep.displayIncomingCall(
|
|
@@ -339,7 +328,7 @@ export const useCallKeep = (): void => {
|
|
|
339
328
|
if (isIOS && isOutgoing && !activeUuidRef.current && callKeep.startCall) {
|
|
340
329
|
// Unique per call: CallKit refuses a UUID it has seen before, and
|
|
341
330
|
// the callId only arrives with the token.
|
|
342
|
-
const uuid =
|
|
331
|
+
const uuid = callKitUuid(`${call.roomJid || ''}|${call.startedAt || Date.now()}`);
|
|
343
332
|
activeUuidRef.current = uuid;
|
|
344
333
|
outgoingReportedRef.current = 'none';
|
|
345
334
|
trace('startCall', uuid);
|
|
@@ -382,6 +371,7 @@ export const useCallKeep = (): void => {
|
|
|
382
371
|
}, [
|
|
383
372
|
callKeep,
|
|
384
373
|
ready,
|
|
374
|
+
dispatch,
|
|
385
375
|
isIOS,
|
|
386
376
|
isRinging,
|
|
387
377
|
isLive,
|
|
@@ -1,33 +1,61 @@
|
|
|
1
1
|
import { store } from '../roomStore';
|
|
2
|
-
import { setIncomingCallToken } from '../roomStore/callSlice';
|
|
2
|
+
import { acceptIncomingCall, setIncomingCallToken } from '../roomStore/callSlice';
|
|
3
|
+
import { endSystemIncomingCall } from '../components/VideoCalls/systemCall';
|
|
4
|
+
import { declineActiveCallWhenOnline } from '../components/VideoCalls/callActions';
|
|
3
5
|
import { isCallPush, parseCallPush } from './callPush';
|
|
6
|
+
import { normalizeRoomJid } from './normalizeRoomJid';
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
8
|
+
const PENDING_CALL_PUSH_TTL_MS = 60_000;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export type CallPushIntent = 'ring' | 'answer' | 'decline';
|
|
12
|
+
|
|
13
|
+
let pendingPush: { data: Record<string, any>; intent: CallPushIntent; at: number } | null = null;
|
|
14
|
+
let unwatch: (() => void) | null = null;
|
|
15
|
+
|
|
16
|
+
const trace = (...args: unknown[]) => {
|
|
17
|
+
if (__DEV__) {console.log('[calls] push:', ...args);}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const callsConfigured = (): boolean =>
|
|
21
|
+
store.getState().chatSettingStore.config?.videoCalls !== undefined;
|
|
22
|
+
|
|
23
|
+
const holdUntilConfigured = (data: Record<string, any>, intent: CallPushIntent): void => {
|
|
24
|
+
pendingPush = { data, intent, at: Date.now() };
|
|
25
|
+
if (unwatch) {return;}
|
|
26
|
+
unwatch = store.subscribe(() => {
|
|
27
|
+
if (!callsConfigured()) {return;}
|
|
28
|
+
unwatch?.();
|
|
29
|
+
unwatch = null;
|
|
30
|
+
const held = pendingPush;
|
|
31
|
+
pendingPush = null;
|
|
32
|
+
if (!held) {return;}
|
|
33
|
+
if (Date.now() - held.at > PENDING_CALL_PUSH_TTL_MS) {
|
|
34
|
+
trace('held call push expired');
|
|
35
|
+
endSystemIncomingCall(parseCallPush(held.data).callId);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
ringFromPush(held.data, held.intent);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
23
41
|
|
|
42
|
+
const applyIntent = (intent: CallPushIntent): void => {
|
|
43
|
+
const call = store.getState().call;
|
|
44
|
+
if (call.direction !== 'incoming' || call.phase !== 'ringing-incoming') {return;}
|
|
45
|
+
if (intent === 'answer') {store.dispatch(acceptIncomingCall());}
|
|
46
|
+
if (intent === 'decline') {declineActiveCallWhenOnline();}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ringFromPush = (data: Record<string, any>, intent: CallPushIntent): boolean => {
|
|
24
50
|
const parsed = parseCallPush(data);
|
|
25
51
|
const state = store.getState();
|
|
26
52
|
|
|
27
53
|
// Calls off in this deployment: swallow the push rather than ringing a
|
|
28
54
|
// screen the host never opted into.
|
|
29
55
|
if (state.chatSettingStore.config?.videoCalls?.enabled !== true) {
|
|
30
|
-
|
|
56
|
+
trace('calls are off, dropping', parsed.callId);
|
|
57
|
+
endSystemIncomingCall(parsed.callId);
|
|
58
|
+
return false;
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
// Already ringing or in this exact call (the push raced the stanza, or
|
|
@@ -38,9 +66,15 @@ export const handleCallPush = (
|
|
|
38
66
|
const sameCall =
|
|
39
67
|
(!!parsed.callId && parsed.callId === current.callId) ||
|
|
40
68
|
(!!parsed.roomBareName && parsed.roomBareName === current.roomBareName);
|
|
41
|
-
if (sameCall
|
|
69
|
+
if (sameCall) {
|
|
70
|
+
applyIntent(intent);
|
|
42
71
|
return true;
|
|
43
72
|
}
|
|
73
|
+
if (current.phase === 'in-call') {
|
|
74
|
+
// Busy with another call: no second ring.
|
|
75
|
+
endSystemIncomingCall(parsed.callId);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
44
78
|
}
|
|
45
79
|
|
|
46
80
|
// Without a LiveKit token there is nothing to accept: the push is only
|
|
@@ -48,7 +82,8 @@ export const handleCallPush = (
|
|
|
48
82
|
// cannot work, so treat it as handled and let the stanza (once the
|
|
49
83
|
// socket is back up) open the real ring screen.
|
|
50
84
|
if (!parsed.token) {
|
|
51
|
-
|
|
85
|
+
endSystemIncomingCall(parsed.callId);
|
|
86
|
+
return false;
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
// Resolve the full room JID from the local room map. The push carries
|
|
@@ -60,11 +95,16 @@ export const handleCallPush = (
|
|
|
60
95
|
room?.jid === parsed.roomRef
|
|
61
96
|
) as { jid?: string; name?: string; title?: string } | undefined;
|
|
62
97
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
const conference =
|
|
99
|
+
state.chatSettingStore.config?.xmppSettings?.conference || '';
|
|
100
|
+
const fromRef = parsed.roomRef
|
|
101
|
+
? normalizeRoomJid(parsed.roomRef, conference)
|
|
102
|
+
: '';
|
|
103
|
+
const roomJid = resolved?.jid || (fromRef.includes('@') ? fromRef : '');
|
|
66
104
|
if (!roomJid) {
|
|
67
|
-
|
|
105
|
+
trace('no room for call push', parsed.roomRef);
|
|
106
|
+
endSystemIncomingCall(parsed.callId);
|
|
107
|
+
return false;
|
|
68
108
|
}
|
|
69
109
|
|
|
70
110
|
store.dispatch(
|
|
@@ -78,6 +118,37 @@ export const handleCallPush = (
|
|
|
78
118
|
peerXmppUsername: parsed.callerXmppUsername,
|
|
79
119
|
})
|
|
80
120
|
);
|
|
121
|
+
trace('ringing', parsed.callId, intent);
|
|
122
|
+
applyIntent(intent);
|
|
123
|
+
return true;
|
|
124
|
+
};
|
|
81
125
|
|
|
126
|
+
export const handleCallPush = (
|
|
127
|
+
data: Record<string, any> | null | undefined,
|
|
128
|
+
intent: CallPushIntent = 'ring'
|
|
129
|
+
): boolean => {
|
|
130
|
+
if (!isCallPush(data)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (!callsConfigured()) {
|
|
134
|
+
trace('holding call push until the chat is configured');
|
|
135
|
+
holdUntilConfigured(data as Record<string, any>, intent);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
ringFromPush(data as Record<string, any>, intent);
|
|
82
139
|
return true;
|
|
83
140
|
};
|
|
141
|
+
|
|
142
|
+
export const answerCallFromPush = (
|
|
143
|
+
data: Record<string, any> | null | undefined
|
|
144
|
+
): boolean => handleCallPush(data, 'answer');
|
|
145
|
+
|
|
146
|
+
export const declineCallFromPush = (
|
|
147
|
+
data: Record<string, any> | null | undefined
|
|
148
|
+
): boolean => handleCallPush(data, 'decline');
|
|
149
|
+
|
|
150
|
+
export const __resetCallPushForTests = (): void => {
|
|
151
|
+
unwatch?.();
|
|
152
|
+
unwatch = null;
|
|
153
|
+
pendingPush = null;
|
|
154
|
+
};
|
package/src/main.ts
CHANGED
|
@@ -33,6 +33,14 @@ export { XmppProvider } from './context/xmppProvider';
|
|
|
33
33
|
export { useUnread } from './hooks/useUnreadMessagesCounter';
|
|
34
34
|
|
|
35
35
|
export { handlePushPayload, openRoomFromPush } from './helpers/pushPayload';
|
|
36
|
+
export { callKitUuid } from './components/VideoCalls/systemCall';
|
|
37
|
+
export {
|
|
38
|
+
answerCallFromPush,
|
|
39
|
+
declineCallFromPush,
|
|
40
|
+
} from './helpers/incomingCallFromPush';
|
|
41
|
+
export type { CallPushIntent } from './helpers/incomingCallFromPush';
|
|
42
|
+
export { isCallPush, parseCallPush } from './helpers/callPush';
|
|
43
|
+
export type { ParsedCallPush } from './helpers/callPush';
|
|
36
44
|
export type { PushPayloadOutcome } from './helpers/pushPayload';
|
|
37
45
|
export {
|
|
38
46
|
registerPushToken,
|
|
@@ -73,7 +81,7 @@ export {
|
|
|
73
81
|
presentSealedPush,
|
|
74
82
|
SEALED_PUSH_TASK,
|
|
75
83
|
} from './push/sealedPushTask';
|
|
76
|
-
export type { SealedPushTaskOptions } from './push/sealedPushTask';
|
|
84
|
+
export type { SealedPushTaskOptions, ShownMessagePush } from './push/sealedPushTask';
|
|
77
85
|
export {
|
|
78
86
|
openSealedPush,
|
|
79
87
|
isSealedPush,
|