@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
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
FC,
|
|
3
|
-
useCallback,
|
|
4
|
-
useEffect,
|
|
5
|
-
useMemo,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from 'react';
|
|
1
|
+
import React, { FC, useEffect, useState } from 'react';
|
|
9
2
|
import {
|
|
10
3
|
ActivityIndicator,
|
|
11
|
-
|
|
4
|
+
Animated,
|
|
5
|
+
Pressable,
|
|
12
6
|
StyleSheet,
|
|
13
7
|
Text,
|
|
14
8
|
TouchableOpacity,
|
|
15
9
|
View,
|
|
16
10
|
} from 'react-native';
|
|
11
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
12
|
import { VideoCallIcons } from '../../types/types';
|
|
18
13
|
import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlaceholder';
|
|
19
14
|
import {
|
|
@@ -23,12 +18,20 @@ import {
|
|
|
23
18
|
MicOffIcon,
|
|
24
19
|
MicOnIcon,
|
|
25
20
|
MinimizeIcon,
|
|
26
|
-
ExpandIcon,
|
|
27
21
|
SpeakerIcon,
|
|
28
22
|
SpeakerOffIcon,
|
|
29
23
|
SwitchCameraIcon,
|
|
30
24
|
} from './CallIcons';
|
|
31
|
-
import {
|
|
25
|
+
import { loadLiveKit } from './livekitRuntime';
|
|
26
|
+
import {
|
|
27
|
+
dismissCallDeviceError,
|
|
28
|
+
switchCallCamera,
|
|
29
|
+
toggleCallCamera,
|
|
30
|
+
toggleCallMic,
|
|
31
|
+
toggleCallSpeaker,
|
|
32
|
+
useCallSession,
|
|
33
|
+
} from './callSessionController';
|
|
34
|
+
import { useDraggable } from './useDraggable';
|
|
32
35
|
import { useT } from '../../i18n/useT';
|
|
33
36
|
|
|
34
37
|
const DEFAULT_PRIMARY = '#0052CD';
|
|
@@ -38,334 +41,68 @@ const TEXT_MUTED = '#9AA4B2';
|
|
|
38
41
|
const DANGER = '#E53935';
|
|
39
42
|
const CONTROL_IDLE = 'rgba(255, 255, 255, 0.16)';
|
|
40
43
|
|
|
41
|
-
// A device (camera/mic) permission/availability problem during enable —
|
|
42
|
-
// distinct from a connect failure. Mirrors the web SDK's humanizeDeviceError:
|
|
43
|
-
// this must never fail the whole call (the user can still talk/hear once
|
|
44
|
-
// they re-allow or plug in a device), only surface a dismissible hint. Most
|
|
45
|
-
// relevant on the iOS Simulator, which has no real camera at all — every
|
|
46
|
-
// video call would otherwise die at setCameraEnabled before ever connecting.
|
|
47
|
-
const humanizeDeviceError = (error: unknown, t: (key: string) => string): string => {
|
|
48
|
-
const name = (error as { name?: string })?.name || '';
|
|
49
|
-
const text = String((error as Error)?.message || '').toLowerCase();
|
|
50
|
-
if (name === 'NotAllowedError' || text.includes('permission') || text.includes('denied')) {
|
|
51
|
-
return t('call.error.deviceBlocked');
|
|
52
|
-
}
|
|
53
|
-
if (name === 'NotFoundError' || text.includes('not found')) {
|
|
54
|
-
return t('call.error.deviceNotFound');
|
|
55
|
-
}
|
|
56
|
-
if (name === 'NotReadableError' || text.includes('in use')) {
|
|
57
|
-
return t('call.error.deviceInUse');
|
|
58
|
-
}
|
|
59
|
-
return t('call.error.deviceGeneric');
|
|
60
|
-
};
|
|
61
|
-
|
|
62
44
|
export interface VideoCallSessionProps {
|
|
63
|
-
token: string;
|
|
64
|
-
livekitUrl: string;
|
|
65
45
|
kind?: 'audio' | 'video';
|
|
66
|
-
/** Display name of the peer, shown
|
|
46
|
+
/** Display name of the peer, shown until LiveKit reports theirs. */
|
|
67
47
|
peerName?: string | null;
|
|
68
48
|
primaryColor?: string;
|
|
69
49
|
icons?: VideoCallIcons;
|
|
70
|
-
/**
|
|
71
|
-
startWithCameraOn?: boolean;
|
|
72
|
-
/** Start with the microphone on. Default true. */
|
|
73
|
-
startWithMicOn?: boolean;
|
|
74
|
-
/** Render the compact floating-panel layout instead of the full screen. */
|
|
75
|
-
minimized?: boolean;
|
|
76
|
-
/** Toggle between full and minimized layouts (keeps the session mounted). */
|
|
50
|
+
/** Minimize to the header strip (ActiveCallBar). */
|
|
77
51
|
onToggleMinimize?: () => void;
|
|
78
|
-
onConnected?: () => void;
|
|
79
|
-
onError?: (message: string) => void;
|
|
80
52
|
onHangup: () => void;
|
|
81
53
|
}
|
|
82
54
|
|
|
83
55
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
56
|
+
* Seconds since the call truly connected, counted from the server-set
|
|
57
|
+
* `joinedAt` anchor so both sides show the same time (web's
|
|
58
|
+
* useSharedCallElapsed). Null until the peer is in.
|
|
59
|
+
*/
|
|
60
|
+
export const useSharedCallElapsed = (anchorAt: number | null): number | null => {
|
|
61
|
+
const [, tick] = useState(0);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (anchorAt === null) {return;}
|
|
64
|
+
const id = setInterval(() => tick((n) => n + 1), 1000);
|
|
65
|
+
return () => clearInterval(id);
|
|
66
|
+
}, [anchorAt]);
|
|
67
|
+
if (anchorAt === null) {return null;}
|
|
68
|
+
return Math.max(0, Math.floor((Date.now() - anchorAt) / 1000));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const formatCallDuration = (totalSeconds: number): string => {
|
|
72
|
+
const safe = Math.max(0, Math.floor(totalSeconds));
|
|
73
|
+
const hours = Math.floor(safe / 3600);
|
|
74
|
+
const minutes = Math.floor((safe % 3600) / 60);
|
|
75
|
+
const seconds = safe % 60;
|
|
76
|
+
const mm = String(minutes).padStart(2, '0');
|
|
77
|
+
const ss = String(seconds).padStart(2, '0');
|
|
78
|
+
return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The call screen, React Native edition. Pure view over the LiveKit session
|
|
83
|
+
* owned by callSessionController.ts: it can unmount and remount (minimize,
|
|
84
|
+
* leaving the chat screen) without touching the connection.
|
|
92
85
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
86
|
+
* The web SDK builds this on `@livekit/components-react` hooks, which
|
|
87
|
+
* assume a DOM. Only the video surface comes from `@livekit/react-native`
|
|
88
|
+
* (`<VideoTrack />`, `trackRef` prop, >= 2.7).
|
|
95
89
|
*/
|
|
96
90
|
export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
97
|
-
token,
|
|
98
|
-
livekitUrl,
|
|
99
91
|
kind = 'video',
|
|
100
92
|
peerName,
|
|
101
93
|
primaryColor = DEFAULT_PRIMARY,
|
|
102
94
|
icons,
|
|
103
|
-
startWithCameraOn = true,
|
|
104
|
-
startWithMicOn = true,
|
|
105
|
-
minimized = false,
|
|
106
95
|
onToggleMinimize,
|
|
107
|
-
onConnected,
|
|
108
|
-
onError,
|
|
109
96
|
onHangup,
|
|
110
97
|
}) => {
|
|
111
98
|
const t = useT();
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const [
|
|
119
|
-
const [speakerOn, setSpeakerOn] = useState(kind === 'video');
|
|
120
|
-
const [remoteVideo, setRemoteVideo] = useState<any>(null);
|
|
121
|
-
const [localVideo, setLocalVideo] = useState<any>(null);
|
|
122
|
-
const [remoteName, setRemoteName] = useState<string | null>(null);
|
|
123
|
-
const [remoteJoined, setRemoteJoined] = useState(false);
|
|
124
|
-
|
|
125
|
-
// Callbacks are read through a ref inside the connect effect so that a
|
|
126
|
-
// parent re-render (new inline arrow props) never re-runs connect and
|
|
127
|
-
// tears down a live call. Same guard the web session uses.
|
|
128
|
-
const callbacksRef = useRef({ onConnected, onError, onHangup });
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
callbacksRef.current = { onConnected, onError, onHangup };
|
|
131
|
-
}, [onConnected, onError, onHangup]);
|
|
132
|
-
|
|
133
|
-
// ---- track plumbing -------------------------------------------------
|
|
134
|
-
|
|
135
|
-
// Build the { participant, publication, source } shape `<VideoTrack />`
|
|
136
|
-
// expects, from whatever camera publication a participant currently has.
|
|
137
|
-
const toTrackRef = useCallback(
|
|
138
|
-
(participant: any, Track: any) => {
|
|
139
|
-
if (!participant) {return null;}
|
|
140
|
-
const publication = participant.getTrackPublication?.(Track.Source.Camera);
|
|
141
|
-
if (!publication?.track) {return null;}
|
|
142
|
-
return { participant, publication, source: Track.Source.Camera };
|
|
143
|
-
},
|
|
144
|
-
[]
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const resyncTracks = useCallback(() => {
|
|
148
|
-
const room = roomRef.current;
|
|
149
|
-
if (!room || !runtime) {return;}
|
|
150
|
-
const { Track } = runtime;
|
|
151
|
-
|
|
152
|
-
const remote = Array.from(room.remoteParticipants?.values?.() || [])[0] as any;
|
|
153
|
-
setRemoteJoined(!!remote);
|
|
154
|
-
setRemoteName(remote?.name || remote?.identity || null);
|
|
155
|
-
setRemoteVideo(toTrackRef(remote, Track));
|
|
156
|
-
setLocalVideo(toTrackRef(room.localParticipant, Track));
|
|
157
|
-
}, [runtime, toTrackRef]);
|
|
158
|
-
|
|
159
|
-
// ---- connect / teardown ---------------------------------------------
|
|
160
|
-
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (!runtime) {
|
|
163
|
-
callbacksRef.current.onError?.(t('call.error.sdkMissing'));
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (!token || !livekitUrl) {
|
|
167
|
-
callbacksRef.current.onError?.(t('call.error.missingConfig'));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
ensureLiveKitGlobals(runtime);
|
|
172
|
-
|
|
173
|
-
const { Room, RoomEvent, AudioSession } = runtime;
|
|
174
|
-
let disposed = false;
|
|
175
|
-
|
|
176
|
-
// adaptiveStream + dynacast keep mobile data and battery sane: the SDK
|
|
177
|
-
// drops resolution for a small tile and stops publishing layers nobody
|
|
178
|
-
// subscribes to. Both are safe defaults for 1:1.
|
|
179
|
-
const room = new Room({
|
|
180
|
-
adaptiveStream: true,
|
|
181
|
-
dynacast: true,
|
|
182
|
-
});
|
|
183
|
-
roomRef.current = room;
|
|
184
|
-
|
|
185
|
-
const onStateChange = () => {
|
|
186
|
-
if (disposed) {return;}
|
|
187
|
-
resyncTracks();
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
room
|
|
191
|
-
.on(RoomEvent.TrackSubscribed, onStateChange)
|
|
192
|
-
.on(RoomEvent.TrackUnsubscribed, onStateChange)
|
|
193
|
-
.on(RoomEvent.LocalTrackPublished, onStateChange)
|
|
194
|
-
.on(RoomEvent.LocalTrackUnpublished, onStateChange)
|
|
195
|
-
.on(RoomEvent.TrackMuted, onStateChange)
|
|
196
|
-
.on(RoomEvent.TrackUnmuted, onStateChange)
|
|
197
|
-
.on(RoomEvent.ParticipantConnected, onStateChange)
|
|
198
|
-
.on(RoomEvent.MediaDevicesError, (error: unknown) => {
|
|
199
|
-
if (disposed) {return;}
|
|
200
|
-
setDeviceError(humanizeDeviceError(error, t));
|
|
201
|
-
})
|
|
202
|
-
.on(RoomEvent.ParticipantDisconnected, () => {
|
|
203
|
-
if (disposed) {return;}
|
|
204
|
-
resyncTracks();
|
|
205
|
-
// 1:1 semantics: the other side leaving IS the end of the call.
|
|
206
|
-
// Without this the local user sits on a black canvas until they
|
|
207
|
-
// hang up manually.
|
|
208
|
-
callbacksRef.current.onHangup?.();
|
|
209
|
-
})
|
|
210
|
-
.on(RoomEvent.Disconnected, () => {
|
|
211
|
-
if (disposed) {return;}
|
|
212
|
-
setConnected(false);
|
|
213
|
-
callbacksRef.current.onHangup?.();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
(async () => {
|
|
217
|
-
// Joining the LiveKit room is the only step whose failure should fail
|
|
218
|
-
// the call (bad token, network, server down). Everything after this
|
|
219
|
-
// is best-effort device setup — see the separate try/catches below.
|
|
220
|
-
try {
|
|
221
|
-
// Claims audio focus and sets the right native audio mode. Must
|
|
222
|
-
// start BEFORE connecting or the first seconds route to the
|
|
223
|
-
// earpiece on Android even for a video call.
|
|
224
|
-
await AudioSession.startAudioSession();
|
|
225
|
-
await room.connect(livekitUrl, token);
|
|
226
|
-
} catch (error: any) {
|
|
227
|
-
if (disposed) {return;}
|
|
228
|
-
callbacksRef.current.onError?.(
|
|
229
|
-
String(error?.message || '') || t('call.error.connectFailed')
|
|
230
|
-
);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (disposed) {return;}
|
|
234
|
-
|
|
235
|
-
// Devices are best-effort: if the mic/camera prompt is blocked, or —
|
|
236
|
-
// notably — this is the iOS Simulator (no real camera hardware at
|
|
237
|
-
// all), the call still connects. The user can hear/be heard on
|
|
238
|
-
// audio and re-enable the camera later; failing the whole call here
|
|
239
|
-
// (as an earlier version did, sharing one try/catch with `connect`)
|
|
240
|
-
// meant every video call died before ever reaching the ring-answered
|
|
241
|
-
// screen — indistinguishable from "Accept doesn't work".
|
|
242
|
-
try {
|
|
243
|
-
await room.localParticipant.setMicrophoneEnabled(startWithMicOn);
|
|
244
|
-
} catch (error) {
|
|
245
|
-
if (!disposed) {setDeviceError(humanizeDeviceError(error, t));}
|
|
246
|
-
}
|
|
247
|
-
if (kind === 'video') {
|
|
248
|
-
try {
|
|
249
|
-
await room.localParticipant.setCameraEnabled(startWithCameraOn);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
if (!disposed) {setDeviceError(humanizeDeviceError(error, t));}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (disposed) {return;}
|
|
255
|
-
|
|
256
|
-
// Video calls belong on the loudspeaker, audio calls on the
|
|
257
|
-
// earpiece, matching what every native dialer does.
|
|
258
|
-
try {
|
|
259
|
-
await AudioSession.setAppleAudioConfiguration?.({
|
|
260
|
-
audioCategory: 'playAndRecord',
|
|
261
|
-
audioMode: kind === 'video' ? 'videoChat' : 'voiceChat',
|
|
262
|
-
});
|
|
263
|
-
} catch {
|
|
264
|
-
// iOS-only helper, absent on Android builds of the SDK.
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
setConnected(true);
|
|
268
|
-
resyncTracks();
|
|
269
|
-
callbacksRef.current.onConnected?.();
|
|
270
|
-
})();
|
|
271
|
-
|
|
272
|
-
return () => {
|
|
273
|
-
disposed = true;
|
|
274
|
-
try {
|
|
275
|
-
room.removeAllListeners?.();
|
|
276
|
-
void room.disconnect();
|
|
277
|
-
} catch {
|
|
278
|
-
// Already gone.
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
void AudioSession.stopAudioSession();
|
|
282
|
-
} catch {
|
|
283
|
-
// Never let teardown throw, it runs during unmount.
|
|
284
|
-
}
|
|
285
|
-
roomRef.current = null;
|
|
286
|
-
};
|
|
287
|
-
// Connect exactly once per (url, token, kind). Everything else is read
|
|
288
|
-
// through refs, see callbacksRef above.
|
|
289
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
|
-
}, [runtime, livekitUrl, token, kind]);
|
|
291
|
-
|
|
292
|
-
// ---- controls --------------------------------------------------------
|
|
293
|
-
|
|
294
|
-
const toggleMic = useCallback(async () => {
|
|
295
|
-
const room = roomRef.current;
|
|
296
|
-
if (!room) {return;}
|
|
297
|
-
const next = !micOn;
|
|
298
|
-
setMicOn(next);
|
|
299
|
-
try {
|
|
300
|
-
await room.localParticipant.setMicrophoneEnabled(next);
|
|
301
|
-
} catch {
|
|
302
|
-
setMicOn(!next);
|
|
303
|
-
}
|
|
304
|
-
}, [micOn]);
|
|
305
|
-
|
|
306
|
-
const toggleCamera = useCallback(async () => {
|
|
307
|
-
const room = roomRef.current;
|
|
308
|
-
if (!room) {return;}
|
|
309
|
-
const next = !cameraOn;
|
|
310
|
-
setCameraOn(next);
|
|
311
|
-
try {
|
|
312
|
-
await room.localParticipant.setCameraEnabled(next);
|
|
313
|
-
resyncTracks();
|
|
314
|
-
} catch {
|
|
315
|
-
setCameraOn(!next);
|
|
316
|
-
}
|
|
317
|
-
}, [cameraOn, resyncTracks]);
|
|
318
|
-
|
|
319
|
-
const switchCamera = useCallback(async () => {
|
|
320
|
-
const room = roomRef.current;
|
|
321
|
-
if (!room || !runtime) {return;}
|
|
322
|
-
try {
|
|
323
|
-
const publication = room.localParticipant.getTrackPublication?.(
|
|
324
|
-
runtime.Track.Source.Camera
|
|
325
|
-
);
|
|
326
|
-
const track = publication?.track;
|
|
327
|
-
// `restartTrack` with the flipped facingMode is the portable way to
|
|
328
|
-
// switch cameras; the RN SDK's `_switchCamera` helper is internal and
|
|
329
|
-
// has moved between versions.
|
|
330
|
-
await track?.restartTrack?.({
|
|
331
|
-
facingMode: track?.mediaStreamTrack?.getSettings?.().facingMode ===
|
|
332
|
-
'environment'
|
|
333
|
-
? 'user'
|
|
334
|
-
: 'environment',
|
|
335
|
-
});
|
|
336
|
-
resyncTracks();
|
|
337
|
-
} catch {
|
|
338
|
-
// Device has a single camera, or the track is mid-restart.
|
|
339
|
-
}
|
|
340
|
-
}, [runtime, resyncTracks]);
|
|
341
|
-
|
|
342
|
-
const toggleSpeaker = useCallback(async () => {
|
|
343
|
-
const next = !speakerOn;
|
|
344
|
-
setSpeakerOn(next);
|
|
345
|
-
if (!runtime) {return;}
|
|
346
|
-
try {
|
|
347
|
-
if (Platform.OS === 'android') {
|
|
348
|
-
// NB: not `setAndroidAudioConfiguration` — that method has never
|
|
349
|
-
// existed on AudioSession (the old `?.` call silently no-oped, so
|
|
350
|
-
// the speaker icon toggled while audio stayed on the earpiece).
|
|
351
|
-
// `selectAudioOutput` routes directly; on Android its device ids
|
|
352
|
-
// are the literal strings 'speaker' / 'earpiece'.
|
|
353
|
-
await runtime.AudioSession.selectAudioOutput(
|
|
354
|
-
next ? 'speaker' : 'earpiece'
|
|
355
|
-
);
|
|
356
|
-
} else {
|
|
357
|
-
await runtime.AudioSession.setAppleAudioConfiguration?.({
|
|
358
|
-
audioCategory: 'playAndRecord',
|
|
359
|
-
audioCategoryOptions: next ? ['defaultToSpeaker'] : [],
|
|
360
|
-
audioMode: next ? 'videoChat' : 'voiceChat',
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
} catch {
|
|
364
|
-
setSpeakerOn(!next);
|
|
365
|
-
}
|
|
366
|
-
}, [runtime, speakerOn]);
|
|
367
|
-
|
|
368
|
-
// ---- render ----------------------------------------------------------
|
|
99
|
+
const insets = useSafeAreaInsets();
|
|
100
|
+
const runtime = loadLiveKit();
|
|
101
|
+
const session = useCallSession();
|
|
102
|
+
const selfView = useDraggable();
|
|
103
|
+
const elapsed = useSharedCallElapsed(session.anchorAt);
|
|
104
|
+
// Telegram-style: tap the small tile to swap it with the big one.
|
|
105
|
+
const [swapped, setSwapped] = useState(false);
|
|
369
106
|
|
|
370
107
|
if (!runtime) {
|
|
371
108
|
return (
|
|
@@ -376,114 +113,153 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
376
113
|
}
|
|
377
114
|
|
|
378
115
|
const { VideoTrack } = runtime;
|
|
379
|
-
const
|
|
116
|
+
const {
|
|
117
|
+
connected,
|
|
118
|
+
remoteJoined,
|
|
119
|
+
remoteVideo,
|
|
120
|
+
localVideo,
|
|
121
|
+
micOn,
|
|
122
|
+
cameraOn,
|
|
123
|
+
speakerOn,
|
|
124
|
+
deviceError,
|
|
125
|
+
remoteMicMuted,
|
|
126
|
+
facingMode,
|
|
127
|
+
} = session;
|
|
128
|
+
// The call slice's name (the chat title) first, as web does: LiveKit's
|
|
129
|
+
// participant name is often just the opaque xmpp identity.
|
|
130
|
+
const displayName = peerName || session.remoteName || t('call.unknownCaller');
|
|
380
131
|
const isVideo = kind === 'video';
|
|
381
132
|
const showRemoteVideo = isVideo && !!remoteVideo;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
style={[styles.miniButton, { backgroundColor: CONTROL_IDLE }]}
|
|
403
|
-
>
|
|
404
|
-
<ExpandIcon color={TEXT_ON_DARK} size={20} />
|
|
405
|
-
</TouchableOpacity>
|
|
406
|
-
<TouchableOpacity
|
|
407
|
-
accessibilityRole="button"
|
|
408
|
-
accessibilityLabel={t('call.action.hangUp')}
|
|
409
|
-
onPress={onHangup}
|
|
410
|
-
style={[styles.miniButton, { backgroundColor: DANGER }]}
|
|
411
|
-
>
|
|
412
|
-
{icons?.hangup ?? <HangUpIcon color={TEXT_ON_DARK} size={20} />}
|
|
413
|
-
</TouchableOpacity>
|
|
414
|
-
</View>
|
|
415
|
-
);
|
|
416
|
-
}
|
|
133
|
+
const localLive = isVideo && cameraOn && !!localVideo;
|
|
134
|
+
// Swapping only means something while both sides have a picture; if
|
|
135
|
+
// either goes away, fall back to the default layout on its own.
|
|
136
|
+
const canSwap = showRemoteVideo && localLive;
|
|
137
|
+
const isSwapped = swapped && canSwap;
|
|
138
|
+
const mainVideo = isSwapped ? localVideo : showRemoteVideo ? remoteVideo : null;
|
|
139
|
+
const pipVideo = isSwapped ? remoteVideo : localLive ? localVideo : null;
|
|
140
|
+
// Only the front camera is mirrored, as in every camera app.
|
|
141
|
+
const selfMirror = facingMode === 'user';
|
|
142
|
+
|
|
143
|
+
const statusText =
|
|
144
|
+
elapsed !== null
|
|
145
|
+
? formatCallDuration(elapsed)
|
|
146
|
+
: !connected
|
|
147
|
+
? t('call.status.connecting')
|
|
148
|
+
: !remoteJoined
|
|
149
|
+
? t('call.status.waitingForPeer')
|
|
150
|
+
: t('call.status.inCall');
|
|
151
|
+
|
|
152
|
+
const peerMutedLabel = t('call.peerMuted', { name: displayName });
|
|
417
153
|
|
|
418
154
|
return (
|
|
419
155
|
<View style={styles.canvas}>
|
|
420
|
-
{!!deviceError && (
|
|
421
|
-
<TouchableOpacity
|
|
422
|
-
style={styles.deviceErrorBanner}
|
|
423
|
-
onPress={() => setDeviceError(null)}
|
|
424
|
-
accessibilityRole="button"
|
|
425
|
-
>
|
|
426
|
-
<Text style={styles.deviceErrorText} numberOfLines={2}>
|
|
427
|
-
{deviceError}
|
|
428
|
-
</Text>
|
|
429
|
-
</TouchableOpacity>
|
|
430
|
-
)}
|
|
431
156
|
{/* Stage */}
|
|
432
157
|
<View style={styles.stage}>
|
|
433
|
-
{
|
|
434
|
-
<VideoTrack
|
|
158
|
+
{mainVideo ? (
|
|
159
|
+
<VideoTrack
|
|
160
|
+
trackRef={mainVideo}
|
|
161
|
+
style={styles.remoteVideo}
|
|
162
|
+
objectFit="cover"
|
|
163
|
+
mirror={isSwapped && selfMirror}
|
|
164
|
+
zOrder={0}
|
|
165
|
+
/>
|
|
435
166
|
) : (
|
|
436
167
|
<View style={styles.centered}>
|
|
437
168
|
<ProfileImagePlaceholder name={displayName} size={120} />
|
|
438
169
|
<Text style={styles.stageName}>{displayName}</Text>
|
|
439
170
|
<Text style={styles.stageStatus}>
|
|
440
|
-
{
|
|
441
|
-
? t('call.status.
|
|
442
|
-
:
|
|
443
|
-
? t('call.status.waitingForPeer')
|
|
444
|
-
: isVideo
|
|
445
|
-
? t('call.status.cameraOff')
|
|
446
|
-
: t('call.status.inCall')}
|
|
171
|
+
{isVideo && remoteJoined && elapsed !== null
|
|
172
|
+
? t('call.status.cameraOff')
|
|
173
|
+
: statusText}
|
|
447
174
|
</Text>
|
|
448
175
|
{!connected && (
|
|
449
|
-
<ActivityIndicator
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
176
|
+
<ActivityIndicator color={primaryColor} style={styles.spinner} size="small" />
|
|
177
|
+
)}
|
|
178
|
+
{!micOn && (
|
|
179
|
+
<View style={[styles.pill, styles.pillDanger]}>
|
|
180
|
+
<Text style={styles.pillDangerText}>{t('call.selfMuted')}</Text>
|
|
181
|
+
</View>
|
|
182
|
+
)}
|
|
183
|
+
{remoteMicMuted && !showRemoteVideo && (
|
|
184
|
+
<View style={styles.pill}>
|
|
185
|
+
<Text style={styles.pillText}>{peerMutedLabel}</Text>
|
|
186
|
+
</View>
|
|
454
187
|
)}
|
|
455
188
|
</View>
|
|
456
189
|
)}
|
|
457
190
|
|
|
458
|
-
{/*
|
|
459
|
-
{
|
|
460
|
-
<View style={styles.
|
|
461
|
-
<
|
|
191
|
+
{/* Header strip: who, how long, and whether they can hear us. */}
|
|
192
|
+
{mainVideo && (
|
|
193
|
+
<View style={[styles.header, { paddingTop: 14 + insets.top }]}>
|
|
194
|
+
<Text style={styles.headerName} numberOfLines={1}>
|
|
195
|
+
{displayName}
|
|
196
|
+
</Text>
|
|
197
|
+
<Text style={styles.headerStatus}>{statusText}</Text>
|
|
198
|
+
{remoteMicMuted && (
|
|
199
|
+
<View style={[styles.pill, styles.headerPill]}>
|
|
200
|
+
<MicOffIcon color={TEXT_ON_DARK} size={14} />
|
|
201
|
+
<Text style={styles.pillText}>{peerMutedLabel}</Text>
|
|
202
|
+
</View>
|
|
203
|
+
)}
|
|
462
204
|
</View>
|
|
463
205
|
)}
|
|
464
206
|
|
|
207
|
+
{/* Picture-in-picture: draggable like web's self-view, tap to swap
|
|
208
|
+
it with the main video. */}
|
|
209
|
+
{pipVideo && (
|
|
210
|
+
<Animated.View
|
|
211
|
+
{...selfView.panHandlers}
|
|
212
|
+
style={[styles.pip, { top: 16 + insets.top }, selfView.style]}
|
|
213
|
+
>
|
|
214
|
+
<Pressable
|
|
215
|
+
style={styles.pipVideo}
|
|
216
|
+
disabled={!canSwap}
|
|
217
|
+
onPress={() => setSwapped((value) => !value)}
|
|
218
|
+
accessibilityRole="button"
|
|
219
|
+
accessibilityLabel={t('call.action.swapVideo')}
|
|
220
|
+
>
|
|
221
|
+
<VideoTrack
|
|
222
|
+
trackRef={pipVideo}
|
|
223
|
+
style={styles.pipVideo}
|
|
224
|
+
objectFit="cover"
|
|
225
|
+
mirror={!isSwapped && selfMirror}
|
|
226
|
+
zOrder={1}
|
|
227
|
+
/>
|
|
228
|
+
</Pressable>
|
|
229
|
+
</Animated.View>
|
|
230
|
+
)}
|
|
231
|
+
|
|
465
232
|
{onToggleMinimize && (
|
|
466
233
|
<TouchableOpacity
|
|
467
234
|
accessibilityRole="button"
|
|
468
235
|
accessibilityLabel={t('call.action.minimize')}
|
|
469
236
|
onPress={onToggleMinimize}
|
|
470
|
-
style={styles.minimizeButton}
|
|
237
|
+
style={[styles.minimizeButton, { top: 16 + insets.top }]}
|
|
471
238
|
>
|
|
472
239
|
<MinimizeIcon color={TEXT_ON_DARK} size={20} />
|
|
473
240
|
</TouchableOpacity>
|
|
474
241
|
)}
|
|
242
|
+
|
|
243
|
+
{!!deviceError && (
|
|
244
|
+
<TouchableOpacity
|
|
245
|
+
style={[styles.deviceErrorBanner, { top: 64 + insets.top }]}
|
|
246
|
+
onPress={dismissCallDeviceError}
|
|
247
|
+
accessibilityRole="button"
|
|
248
|
+
>
|
|
249
|
+
<Text style={styles.deviceErrorText} numberOfLines={2}>
|
|
250
|
+
{deviceError}
|
|
251
|
+
</Text>
|
|
252
|
+
</TouchableOpacity>
|
|
253
|
+
)}
|
|
475
254
|
</View>
|
|
476
255
|
|
|
477
256
|
{/* Control bar */}
|
|
478
|
-
<View style={styles.controls}>
|
|
257
|
+
<View style={[styles.controls, { paddingBottom: 20 + insets.bottom }]}>
|
|
479
258
|
<TouchableOpacity
|
|
480
259
|
accessibilityRole="button"
|
|
481
260
|
accessibilityLabel={micOn ? t('call.action.muteMic') : t('call.action.unmuteMic')}
|
|
482
|
-
onPress={
|
|
483
|
-
style={[
|
|
484
|
-
styles.controlButton,
|
|
485
|
-
{ backgroundColor: micOn ? CONTROL_IDLE : DANGER },
|
|
486
|
-
]}
|
|
261
|
+
onPress={toggleCallMic}
|
|
262
|
+
style={[styles.controlButton, { backgroundColor: micOn ? CONTROL_IDLE : DANGER }]}
|
|
487
263
|
>
|
|
488
264
|
{micOn
|
|
489
265
|
? (icons?.micOn ?? <MicOnIcon color={TEXT_ON_DARK} />)
|
|
@@ -496,11 +272,8 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
496
272
|
accessibilityLabel={
|
|
497
273
|
cameraOn ? t('call.action.stopCamera') : t('call.action.startCamera')
|
|
498
274
|
}
|
|
499
|
-
onPress={
|
|
500
|
-
style={[
|
|
501
|
-
styles.controlButton,
|
|
502
|
-
{ backgroundColor: cameraOn ? CONTROL_IDLE : DANGER },
|
|
503
|
-
]}
|
|
275
|
+
onPress={toggleCallCamera}
|
|
276
|
+
style={[styles.controlButton, { backgroundColor: cameraOn ? CONTROL_IDLE : DANGER }]}
|
|
504
277
|
>
|
|
505
278
|
{cameraOn
|
|
506
279
|
? (icons?.cameraOn ?? <CameraOnIcon color={TEXT_ON_DARK} />)
|
|
@@ -512,7 +285,7 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
512
285
|
<TouchableOpacity
|
|
513
286
|
accessibilityRole="button"
|
|
514
287
|
accessibilityLabel={t('call.action.switchCamera')}
|
|
515
|
-
onPress={
|
|
288
|
+
onPress={switchCallCamera}
|
|
516
289
|
style={[styles.controlButton, { backgroundColor: CONTROL_IDLE }]}
|
|
517
290
|
>
|
|
518
291
|
<SwitchCameraIcon color={TEXT_ON_DARK} />
|
|
@@ -521,17 +294,11 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
521
294
|
|
|
522
295
|
<TouchableOpacity
|
|
523
296
|
accessibilityRole="button"
|
|
524
|
-
accessibilityLabel={
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
onPress={toggleSpeaker}
|
|
297
|
+
accessibilityLabel={speakerOn ? t('call.action.speakerOff') : t('call.action.speakerOn')}
|
|
298
|
+
onPress={toggleCallSpeaker}
|
|
528
299
|
style={[styles.controlButton, { backgroundColor: CONTROL_IDLE }]}
|
|
529
300
|
>
|
|
530
|
-
{speakerOn ?
|
|
531
|
-
<SpeakerIcon color={TEXT_ON_DARK} />
|
|
532
|
-
) : (
|
|
533
|
-
<SpeakerOffIcon color={TEXT_ON_DARK} />
|
|
534
|
-
)}
|
|
301
|
+
{speakerOn ? <SpeakerIcon color={TEXT_ON_DARK} /> : <SpeakerOffIcon color={TEXT_ON_DARK} />}
|
|
535
302
|
</TouchableOpacity>
|
|
536
303
|
|
|
537
304
|
<TouchableOpacity
|
|
@@ -557,6 +324,7 @@ const styles = StyleSheet.create({
|
|
|
557
324
|
alignItems: 'center',
|
|
558
325
|
justifyContent: 'center',
|
|
559
326
|
gap: 12,
|
|
327
|
+
paddingHorizontal: 24,
|
|
560
328
|
},
|
|
561
329
|
stage: {
|
|
562
330
|
flex: 1,
|
|
@@ -571,18 +339,66 @@ const styles = StyleSheet.create({
|
|
|
571
339
|
color: TEXT_ON_DARK,
|
|
572
340
|
fontSize: 22,
|
|
573
341
|
marginTop: 16,
|
|
342
|
+
textAlign: 'center',
|
|
574
343
|
},
|
|
575
344
|
stageStatus: {
|
|
576
345
|
color: TEXT_MUTED,
|
|
577
346
|
fontSize: 14,
|
|
347
|
+
fontVariant: ['tabular-nums'],
|
|
578
348
|
},
|
|
579
349
|
spinner: {
|
|
580
350
|
marginTop: 8,
|
|
581
351
|
},
|
|
352
|
+
header: {
|
|
353
|
+
position: 'absolute',
|
|
354
|
+
top: 0,
|
|
355
|
+
left: 0,
|
|
356
|
+
right: 0,
|
|
357
|
+
paddingHorizontal: 64,
|
|
358
|
+
paddingBottom: 14,
|
|
359
|
+
alignItems: 'center',
|
|
360
|
+
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
361
|
+
gap: 2,
|
|
362
|
+
},
|
|
363
|
+
headerName: {
|
|
364
|
+
color: TEXT_ON_DARK,
|
|
365
|
+
fontSize: 15,
|
|
366
|
+
fontWeight: '600',
|
|
367
|
+
},
|
|
368
|
+
headerStatus: {
|
|
369
|
+
color: 'rgba(255,255,255,0.7)',
|
|
370
|
+
fontSize: 13,
|
|
371
|
+
fontVariant: ['tabular-nums'],
|
|
372
|
+
},
|
|
373
|
+
headerPill: {
|
|
374
|
+
marginTop: 4,
|
|
375
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
376
|
+
},
|
|
377
|
+
pill: {
|
|
378
|
+
flexDirection: 'row',
|
|
379
|
+
alignItems: 'center',
|
|
380
|
+
gap: 6,
|
|
381
|
+
paddingVertical: 4,
|
|
382
|
+
paddingHorizontal: 10,
|
|
383
|
+
borderRadius: 999,
|
|
384
|
+
backgroundColor: CONTROL_IDLE,
|
|
385
|
+
},
|
|
386
|
+
pillText: {
|
|
387
|
+
color: TEXT_ON_DARK,
|
|
388
|
+
fontSize: 12,
|
|
389
|
+
},
|
|
390
|
+
pillDanger: {
|
|
391
|
+
backgroundColor: 'rgba(229, 57, 53, 0.18)',
|
|
392
|
+
borderWidth: 1,
|
|
393
|
+
borderColor: 'rgba(229, 57, 53, 0.45)',
|
|
394
|
+
},
|
|
395
|
+
pillDangerText: {
|
|
396
|
+
color: '#FF8A80',
|
|
397
|
+
fontSize: 12,
|
|
398
|
+
},
|
|
582
399
|
pip: {
|
|
583
400
|
position: 'absolute',
|
|
584
401
|
right: 16,
|
|
585
|
-
top: 16,
|
|
586
402
|
width: 104,
|
|
587
403
|
height: 148,
|
|
588
404
|
borderRadius: 12,
|
|
@@ -590,6 +406,7 @@ const styles = StyleSheet.create({
|
|
|
590
406
|
backgroundColor: '#000',
|
|
591
407
|
borderWidth: 1,
|
|
592
408
|
borderColor: 'rgba(255,255,255,0.18)',
|
|
409
|
+
zIndex: 5,
|
|
593
410
|
},
|
|
594
411
|
pipVideo: {
|
|
595
412
|
flex: 1,
|
|
@@ -597,20 +414,20 @@ const styles = StyleSheet.create({
|
|
|
597
414
|
minimizeButton: {
|
|
598
415
|
position: 'absolute',
|
|
599
416
|
left: 16,
|
|
600
|
-
top: 16,
|
|
601
417
|
width: 36,
|
|
602
418
|
height: 36,
|
|
603
419
|
borderRadius: 18,
|
|
604
420
|
alignItems: 'center',
|
|
605
421
|
justifyContent: 'center',
|
|
606
422
|
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
423
|
+
zIndex: 6,
|
|
607
424
|
},
|
|
608
425
|
controls: {
|
|
609
426
|
flexDirection: 'row',
|
|
610
427
|
alignItems: 'center',
|
|
611
428
|
justifyContent: 'center',
|
|
612
429
|
gap: 16,
|
|
613
|
-
|
|
430
|
+
paddingTop: 20,
|
|
614
431
|
paddingHorizontal: 16,
|
|
615
432
|
},
|
|
616
433
|
controlButton: {
|
|
@@ -629,11 +446,10 @@ const styles = StyleSheet.create({
|
|
|
629
446
|
textAlign: 'center',
|
|
630
447
|
paddingHorizontal: 24,
|
|
631
448
|
},
|
|
632
|
-
// Dismissible, non-blocking
|
|
633
|
-
//
|
|
449
|
+
// Dismissible, non-blocking: the call is still up underneath; this only
|
|
450
|
+
// flags that camera/mic didn't come up.
|
|
634
451
|
deviceErrorBanner: {
|
|
635
452
|
position: 'absolute',
|
|
636
|
-
top: 16,
|
|
637
453
|
left: 16,
|
|
638
454
|
right: 16,
|
|
639
455
|
zIndex: 10,
|
|
@@ -647,34 +463,6 @@ const styles = StyleSheet.create({
|
|
|
647
463
|
fontSize: 13,
|
|
648
464
|
textAlign: 'center',
|
|
649
465
|
},
|
|
650
|
-
miniBar: {
|
|
651
|
-
flexDirection: 'row',
|
|
652
|
-
alignItems: 'center',
|
|
653
|
-
gap: 12,
|
|
654
|
-
paddingHorizontal: 16,
|
|
655
|
-
paddingVertical: 12,
|
|
656
|
-
borderRadius: 16,
|
|
657
|
-
backgroundColor: SURFACE_DARK,
|
|
658
|
-
},
|
|
659
|
-
miniInfo: {
|
|
660
|
-
flex: 1,
|
|
661
|
-
minWidth: 0,
|
|
662
|
-
},
|
|
663
|
-
miniName: {
|
|
664
|
-
color: TEXT_ON_DARK,
|
|
665
|
-
fontSize: 15,
|
|
666
|
-
},
|
|
667
|
-
miniStatus: {
|
|
668
|
-
color: TEXT_MUTED,
|
|
669
|
-
fontSize: 12,
|
|
670
|
-
},
|
|
671
|
-
miniButton: {
|
|
672
|
-
width: 40,
|
|
673
|
-
height: 40,
|
|
674
|
-
borderRadius: 20,
|
|
675
|
-
alignItems: 'center',
|
|
676
|
-
justifyContent: 'center',
|
|
677
|
-
},
|
|
678
466
|
});
|
|
679
467
|
|
|
680
468
|
export default VideoCallSession;
|