@ethora/chat-component-rn 26.10.2 → 26.10.4
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/lib/commonjs/components/Header/HeaderRoomList.js +72 -69
- package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +13 -11
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageContainer.js +10 -2
- package/lib/commonjs/components/MainComponents/MessageContainer.js.map +1 -1
- package/lib/commonjs/components/MainComponents/RoomList.js +8 -18
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +17 -14
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +131 -17
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +6 -1
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/ActiveCallBar.js +203 -0
- package/lib/commonjs/components/VideoCalls/ActiveCallBar.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/CallButtons.js +3 -1
- package/lib/commonjs/components/VideoCalls/CallButtons.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +123 -129
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js +212 -414
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/callActions.js +61 -0
- package/lib/commonjs/components/VideoCalls/callActions.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/callSessionController.js +461 -0
- package/lib/commonjs/components/VideoCalls/callSessionController.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/callUiStore.js +91 -0
- package/lib/commonjs/components/VideoCalls/callUiStore.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/livekitRuntime.js +1 -0
- package/lib/commonjs/components/VideoCalls/livekitRuntime.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/useDraggable.js +59 -0
- package/lib/commonjs/components/VideoCalls/useDraggable.js.map +1 -0
- package/lib/commonjs/helpers/createRoomFromApi.js +4 -0
- package/lib/commonjs/helpers/createRoomFromApi.js.map +1 -1
- package/lib/commonjs/helpers/incomingCallFromPush.js +1 -1
- package/lib/commonjs/helpers/incomingCallFromPush.js.map +1 -1
- package/lib/commonjs/helpers/roomUserCount.js +7 -1
- package/lib/commonjs/helpers/roomUserCount.js.map +1 -1
- package/lib/commonjs/helpers/uniqueMembers.js +36 -0
- package/lib/commonjs/helpers/uniqueMembers.js.map +1 -0
- package/lib/commonjs/hooks/useLeaveRoom.js +52 -0
- package/lib/commonjs/hooks/useLeaveRoom.js.map +1 -0
- package/lib/commonjs/i18n/strings.js +24 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +36 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/callTokenStanza.js +47 -3
- package/lib/commonjs/networking/callTokenStanza.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +9 -2
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/components/Header/HeaderRoomList.js +73 -70
- package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +15 -13
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/MessageContainer.js +10 -2
- package/lib/module/components/MainComponents/MessageContainer.js.map +1 -1
- package/lib/module/components/MainComponents/RoomList.js +8 -18
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +18 -15
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +132 -18
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/module/components/RoomComponents/ChatRoomItem.js +6 -1
- package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/module/components/VideoCalls/ActiveCallBar.js +195 -0
- package/lib/module/components/VideoCalls/ActiveCallBar.js.map +1 -0
- package/lib/module/components/VideoCalls/CallButtons.js +3 -1
- package/lib/module/components/VideoCalls/CallButtons.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallOverlay.js +125 -131
- package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallSession.js +213 -417
- package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -1
- package/lib/module/components/VideoCalls/callActions.js +55 -0
- package/lib/module/components/VideoCalls/callActions.js.map +1 -0
- package/lib/module/components/VideoCalls/callSessionController.js +450 -0
- package/lib/module/components/VideoCalls/callSessionController.js.map +1 -0
- package/lib/module/components/VideoCalls/callUiStore.js +84 -0
- package/lib/module/components/VideoCalls/callUiStore.js.map +1 -0
- package/lib/module/components/VideoCalls/livekitRuntime.js +1 -0
- package/lib/module/components/VideoCalls/livekitRuntime.js.map +1 -1
- package/lib/module/components/VideoCalls/useDraggable.js +55 -0
- package/lib/module/components/VideoCalls/useDraggable.js.map +1 -0
- package/lib/module/helpers/createRoomFromApi.js +4 -0
- package/lib/module/helpers/createRoomFromApi.js.map +1 -1
- package/lib/module/helpers/incomingCallFromPush.js +1 -1
- package/lib/module/helpers/incomingCallFromPush.js.map +1 -1
- package/lib/module/helpers/roomUserCount.js +7 -1
- package/lib/module/helpers/roomUserCount.js.map +1 -1
- package/lib/module/helpers/uniqueMembers.js +31 -0
- package/lib/module/helpers/uniqueMembers.js.map +1 -0
- package/lib/module/hooks/useLeaveRoom.js +48 -0
- package/lib/module/hooks/useLeaveRoom.js.map +1 -0
- package/lib/module/i18n/strings.js +24 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +35 -1
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/callTokenStanza.js +47 -3
- package/lib/module/networking/callTokenStanza.js.map +1 -1
- package/lib/module/networking/xmppClient.js +8 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageContainer.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/ActiveCallBar.d.ts +21 -0
- package/lib/typescript/components/VideoCalls/ActiveCallBar.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/CallButtons.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts +0 -6
- package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts +15 -22
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/callActions.d.ts +15 -0
- package/lib/typescript/components/VideoCalls/callActions.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/callSessionController.d.ts +63 -0
- package/lib/typescript/components/VideoCalls/callSessionController.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/callUiStore.d.ts +24 -0
- package/lib/typescript/components/VideoCalls/callUiStore.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts +2 -0
- package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/useDraggable.d.ts +19 -0
- package/lib/typescript/components/VideoCalls/useDraggable.d.ts.map +1 -0
- package/lib/typescript/helpers/createRoomFromApi.d.ts.map +1 -1
- package/lib/typescript/helpers/roomUserCount.d.ts +1 -0
- package/lib/typescript/helpers/roomUserCount.d.ts.map +1 -1
- package/lib/typescript/helpers/uniqueMembers.d.ts +11 -0
- package/lib/typescript/helpers/uniqueMembers.d.ts.map +1 -0
- package/lib/typescript/hooks/useLeaveRoom.d.ts +9 -0
- package/lib/typescript/hooks/useLeaveRoom.d.ts.map +1 -0
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +8 -0
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/networking/callTokenStanza.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Header/HeaderRoomList.tsx +66 -61
- package/src/components/MainComponents/ChatHeader.tsx +13 -8
- package/src/components/MainComponents/MessageContainer.tsx +14 -1
- package/src/components/MainComponents/RoomList.tsx +8 -14
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +16 -13
- package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +167 -36
- package/src/components/RoomComponents/ChatRoomItem.tsx +8 -1
- package/src/components/VideoCalls/ActiveCallBar.tsx +176 -0
- package/src/components/VideoCalls/CallButtons.tsx +5 -1
- package/src/components/VideoCalls/VideoCallOverlay.tsx +153 -183
- package/src/components/VideoCalls/VideoCallSession.tsx +227 -439
- package/src/components/VideoCalls/callActions.ts +63 -0
- package/src/components/VideoCalls/callSessionController.ts +439 -0
- package/src/components/VideoCalls/callUiStore.ts +76 -0
- package/src/components/VideoCalls/livekitRuntime.ts +3 -0
- package/src/components/VideoCalls/useDraggable.ts +46 -0
- package/src/helpers/createRoomFromApi.tsx +2 -0
- package/src/helpers/incomingCallFromPush.ts +2 -2
- package/src/helpers/roomUserCount.ts +6 -2
- package/src/helpers/uniqueMembers.ts +26 -0
- package/src/hooks/useLeaveRoom.ts +42 -0
- package/src/i18n/strings.ts +24 -0
- package/src/networking/api-requests/rooms.api.ts +28 -1
- package/src/networking/callTokenStanza.ts +46 -6
- package/src/networking/xmppClient.ts +10 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { store } from '../../roomStore';
|
|
2
|
+
import { declineIncomingCall, endCall } from '../../roomStore/callSlice';
|
|
3
|
+
import { addRoomMessage } from '../../roomStore/roomsSlice';
|
|
4
|
+
import { buildLocalCallLogMessage } from '../../helpers/callLogMessage';
|
|
5
|
+
import {
|
|
6
|
+
CallSignalState,
|
|
7
|
+
sendCallStateSignal,
|
|
8
|
+
} from '../../networking/callTokenStanza';
|
|
9
|
+
import { translateKey } from '../../i18n/strings';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Call teardown that doesn't need a mounted component.
|
|
13
|
+
*
|
|
14
|
+
* It used to live inside VideoCallOverlay, which meant only a rendered
|
|
15
|
+
* overlay could end a call. The LiveKit session now outlives the UI
|
|
16
|
+
* (callSessionController.ts), so the session's own "peer left" and
|
|
17
|
+
* "disconnected" events need the same teardown, with the same local log
|
|
18
|
+
* entry and the same signal to the peer.
|
|
19
|
+
*/
|
|
20
|
+
export const terminateActiveCall = (state: CallSignalState): void => {
|
|
21
|
+
// Write a local "call ended" log entry BEFORE endCall() wipes the call
|
|
22
|
+
// slice. The authoritative entry is a server call-state broadcast, but
|
|
23
|
+
// that isn't guaranteed to arrive, so we synthesize our own. It's
|
|
24
|
+
// deduped against any server copy by callId in addRoomMessage.
|
|
25
|
+
const snap = store.getState().call;
|
|
26
|
+
if (snap.phase === 'idle') {return;}
|
|
27
|
+
if (snap.roomJid && (snap.callId || snap.connectedAt)) {
|
|
28
|
+
const durationMs = snap.connectedAt
|
|
29
|
+
? Math.max(0, Date.now() - snap.connectedAt)
|
|
30
|
+
: 0;
|
|
31
|
+
store.dispatch(
|
|
32
|
+
addRoomMessage({
|
|
33
|
+
roomJID: snap.roomJid,
|
|
34
|
+
message: buildLocalCallLogMessage({
|
|
35
|
+
callId: snap.callId || '',
|
|
36
|
+
direction: snap.direction === 'incoming' ? 'incoming' : 'outgoing',
|
|
37
|
+
durationMs,
|
|
38
|
+
kind: snap.kind === 'audio' ? 'audio' : 'video',
|
|
39
|
+
selfXmppUsername:
|
|
40
|
+
store.getState().chatSettingStore.user?.xmppUsername || '',
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
sendCallStateSignal(state);
|
|
46
|
+
store.dispatch(endCall());
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const declineActiveCall = (): void => {
|
|
50
|
+
sendCallStateSignal('declined');
|
|
51
|
+
store.dispatch(declineIncomingCall());
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** `t` for code outside React, resolved by the same rule as useT. */
|
|
55
|
+
export const translateCallString = (
|
|
56
|
+
key: string,
|
|
57
|
+
vars?: Record<string, string | number>
|
|
58
|
+
): string => {
|
|
59
|
+
const settings = store.getState().chatSettingStore as any;
|
|
60
|
+
const locale =
|
|
61
|
+
settings?.uiLocale || settings?.config?.i18n?.locale || settings?.langSource;
|
|
62
|
+
return translateKey(key, locale, settings?.config?.i18n?.strings, vars);
|
|
63
|
+
};
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { store } from '../../roomStore';
|
|
4
|
+
import { setCallError, setCallPhase } from '../../roomStore/callSlice';
|
|
5
|
+
import { ensureLiveKitGlobals, loadLiveKit } from './livekitRuntime';
|
|
6
|
+
import { terminateActiveCall, translateCallString } from './callActions';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The LiveKit connection of the current call, owned outside React.
|
|
10
|
+
*
|
|
11
|
+
* It used to live in VideoCallSession's effect, so the connection was as
|
|
12
|
+
* long-lived as that component instance. Minimizing swapped the Modal for a
|
|
13
|
+
* floating View, React remounted the session, the effect cleanup
|
|
14
|
+
* disconnected, and the peer saw us leave (and hung up). The web SDK avoids
|
|
15
|
+
* that by keeping one LiveKitRoom element mounted across both layouts; the
|
|
16
|
+
* RN equivalent is to not tie the room to a component at all.
|
|
17
|
+
*
|
|
18
|
+
* Lifetime is driven by the call slice: a session exists while the call is
|
|
19
|
+
* 'connecting' or 'in-call' with a token, and is torn down by a store
|
|
20
|
+
* subscription the moment that stops being true (hangup, remote end,
|
|
21
|
+
* logout's resetCall), whether or not any call UI is mounted. Components
|
|
22
|
+
* only read the snapshot and call the controls.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface CallSessionSnapshot {
|
|
26
|
+
/** A LiveKit room exists for the current call. */
|
|
27
|
+
active: boolean;
|
|
28
|
+
connected: boolean;
|
|
29
|
+
/** Translated, dismissible camera/mic problem. Never fails the call. */
|
|
30
|
+
deviceError: string | null;
|
|
31
|
+
micOn: boolean;
|
|
32
|
+
cameraOn: boolean;
|
|
33
|
+
speakerOn: boolean;
|
|
34
|
+
/** Which camera the local video comes from; only the front one is mirrored. */
|
|
35
|
+
facingMode: 'user' | 'environment';
|
|
36
|
+
/** `{ participant, publication, source }` for `<VideoTrack />`, live and unmuted only. */
|
|
37
|
+
remoteVideo: any;
|
|
38
|
+
localVideo: any;
|
|
39
|
+
remoteName: string | null;
|
|
40
|
+
remoteJoined: boolean;
|
|
41
|
+
/** Peer joined but has no live microphone. */
|
|
42
|
+
remoteMicMuted: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* When the call truly connected: the later of the two LiveKit `joinedAt`
|
|
45
|
+
* stamps. The server sets them, so both sides count from the same moment
|
|
46
|
+
* (web's useSharedCallElapsed). Null until the peer is in.
|
|
47
|
+
*/
|
|
48
|
+
anchorAt: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const IDLE: CallSessionSnapshot = {
|
|
52
|
+
active: false,
|
|
53
|
+
connected: false,
|
|
54
|
+
deviceError: null,
|
|
55
|
+
micOn: true,
|
|
56
|
+
cameraOn: false,
|
|
57
|
+
speakerOn: false,
|
|
58
|
+
facingMode: 'user',
|
|
59
|
+
remoteVideo: null,
|
|
60
|
+
localVideo: null,
|
|
61
|
+
remoteName: null,
|
|
62
|
+
remoteJoined: false,
|
|
63
|
+
remoteMicMuted: false,
|
|
64
|
+
anchorAt: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
interface Session {
|
|
68
|
+
key: string;
|
|
69
|
+
room: any;
|
|
70
|
+
kind: 'audio' | 'video';
|
|
71
|
+
disposed: boolean;
|
|
72
|
+
everJoined: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let session: Session | null = null;
|
|
76
|
+
let snapshot: CallSessionSnapshot = IDLE;
|
|
77
|
+
const listeners = new Set<() => void>();
|
|
78
|
+
let storeUnsubscribe: (() => void) | null = null;
|
|
79
|
+
|
|
80
|
+
const emit = (patch: Partial<CallSessionSnapshot>) => {
|
|
81
|
+
snapshot = { ...snapshot, ...patch };
|
|
82
|
+
listeners.forEach((listener) => listener());
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const subscribe = (listener: () => void) => {
|
|
86
|
+
listeners.add(listener);
|
|
87
|
+
return () => {
|
|
88
|
+
listeners.delete(listener);
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getSnapshot = () => snapshot;
|
|
93
|
+
|
|
94
|
+
export const useCallSession = (): CallSessionSnapshot =>
|
|
95
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
96
|
+
|
|
97
|
+
const sessionKey = (livekitUrl: string, token: string) => `${livekitUrl}|${token}`;
|
|
98
|
+
|
|
99
|
+
// A device (camera/mic) permission/availability problem during enable,
|
|
100
|
+
// distinct from a connect failure. Mirrors the web SDK's humanizeDeviceError.
|
|
101
|
+
const humanizeDeviceError = (error: unknown): string => {
|
|
102
|
+
const name = (error as { name?: string })?.name || '';
|
|
103
|
+
const text = String((error as Error)?.message || '').toLowerCase();
|
|
104
|
+
if (name === 'NotAllowedError' || text.includes('permission') || text.includes('denied')) {
|
|
105
|
+
return translateCallString('call.error.deviceBlocked');
|
|
106
|
+
}
|
|
107
|
+
if (name === 'NotFoundError' || text.includes('not found')) {
|
|
108
|
+
return translateCallString('call.error.deviceNotFound');
|
|
109
|
+
}
|
|
110
|
+
if (name === 'NotReadableError' || text.includes('in use')) {
|
|
111
|
+
return translateCallString('call.error.deviceInUse');
|
|
112
|
+
}
|
|
113
|
+
return translateCallString('call.error.deviceGeneric');
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Only a live, unmuted track gets a video surface, so the stage never
|
|
117
|
+
// flips between an avatar and a black frame (web's hasLiveVideo).
|
|
118
|
+
const liveVideoRef = (participant: any, Track: any) => {
|
|
119
|
+
if (!participant) {return null;}
|
|
120
|
+
const publication = participant.getTrackPublication?.(Track.Source.Camera);
|
|
121
|
+
if (!publication?.track || publication.isMuted) {return null;}
|
|
122
|
+
return { participant, publication, source: Track.Source.Camera };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const joinedAtMs = (participant: any): number => {
|
|
126
|
+
const value = participant?.joinedAt;
|
|
127
|
+
const ms = value ? new Date(value).getTime() : 0;
|
|
128
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const resync = () => {
|
|
132
|
+
const current = session;
|
|
133
|
+
const runtime = loadLiveKit();
|
|
134
|
+
if (!current || current.disposed || !runtime) {return;}
|
|
135
|
+
const { Track } = runtime;
|
|
136
|
+
const room = current.room;
|
|
137
|
+
|
|
138
|
+
const remote = Array.from(room.remoteParticipants?.values?.() || [])[0] as any;
|
|
139
|
+
if (remote) {current.everJoined = true;}
|
|
140
|
+
|
|
141
|
+
const remoteMic = remote?.getTrackPublication?.(Track.Source.Microphone);
|
|
142
|
+
const localJoined = joinedAtMs(room.localParticipant);
|
|
143
|
+
const remoteJoinedAt = joinedAtMs(remote);
|
|
144
|
+
|
|
145
|
+
emit({
|
|
146
|
+
remoteJoined: !!remote,
|
|
147
|
+
remoteName: remote?.name || remote?.identity || null,
|
|
148
|
+
remoteVideo: liveVideoRef(remote, Track),
|
|
149
|
+
localVideo: liveVideoRef(room.localParticipant, Track),
|
|
150
|
+
remoteMicMuted: !!remote && (!remoteMic || remoteMic.isMuted !== false),
|
|
151
|
+
anchorAt:
|
|
152
|
+
remote && localJoined && remoteJoinedAt
|
|
153
|
+
? Math.max(localJoined, remoteJoinedAt)
|
|
154
|
+
: remote
|
|
155
|
+
? snapshot.anchorAt ?? Date.now()
|
|
156
|
+
: null,
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const stopCallSession = (): void => {
|
|
161
|
+
const current = session;
|
|
162
|
+
if (!current) {return;}
|
|
163
|
+
current.disposed = true;
|
|
164
|
+
session = null;
|
|
165
|
+
try {
|
|
166
|
+
current.room.removeAllListeners?.();
|
|
167
|
+
void current.room.disconnect();
|
|
168
|
+
} catch {
|
|
169
|
+
// Already gone.
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
void loadLiveKit()?.AudioSession?.stopAudioSession();
|
|
173
|
+
} catch {
|
|
174
|
+
// Never let teardown throw.
|
|
175
|
+
}
|
|
176
|
+
snapshot = IDLE;
|
|
177
|
+
listeners.forEach((listener) => listener());
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Tear the session down as soon as the call slice says there is no call to
|
|
181
|
+
// carry it any more, independent of what UI happens to be mounted.
|
|
182
|
+
const ensureStoreWatcher = () => {
|
|
183
|
+
if (storeUnsubscribe) {return;}
|
|
184
|
+
storeUnsubscribe = store.subscribe(() => {
|
|
185
|
+
if (!session) {return;}
|
|
186
|
+
const call = store.getState().call;
|
|
187
|
+
const live =
|
|
188
|
+
!!call.token && (call.phase === 'connecting' || call.phase === 'in-call');
|
|
189
|
+
if (!live || !session.key.endsWith(`|${call.token}`)) {
|
|
190
|
+
stopCallSession();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export interface StartCallSessionOptions {
|
|
196
|
+
token: string;
|
|
197
|
+
livekitUrl: string;
|
|
198
|
+
kind: 'audio' | 'video';
|
|
199
|
+
startWithMicOn?: boolean;
|
|
200
|
+
startWithCameraOn?: boolean;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Join the LiveKit room for the current call. Idempotent per (url, token):
|
|
205
|
+
* calling it again from a remounted overlay re-attaches to the same room.
|
|
206
|
+
*/
|
|
207
|
+
export const startCallSession = ({
|
|
208
|
+
token,
|
|
209
|
+
livekitUrl,
|
|
210
|
+
kind,
|
|
211
|
+
startWithMicOn = true,
|
|
212
|
+
startWithCameraOn = true,
|
|
213
|
+
}: StartCallSessionOptions): void => {
|
|
214
|
+
const key = sessionKey(livekitUrl, token);
|
|
215
|
+
if (session?.key === key) {return;}
|
|
216
|
+
stopCallSession();
|
|
217
|
+
ensureStoreWatcher();
|
|
218
|
+
|
|
219
|
+
const runtime = loadLiveKit();
|
|
220
|
+
if (!runtime) {
|
|
221
|
+
store.dispatch(setCallError(translateCallString('call.error.sdkMissing')));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!token || !livekitUrl) {
|
|
225
|
+
store.dispatch(setCallError(translateCallString('call.error.missingConfig')));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
ensureLiveKitGlobals(runtime);
|
|
230
|
+
const { Room, RoomEvent, AudioSession, VideoPresets } = runtime;
|
|
231
|
+
|
|
232
|
+
// The web session's options, verbatim. `pixelDensity: 'screen'` matters:
|
|
233
|
+
// with a plain `adaptiveStream: true` the subscriber asks for a layer
|
|
234
|
+
// sized to the view in POINTS, i.e. 2-3x below the phone's real pixels,
|
|
235
|
+
// and the peer's video arrived visibly blurry next to web.
|
|
236
|
+
const room = new Room({
|
|
237
|
+
adaptiveStream: { pixelDensity: 'screen' },
|
|
238
|
+
dynacast: true,
|
|
239
|
+
...(VideoPresets
|
|
240
|
+
? {
|
|
241
|
+
publishDefaults: {
|
|
242
|
+
videoSimulcastLayers: [VideoPresets.h540, VideoPresets.h216],
|
|
243
|
+
},
|
|
244
|
+
videoCaptureDefaults: { resolution: VideoPresets.h720.resolution },
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
});
|
|
248
|
+
const current: Session = { key, room, kind, disposed: false, everJoined: false };
|
|
249
|
+
session = current;
|
|
250
|
+
snapshot = {
|
|
251
|
+
...IDLE,
|
|
252
|
+
active: true,
|
|
253
|
+
micOn: startWithMicOn,
|
|
254
|
+
cameraOn: kind === 'video' && startWithCameraOn,
|
|
255
|
+
// Video calls belong on the loudspeaker, audio calls on the earpiece.
|
|
256
|
+
speakerOn: kind === 'video',
|
|
257
|
+
};
|
|
258
|
+
listeners.forEach((listener) => listener());
|
|
259
|
+
|
|
260
|
+
const onChange = () => {
|
|
261
|
+
if (!current.disposed) {resync();}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
room
|
|
265
|
+
.on(RoomEvent.TrackSubscribed, onChange)
|
|
266
|
+
.on(RoomEvent.TrackUnsubscribed, onChange)
|
|
267
|
+
.on(RoomEvent.TrackPublished, onChange)
|
|
268
|
+
.on(RoomEvent.TrackUnpublished, onChange)
|
|
269
|
+
.on(RoomEvent.LocalTrackPublished, onChange)
|
|
270
|
+
.on(RoomEvent.LocalTrackUnpublished, onChange)
|
|
271
|
+
.on(RoomEvent.TrackMuted, onChange)
|
|
272
|
+
.on(RoomEvent.TrackUnmuted, onChange)
|
|
273
|
+
.on(RoomEvent.ParticipantConnected, onChange)
|
|
274
|
+
.on(RoomEvent.MediaDevicesError, (error: unknown) => {
|
|
275
|
+
if (current.disposed) {return;}
|
|
276
|
+
emit({ deviceError: humanizeDeviceError(error) });
|
|
277
|
+
})
|
|
278
|
+
.on(RoomEvent.ParticipantDisconnected, () => {
|
|
279
|
+
if (current.disposed) {return;}
|
|
280
|
+
resync();
|
|
281
|
+
// 1:1 semantics, as web's PeerLeaveWatcher: once the peer had been
|
|
282
|
+
// in the room, their leaving IS the end of the call. Before they
|
|
283
|
+
// join (outgoing "Ringing...") an empty room is expected.
|
|
284
|
+
if (current.everJoined && (room.remoteParticipants?.size ?? 0) === 0) {
|
|
285
|
+
terminateActiveCall('ended');
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
.on(RoomEvent.Disconnected, () => {
|
|
289
|
+
if (current.disposed) {return;}
|
|
290
|
+
emit({ connected: false });
|
|
291
|
+
terminateActiveCall('ended');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
(async () => {
|
|
295
|
+
// Joining the LiveKit room is the only step whose failure should fail
|
|
296
|
+
// the call (bad token, network, server down). Device setup below is
|
|
297
|
+
// best-effort.
|
|
298
|
+
try {
|
|
299
|
+
// Claims audio focus and sets the right native audio mode. Must
|
|
300
|
+
// start BEFORE connecting or the first seconds route to the earpiece
|
|
301
|
+
// on Android even for a video call.
|
|
302
|
+
await AudioSession.startAudioSession();
|
|
303
|
+
await room.connect(livekitUrl, token);
|
|
304
|
+
} catch (error: any) {
|
|
305
|
+
if (current.disposed) {return;}
|
|
306
|
+
stopCallSession();
|
|
307
|
+
store.dispatch(
|
|
308
|
+
setCallError(
|
|
309
|
+
String(error?.message || '') ||
|
|
310
|
+
translateCallString('call.error.connectFailed')
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (current.disposed) {return;}
|
|
316
|
+
|
|
317
|
+
// Devices are best-effort: a blocked permission or the iOS Simulator
|
|
318
|
+
// (no camera at all) must not kill a call that connected fine.
|
|
319
|
+
try {
|
|
320
|
+
await room.localParticipant.setMicrophoneEnabled(startWithMicOn);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (!current.disposed) {emit({ deviceError: humanizeDeviceError(error) });}
|
|
323
|
+
}
|
|
324
|
+
if (kind === 'video') {
|
|
325
|
+
try {
|
|
326
|
+
await room.localParticipant.setCameraEnabled(startWithCameraOn);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (!current.disposed) {
|
|
329
|
+
emit({ cameraOn: false, deviceError: humanizeDeviceError(error) });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (current.disposed) {return;}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
await AudioSession.setAppleAudioConfiguration?.({
|
|
337
|
+
audioCategory: 'playAndRecord',
|
|
338
|
+
audioCategoryOptions: kind === 'video' ? ['defaultToSpeaker'] : [],
|
|
339
|
+
audioMode: kind === 'video' ? 'videoChat' : 'voiceChat',
|
|
340
|
+
});
|
|
341
|
+
} catch {
|
|
342
|
+
// iOS-only helper.
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
emit({ connected: true });
|
|
346
|
+
resync();
|
|
347
|
+
store.dispatch(setCallPhase('in-call'));
|
|
348
|
+
})();
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// ---- controls -----------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
export const toggleCallMic = async (): Promise<void> => {
|
|
354
|
+
const current = session;
|
|
355
|
+
if (!current) {return;}
|
|
356
|
+
const next = !snapshot.micOn;
|
|
357
|
+
emit({ micOn: next });
|
|
358
|
+
try {
|
|
359
|
+
await current.room.localParticipant.setMicrophoneEnabled(next);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (!current.disposed) {
|
|
362
|
+
emit({ micOn: !next, deviceError: humanizeDeviceError(error) });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export const toggleCallCamera = async (): Promise<void> => {
|
|
368
|
+
const current = session;
|
|
369
|
+
if (!current) {return;}
|
|
370
|
+
const next = !snapshot.cameraOn;
|
|
371
|
+
emit({ cameraOn: next });
|
|
372
|
+
try {
|
|
373
|
+
await current.room.localParticipant.setCameraEnabled(next);
|
|
374
|
+
resync();
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (!current.disposed) {
|
|
377
|
+
emit({ cameraOn: !next, deviceError: humanizeDeviceError(error) });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export const switchCallCamera = async (): Promise<void> => {
|
|
383
|
+
const current = session;
|
|
384
|
+
const runtime = loadLiveKit();
|
|
385
|
+
if (!current || !runtime) {return;}
|
|
386
|
+
const mediaTrack = current.room.localParticipant.getTrackPublication?.(
|
|
387
|
+
runtime.Track.Source.Camera
|
|
388
|
+
)?.track?.mediaStreamTrack;
|
|
389
|
+
if (!mediaTrack?.applyConstraints) {return;}
|
|
390
|
+
const next = snapshot.facingMode === 'user' ? 'environment' : 'user';
|
|
391
|
+
try {
|
|
392
|
+
// Swap the capture device inside the SAME track (react-native-webrtc's
|
|
393
|
+
// own camera switch). `restartTrack` stopped the published track and
|
|
394
|
+
// made a new one, and the native view kept showing the stopped stream:
|
|
395
|
+
// the picture froze on the switch.
|
|
396
|
+
// Keep the current capture settings (resolution, frame rate) and only
|
|
397
|
+
// flip the camera, exactly like the library's `_switchCamera`.
|
|
398
|
+
const constraints = { ...(mediaTrack.getSettings?.() || {}) };
|
|
399
|
+
delete constraints.deviceId;
|
|
400
|
+
await mediaTrack.applyConstraints({ ...constraints, facingMode: next });
|
|
401
|
+
if (!current.disposed) {emit({ facingMode: next });}
|
|
402
|
+
resync();
|
|
403
|
+
} catch {
|
|
404
|
+
// Single camera, or the switch is still in flight.
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
export const toggleCallSpeaker = async (): Promise<void> => {
|
|
409
|
+
const runtime = loadLiveKit();
|
|
410
|
+
if (!session || !runtime) {return;}
|
|
411
|
+
const next = !snapshot.speakerOn;
|
|
412
|
+
emit({ speakerOn: next });
|
|
413
|
+
try {
|
|
414
|
+
if (Platform.OS === 'android') {
|
|
415
|
+
// On Android `selectAudioOutput` device ids are the literal strings
|
|
416
|
+
// 'speaker' / 'earpiece'.
|
|
417
|
+
await runtime.AudioSession.selectAudioOutput(next ? 'speaker' : 'earpiece');
|
|
418
|
+
} else {
|
|
419
|
+
await runtime.AudioSession.setAppleAudioConfiguration?.({
|
|
420
|
+
audioCategory: 'playAndRecord',
|
|
421
|
+
audioCategoryOptions: next ? ['defaultToSpeaker'] : [],
|
|
422
|
+
audioMode: next ? 'videoChat' : 'voiceChat',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
emit({ speakerOn: !next });
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
export const dismissCallDeviceError = (): void => {
|
|
431
|
+
emit({ deviceError: null });
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/** Reset module state. Test seam only. */
|
|
435
|
+
export const __resetCallSessionForTests = (): void => {
|
|
436
|
+
stopCallSession();
|
|
437
|
+
storeUnsubscribe?.();
|
|
438
|
+
storeUnsubscribe = null;
|
|
439
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { store } from '../../roomStore';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UI state of the call shared between the overlay and the header bar:
|
|
6
|
+
* whether the call is minimized, and which header (if any) currently hosts
|
|
7
|
+
* the minimized bar.
|
|
8
|
+
*
|
|
9
|
+
* Minimized, the call is a strip under the visible header (ChatHeader or
|
|
10
|
+
* HeaderRoomList), Telegram-style, instead of a floating panel. Each header
|
|
11
|
+
* registers itself while mounted; the most recently mounted one shows the
|
|
12
|
+
* strip (on an iPad split view both are mounted, and only one strip should
|
|
13
|
+
* appear). With no header mounted (the user is on a host-app screen) the
|
|
14
|
+
* overlay docks the same strip to the top of the screen.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface CallUiState {
|
|
18
|
+
minimized: boolean;
|
|
19
|
+
/** Id of the header that shows the strip, or null when none is mounted. */
|
|
20
|
+
barHost: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let state: CallUiState = { minimized: false, barHost: null };
|
|
24
|
+
const hosts: number[] = [];
|
|
25
|
+
let nextHostId = 1;
|
|
26
|
+
const listeners = new Set<() => void>();
|
|
27
|
+
let storeUnsubscribe: (() => void) | null = null;
|
|
28
|
+
|
|
29
|
+
const emit = (patch: Partial<CallUiState>) => {
|
|
30
|
+
state = { ...state, ...patch };
|
|
31
|
+
listeners.forEach((listener) => listener());
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// The next call always opens full-screen.
|
|
35
|
+
const ensureStoreWatcher = () => {
|
|
36
|
+
if (storeUnsubscribe) {return;}
|
|
37
|
+
storeUnsubscribe = store.subscribe(() => {
|
|
38
|
+
if (state.minimized && store.getState().call.phase === 'idle') {
|
|
39
|
+
emit({ minimized: false });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const subscribe = (listener: () => void) => {
|
|
45
|
+
ensureStoreWatcher();
|
|
46
|
+
listeners.add(listener);
|
|
47
|
+
return () => {
|
|
48
|
+
listeners.delete(listener);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getSnapshot = () => state;
|
|
53
|
+
|
|
54
|
+
/** Current state, for non-React code and tests. */
|
|
55
|
+
export const getCallUiState = (): CallUiState => state;
|
|
56
|
+
|
|
57
|
+
export const useCallUi = (): CallUiState =>
|
|
58
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
59
|
+
|
|
60
|
+
export const setCallMinimized = (minimized: boolean): void => {
|
|
61
|
+
if (state.minimized !== minimized) {emit({ minimized });}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const registerCallBarHost = (
|
|
65
|
+
onId?: (id: number) => void
|
|
66
|
+
): (() => void) => {
|
|
67
|
+
const id = nextHostId++;
|
|
68
|
+
hosts.push(id);
|
|
69
|
+
onId?.(id);
|
|
70
|
+
emit({ barHost: id });
|
|
71
|
+
return () => {
|
|
72
|
+
const index = hosts.indexOf(id);
|
|
73
|
+
if (index >= 0) {hosts.splice(index, 1);}
|
|
74
|
+
emit({ barHost: hosts.length ? hosts[hosts.length - 1] : null });
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -26,6 +26,8 @@ export interface LiveKitRuntime {
|
|
|
26
26
|
Track: any;
|
|
27
27
|
/** livekit-client `ConnectionState` enum. */
|
|
28
28
|
ConnectionState: any;
|
|
29
|
+
/** livekit-client `VideoPresets` (h540, h216...). */
|
|
30
|
+
VideoPresets: any;
|
|
29
31
|
/** RN `<VideoTrack trackRef={...} />` renderer. */
|
|
30
32
|
VideoTrack: any;
|
|
31
33
|
/** Native audio session control (routing, speaker, focus). */
|
|
@@ -63,6 +65,7 @@ export const loadLiveKit = (): LiveKitRuntime | null => {
|
|
|
63
65
|
RoomEvent: core.RoomEvent,
|
|
64
66
|
Track: core.Track,
|
|
65
67
|
ConnectionState: core.ConnectionState,
|
|
68
|
+
VideoPresets: core.VideoPresets,
|
|
66
69
|
VideoTrack: rn.VideoTrack,
|
|
67
70
|
AudioSession: rn.AudioSession,
|
|
68
71
|
registerGlobals: rn.registerGlobals,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useMemo, useRef } from 'react';
|
|
2
|
+
import { Animated, PanResponder } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Drag a floating element around (web's useDraggable, for the self-view
|
|
6
|
+
* tile and the minimized call panel). The offset is kept between drags.
|
|
7
|
+
*
|
|
8
|
+
* The responder only claims the gesture once the finger actually moves a
|
|
9
|
+
* few points, so taps still reach the buttons inside the dragged element.
|
|
10
|
+
*/
|
|
11
|
+
export const useDraggable = () => {
|
|
12
|
+
const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
|
|
13
|
+
const offset = useRef({ x: 0, y: 0 });
|
|
14
|
+
|
|
15
|
+
const panHandlers = useMemo(
|
|
16
|
+
() =>
|
|
17
|
+
PanResponder.create({
|
|
18
|
+
onMoveShouldSetPanResponder: (_event, gesture) =>
|
|
19
|
+
Math.abs(gesture.dx) > 4 || Math.abs(gesture.dy) > 4,
|
|
20
|
+
onPanResponderGrant: () => {
|
|
21
|
+
position.setOffset(offset.current);
|
|
22
|
+
position.setValue({ x: 0, y: 0 });
|
|
23
|
+
},
|
|
24
|
+
onPanResponderMove: Animated.event(
|
|
25
|
+
[null, { dx: position.x, dy: position.y }],
|
|
26
|
+
{ useNativeDriver: false }
|
|
27
|
+
),
|
|
28
|
+
onPanResponderRelease: (_event, gesture) => {
|
|
29
|
+
offset.current = {
|
|
30
|
+
x: offset.current.x + gesture.dx,
|
|
31
|
+
y: offset.current.y + gesture.dy,
|
|
32
|
+
};
|
|
33
|
+
position.flattenOffset();
|
|
34
|
+
},
|
|
35
|
+
onPanResponderTerminate: () => {
|
|
36
|
+
position.flattenOffset();
|
|
37
|
+
},
|
|
38
|
+
}).panHandlers,
|
|
39
|
+
[position]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
panHandlers,
|
|
44
|
+
style: { transform: position.getTranslateTransform() },
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { uniqueMembers } from './uniqueMembers';
|
|
1
2
|
import { ApiRoom, IRoom } from '../types/types';
|
|
2
3
|
import type { LastMessage } from '../types/models/message.model';
|
|
3
4
|
|
|
@@ -81,6 +82,7 @@ export const createRoomFromApi = (
|
|
|
81
82
|
const lastMessage = mapApiLastMessage(room?.lastMessage, jid);
|
|
82
83
|
const roomData: IRoom = {
|
|
83
84
|
...room,
|
|
85
|
+
...(Array.isArray(room?.members) ? { members: uniqueMembers(room.members) } : {}),
|
|
84
86
|
id: (room as any)?._id || '',
|
|
85
87
|
jid,
|
|
86
88
|
name: room?.title || '',
|
|
@@ -58,7 +58,7 @@ export const handleCallPush = (
|
|
|
58
58
|
(room: any) =>
|
|
59
59
|
(room?.jid || '').split('@')[0] === parsed.roomBareName ||
|
|
60
60
|
room?.jid === parsed.roomRef
|
|
61
|
-
) as { jid?: string; name?: string } | undefined;
|
|
61
|
+
) as { jid?: string; name?: string; title?: string } | undefined;
|
|
62
62
|
|
|
63
63
|
const roomJid =
|
|
64
64
|
resolved?.jid ||
|
|
@@ -70,7 +70,7 @@ export const handleCallPush = (
|
|
|
70
70
|
store.dispatch(
|
|
71
71
|
setIncomingCallToken({
|
|
72
72
|
roomJid,
|
|
73
|
-
roomName: parsed.callerName || resolved?.name || null,
|
|
73
|
+
roomName: parsed.callerName || resolved?.title || resolved?.name || null,
|
|
74
74
|
roomBareName: parsed.roomBareName,
|
|
75
75
|
token: parsed.token,
|
|
76
76
|
kind: parsed.kind,
|