@ethora/chat-component-rn 26.10.7 → 26.10.8
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 +4 -0
- 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 +83 -66
- 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 +6 -0
- 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 +43 -17
- package/lib/commonjs/push/sealedPushTask.js.map +1 -1
- package/lib/commonjs/version.js +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 +77 -64
- 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 +6 -0
- 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 +42 -17
- package/lib/module/push/sealedPushTask.js.map +1 -1
- package/lib/module/version.js +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 +5 -0
- 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 +18 -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/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 +89 -97
- package/src/helpers/incomingCallFromPush.ts +97 -26
- package/src/main.ts +8 -0
- package/src/networking/callTokenStanza.ts +10 -0
- 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 +61 -15
- package/src/types/models/xmpp.model.ts +2 -1
- package/src/version.ts +1 -1
|
@@ -4,10 +4,16 @@ import { useDispatch, useSelector } from 'react-redux';
|
|
|
4
4
|
import { RootState, store } from '../../roomStore';
|
|
5
5
|
import { acceptIncomingCall, declineIncomingCall } from '../../roomStore/callSlice';
|
|
6
6
|
import { sendCallStateSignal } from '../../networking/callTokenStanza';
|
|
7
|
-
import { terminateActiveCall } from './callActions';
|
|
7
|
+
import { declineActiveCallWhenOnline, terminateActiveCall } from './callActions';
|
|
8
8
|
import { setCallMicEnabled, useCallSession } from './callSessionController';
|
|
9
9
|
import { loadLiveKit } from './livekitRuntime';
|
|
10
10
|
import { setCallSystemRinging } from './callUiStore';
|
|
11
|
+
import {
|
|
12
|
+
callKitUuid,
|
|
13
|
+
loadCallKeep,
|
|
14
|
+
rememberSystemCallAction,
|
|
15
|
+
takeSystemCallAction,
|
|
16
|
+
} from './systemCall';
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Native incoming-call UI (Android ConnectionService / iOS CallKit) via
|
|
@@ -27,95 +33,15 @@ import { setCallSystemRinging } from './callUiStore';
|
|
|
27
33
|
* Platform reality, stated plainly:
|
|
28
34
|
* - Android: full value. A ConnectionService call shows the system
|
|
29
35
|
* incoming-call screen over the lock screen.
|
|
30
|
-
* - iOS: this only
|
|
31
|
-
* a killed app
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
36
|
+
* - iOS: this hook only runs while `<Chat>` is mounted. A CallKit screen
|
|
37
|
+
* from a killed app comes from the host: its PushKit handler reports
|
|
38
|
+
* the VoIP push to CallKit natively under `callKitUuid(callId)` and
|
|
39
|
+
* hands the payload to `handlePushPayload`. This hook then adopts that
|
|
40
|
+
* call (same UUID, CallKit answers "CallUUIDAlreadyExists" to our own
|
|
41
|
+
* report) and replays what the user did on it before JS was up.
|
|
35
42
|
*/
|
|
36
43
|
|
|
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
|
-
};
|
|
44
|
+
export { loadCallKeep };
|
|
119
45
|
|
|
120
46
|
const trace = (...args: unknown[]) => {
|
|
121
47
|
if (__DEV__) {console.log('[calls] CallKit:', ...args);}
|
|
@@ -192,7 +118,25 @@ export const useCallKeep = (): void => {
|
|
|
192
118
|
useEffect(() => {
|
|
193
119
|
if (!callKeep) {return;}
|
|
194
120
|
|
|
195
|
-
const
|
|
121
|
+
const isOtherCall = (payload: any): boolean => {
|
|
122
|
+
const uuid = String(payload?.callUUID || '').toLowerCase();
|
|
123
|
+
if (!uuid) {return false;}
|
|
124
|
+
const active = activeUuidRef.current?.toLowerCase() || null;
|
|
125
|
+
if (active === uuid) {return false;}
|
|
126
|
+
const current = store.getState().call;
|
|
127
|
+
const ringingUuid =
|
|
128
|
+
current.direction === 'incoming' && current.callId
|
|
129
|
+
? callKitUuid(current.callId)
|
|
130
|
+
: null;
|
|
131
|
+
return ringingUuid !== uuid;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const onAnswer = (payload?: any) => {
|
|
135
|
+
if (isOtherCall(payload)) {
|
|
136
|
+
trace('answer before the call arrived', payload?.callUUID);
|
|
137
|
+
rememberSystemCallAction(String(payload.callUUID), 'answer');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
196
140
|
answeredRef.current = true;
|
|
197
141
|
// The user answered from the system screen, which on Android may
|
|
198
142
|
// mean the app was not even in the foreground.
|
|
@@ -206,7 +150,12 @@ export const useCallKeep = (): void => {
|
|
|
206
150
|
}
|
|
207
151
|
};
|
|
208
152
|
|
|
209
|
-
const onEnd = () => {
|
|
153
|
+
const onEnd = (payload?: any) => {
|
|
154
|
+
if (isOtherCall(payload)) {
|
|
155
|
+
trace('declined before the call arrived', payload?.callUUID);
|
|
156
|
+
rememberSystemCallAction(String(payload.callUUID), 'end');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
210
159
|
const snapshot = store.getState().call;
|
|
211
160
|
if (snapshot.phase === 'idle') {return;}
|
|
212
161
|
if (
|
|
@@ -262,16 +211,46 @@ export const useCallKeep = (): void => {
|
|
|
262
211
|
callKeep.addEventListener('didResetProvider', onReset);
|
|
263
212
|
// The system's verdict on displayIncomingCall ("Unentitled" means the
|
|
264
213
|
// app lacks the voip background mode).
|
|
265
|
-
const onDisplayed = ({
|
|
266
|
-
|
|
214
|
+
const onDisplayed = ({
|
|
215
|
+
error,
|
|
216
|
+
errorCode,
|
|
217
|
+
fromPushKit,
|
|
218
|
+
}: {
|
|
219
|
+
error?: string;
|
|
220
|
+
errorCode?: string;
|
|
221
|
+
fromPushKit?: string;
|
|
222
|
+
}) => {
|
|
223
|
+
trace(
|
|
224
|
+
`incoming call shown${fromPushKit === '1' ? ' (PushKit)' : ''}`,
|
|
225
|
+
error ? `failed: ${errorCode || ''} ${error}` : 'ok'
|
|
226
|
+
);
|
|
227
|
+
if (errorCode === 'CallUUIDAlreadyExists') {return;}
|
|
267
228
|
// CallKit refused it (Do Not Disturb, unentitled...): bring the
|
|
268
|
-
|
|
269
|
-
if (error) {setCallSystemRinging(false);}
|
|
229
|
+
if (error && fromPushKit !== '1') {setCallSystemRinging(false);}
|
|
270
230
|
};
|
|
271
231
|
callKeep.addEventListener('didDisplayIncomingCall', onDisplayed);
|
|
272
232
|
callKeep.addEventListener('didDeactivateAudioSession', onAudioDeactivated);
|
|
273
233
|
|
|
234
|
+
const replay: Record<string, (data: any) => void> = {
|
|
235
|
+
RNCallKeepPerformAnswerCallAction: onAnswer,
|
|
236
|
+
RNCallKeepPerformEndCallAction: onEnd,
|
|
237
|
+
RNCallKeepDidDisplayIncomingCall: onDisplayed,
|
|
238
|
+
RNCallKeepDidActivateAudioSession: onAudioActivated,
|
|
239
|
+
RNCallKeepDidDeactivateAudioSession: onAudioDeactivated,
|
|
240
|
+
RNCallKeepDidPerformSetMutedCallAction: onMuted,
|
|
241
|
+
};
|
|
242
|
+
const onLoadWithEvents = (events: { name: string; data: any }[]) => {
|
|
243
|
+
if (!Array.isArray(events) || events.length === 0) {return;}
|
|
244
|
+
trace('replaying', events.map((e) => e.name).join(', '));
|
|
245
|
+
callKeep.clearInitialEvents?.();
|
|
246
|
+
for (const event of events) {
|
|
247
|
+
replay[event.name]?.(event.data);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
callKeep.addEventListener('didLoadWithEvents', onLoadWithEvents);
|
|
251
|
+
|
|
274
252
|
return () => {
|
|
253
|
+
callKeep.removeEventListener('didLoadWithEvents');
|
|
275
254
|
callKeep.removeEventListener('answerCall');
|
|
276
255
|
callKeep.removeEventListener('endCall');
|
|
277
256
|
callKeep.removeEventListener('didPerformSetMutedCallAction');
|
|
@@ -313,11 +292,23 @@ export const useCallKeep = (): void => {
|
|
|
313
292
|
|
|
314
293
|
if (isRinging) {
|
|
315
294
|
if (activeUuidRef.current) {return;}
|
|
316
|
-
const uuid =
|
|
295
|
+
const uuid = callKitUuid(call.callId || call.roomJid || String(Date.now()));
|
|
317
296
|
activeUuidRef.current = uuid;
|
|
318
297
|
answeredRef.current = false;
|
|
298
|
+
const earlier = takeSystemCallAction(uuid);
|
|
299
|
+
if (earlier === 'answer') {
|
|
300
|
+
trace('answered earlier on the system screen', uuid);
|
|
301
|
+
answeredRef.current = true;
|
|
302
|
+
dispatch(acceptIncomingCall());
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (earlier === 'end') {
|
|
306
|
+
trace('declined earlier on the system screen', uuid);
|
|
307
|
+
activeUuidRef.current = null;
|
|
308
|
+
declineActiveCallWhenOnline();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
319
311
|
// iOS: the system banner is the ring UI (Telegram-style); the in-app
|
|
320
|
-
// card would only cover the screen under it.
|
|
321
312
|
if (isIOS) {setCallSystemRinging(true);}
|
|
322
313
|
try {
|
|
323
314
|
callKeep.displayIncomingCall(
|
|
@@ -339,7 +330,7 @@ export const useCallKeep = (): void => {
|
|
|
339
330
|
if (isIOS && isOutgoing && !activeUuidRef.current && callKeep.startCall) {
|
|
340
331
|
// Unique per call: CallKit refuses a UUID it has seen before, and
|
|
341
332
|
// the callId only arrives with the token.
|
|
342
|
-
const uuid =
|
|
333
|
+
const uuid = callKitUuid(`${call.roomJid || ''}|${call.startedAt || Date.now()}`);
|
|
343
334
|
activeUuidRef.current = uuid;
|
|
344
335
|
outgoingReportedRef.current = 'none';
|
|
345
336
|
trace('startCall', uuid);
|
|
@@ -382,6 +373,7 @@ export const useCallKeep = (): void => {
|
|
|
382
373
|
}, [
|
|
383
374
|
callKeep,
|
|
384
375
|
ready,
|
|
376
|
+
dispatch,
|
|
385
377
|
isIOS,
|
|
386
378
|
isRinging,
|
|
387
379
|
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,
|
|
@@ -528,6 +528,16 @@ export const onCallTokenMessage = (stanza: Element): boolean => {
|
|
|
528
528
|
return true;
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
+
if (
|
|
532
|
+
callState.direction === 'incoming' &&
|
|
533
|
+
callState.phase !== 'idle' &&
|
|
534
|
+
!!callId &&
|
|
535
|
+
callState.callId === callId
|
|
536
|
+
) {
|
|
537
|
+
callTrace('drop call-token: call already known from push', { callId });
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
|
|
531
541
|
store.dispatch(
|
|
532
542
|
setIncomingCallToken({
|
|
533
543
|
roomJid,
|
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
import { Client, xml } from '@xmpp/client';
|
|
2
|
+
import { shouldEncrypt } from '../../e2ee/send';
|
|
3
|
+
|
|
4
|
+
export interface ReactionStanzaData {
|
|
5
|
+
firstName?: string;
|
|
6
|
+
lastName?: string;
|
|
7
|
+
photoURL?: string;
|
|
8
|
+
push?: boolean;
|
|
9
|
+
messageAuthor?: string;
|
|
10
|
+
messageText?: string;
|
|
11
|
+
isMediafile?: string;
|
|
12
|
+
mimetype?: string;
|
|
13
|
+
fileName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const REACTION_MESSAGE_TEXT_MAX = 120;
|
|
2
17
|
|
|
3
18
|
export function sendMessageReaction(
|
|
4
19
|
client: Client,
|
|
5
20
|
messageId: string,
|
|
6
21
|
roomJid: string,
|
|
7
22
|
reactionsList: string[],
|
|
8
|
-
data:
|
|
9
|
-
reactionSymbol?: any,
|
|
23
|
+
data: ReactionStanzaData = {}
|
|
10
24
|
) {
|
|
11
25
|
const id = `message-reaction:${Date.now().toString()}`;
|
|
12
26
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
const push = Boolean(data.push) && reactionsList.length > 0;
|
|
28
|
+
const messageText =
|
|
29
|
+
push && !shouldEncrypt(roomJid)
|
|
30
|
+
? String(data.messageText ?? '').slice(0, REACTION_MESSAGE_TEXT_MAX)
|
|
31
|
+
: '';
|
|
32
|
+
|
|
33
|
+
const dataReaction: Record<string, string> = {
|
|
34
|
+
senderFirstName: data.firstName ?? '',
|
|
35
|
+
senderLastName: data.lastName ?? '',
|
|
36
|
+
senderJID: client.jid?.toString() ?? '',
|
|
37
|
+
roomJid,
|
|
38
|
+
...(data.photoURL ? { photoURL: data.photoURL } : {}),
|
|
39
|
+
...(push
|
|
40
|
+
? {
|
|
41
|
+
push: 'true',
|
|
42
|
+
messageAuthor: data.messageAuthor ?? '',
|
|
43
|
+
messageText,
|
|
44
|
+
isMediafile: data.isMediafile ?? '',
|
|
45
|
+
mimetype: data.mimetype ?? '',
|
|
46
|
+
fileName: data.fileName ?? '',
|
|
47
|
+
}
|
|
48
|
+
: {}),
|
|
16
49
|
};
|
|
17
50
|
|
|
18
51
|
const message = xml(
|
|
@@ -31,7 +64,6 @@ export function sendMessageReaction(
|
|
|
31
64
|
xmlns: 'urn:xmpp:reactions:0',
|
|
32
65
|
},
|
|
33
66
|
...reactionsList.map((reaction) => xml('reaction', {}, reaction ?? ''))
|
|
34
|
-
// xml('reaction', {}, reactionSymbol ?? '👋'),
|
|
35
67
|
),
|
|
36
68
|
xml('data', dataReaction),
|
|
37
69
|
xml('store', { xmlns: 'urn:xmpp:hints' })
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
HistoryPageCursor,
|
|
17
17
|
} from './xmpp/getHistory.xmpp';
|
|
18
18
|
import { sendTextMessage } from './xmpp/sendTextMessage.xmpp';
|
|
19
|
-
import { sendMessageReaction } from './xmpp/sendMessageReaction.xmpp';
|
|
19
|
+
import { sendMessageReaction, type ReactionStanzaData } from './xmpp/sendMessageReaction.xmpp';
|
|
20
20
|
import { sendReceivedReceipt } from './xmpp/sendReceivedReceipt.xmpp';
|
|
21
21
|
import { sendDisplayedMarker } from './xmpp/sendDisplayedMarker.xmpp';
|
|
22
22
|
import { sendTextMessageWithTranslateTag } from './xmpp/sendTextMessageWithTranslateTag.xmpp';
|
|
@@ -1646,7 +1646,7 @@ export class XmppClient {
|
|
|
1646
1646
|
messageId: string,
|
|
1647
1647
|
roomJid: string,
|
|
1648
1648
|
reactionsList: string[],
|
|
1649
|
-
data?:
|
|
1649
|
+
data?: ReactionStanzaData
|
|
1650
1650
|
) {
|
|
1651
1651
|
sendMessageReaction(
|
|
1652
1652
|
this.client,
|
package/src/push/sealedPush.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface SealedPushEnvelope {
|
|
|
20
20
|
msgID: string;
|
|
21
21
|
/** Recipient. */
|
|
22
22
|
userJid: string;
|
|
23
|
+
/** 'reaction' for a push about an emoji put on the user's message; absent for a message. */
|
|
24
|
+
type?: string;
|
|
23
25
|
/** Base64 of the sealed box; absent when the service could not seal. */
|
|
24
26
|
enc?: string;
|
|
25
27
|
}
|
|
@@ -35,8 +37,14 @@ export interface SealedPushContent {
|
|
|
35
37
|
msgID: string;
|
|
36
38
|
/** Sender avatar URL, '' when none. */
|
|
37
39
|
image: string;
|
|
40
|
+
/** 'reaction' with the emoji in `reaction`; absent for a message. */
|
|
41
|
+
type?: string;
|
|
42
|
+
reaction?: string;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
export const isReactionPush = (envelope: Pick<SealedPushEnvelope, 'type'>): boolean =>
|
|
46
|
+
envelope.type === 'reaction';
|
|
47
|
+
|
|
40
48
|
export type SealedPushFailure =
|
|
41
49
|
/** No key on this device (never generated, or the store is not readable). */
|
|
42
50
|
| 'no-key'
|
|
@@ -68,6 +76,7 @@ export const sealedPushEnvelopeOf = (
|
|
|
68
76
|
jid: str(data.jid),
|
|
69
77
|
msgID: str(data.msgID),
|
|
70
78
|
userJid: str(data.userJid),
|
|
79
|
+
...(str(data.type) ? { type: str(data.type) } : {}),
|
|
71
80
|
...(enc ? { enc } : {}),
|
|
72
81
|
};
|
|
73
82
|
};
|
|
@@ -88,6 +97,8 @@ const parseContent = (plaintext: Uint8Array): SealedPushContent | null => {
|
|
|
88
97
|
chatJid: str(parsed.chatJid),
|
|
89
98
|
msgID: str(parsed.msgID),
|
|
90
99
|
image: str(parsed.image),
|
|
100
|
+
...(str(parsed.type) ? { type: str(parsed.type) } : {}),
|
|
101
|
+
...(str(parsed.reaction) ? { reaction: str(parsed.reaction) } : {}),
|
|
91
102
|
};
|
|
92
103
|
} catch {
|
|
93
104
|
return null;
|