@ethora/chat-component-rn 26.10.2 → 26.10.3
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/MainComponents/ChatHeader.js +11 -10
- 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/CallButtons.js +3 -1
- package/lib/commonjs/components/VideoCalls/CallButtons.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +123 -111
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js +207 -346
- 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 +431 -0
- package/lib/commonjs/components/VideoCalls/callSessionController.js.map +1 -0
- 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 +18 -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/MainComponents/ChatHeader.js +13 -12
- 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/CallButtons.js +3 -1
- package/lib/module/components/VideoCalls/CallButtons.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallOverlay.js +125 -113
- package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallSession.js +208 -348
- 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 +420 -0
- package/lib/module/components/VideoCalls/callSessionController.js.map +1 -0
- 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 +18 -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/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/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 +9 -20
- 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 +61 -0
- package/lib/typescript/components/VideoCalls/callSessionController.d.ts.map +1 -0
- 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/MainComponents/ChatHeader.tsx +10 -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/CallButtons.tsx +5 -1
- package/src/components/VideoCalls/VideoCallOverlay.tsx +156 -163
- package/src/components/VideoCalls/VideoCallSession.tsx +207 -378
- package/src/components/VideoCalls/callActions.ts +63 -0
- package/src/components/VideoCalls/callSessionController.ts +421 -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 +18 -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,13 @@
|
|
|
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,
|
|
12
5
|
StyleSheet,
|
|
13
6
|
Text,
|
|
14
7
|
TouchableOpacity,
|
|
15
8
|
View,
|
|
16
9
|
} from 'react-native';
|
|
10
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
11
|
import { VideoCallIcons } from '../../types/types';
|
|
18
12
|
import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlaceholder';
|
|
19
13
|
import {
|
|
@@ -28,7 +22,16 @@ import {
|
|
|
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,69 @@ 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
|
-
/** Start with the camera on (video calls). Default true. */
|
|
71
|
-
startWithCameraOn?: boolean;
|
|
72
|
-
/** Start with the microphone on. Default true. */
|
|
73
|
-
startWithMicOn?: boolean;
|
|
74
50
|
/** Render the compact floating-panel layout instead of the full screen. */
|
|
75
51
|
minimized?: boolean;
|
|
76
|
-
/** Toggle between full and minimized layouts
|
|
52
|
+
/** Toggle between full and minimized layouts. */
|
|
77
53
|
onToggleMinimize?: () => void;
|
|
78
|
-
onConnected?: () => void;
|
|
79
|
-
onError?: (message: string) => void;
|
|
80
54
|
onHangup: () => void;
|
|
81
55
|
}
|
|
82
56
|
|
|
83
57
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
58
|
+
* Seconds since the call truly connected, counted from the server-set
|
|
59
|
+
* `joinedAt` anchor so both sides show the same time (web's
|
|
60
|
+
* useSharedCallElapsed). Null until the peer is in.
|
|
61
|
+
*/
|
|
62
|
+
const useSharedCallElapsed = (anchorAt: number | null): number | null => {
|
|
63
|
+
const [, tick] = useState(0);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (anchorAt === null) {return;}
|
|
66
|
+
const id = setInterval(() => tick((n) => n + 1), 1000);
|
|
67
|
+
return () => clearInterval(id);
|
|
68
|
+
}, [anchorAt]);
|
|
69
|
+
if (anchorAt === null) {return null;}
|
|
70
|
+
return Math.max(0, Math.floor((Date.now() - anchorAt) / 1000));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const formatCallDuration = (totalSeconds: number): string => {
|
|
74
|
+
const safe = Math.max(0, Math.floor(totalSeconds));
|
|
75
|
+
const hours = Math.floor(safe / 3600);
|
|
76
|
+
const minutes = Math.floor((safe % 3600) / 60);
|
|
77
|
+
const seconds = safe % 60;
|
|
78
|
+
const mm = String(minutes).padStart(2, '0');
|
|
79
|
+
const ss = String(seconds).padStart(2, '0');
|
|
80
|
+
return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The call screen, React Native edition. Pure view over the LiveKit session
|
|
85
|
+
* owned by callSessionController.ts: it can unmount and remount (minimize,
|
|
86
|
+
* leaving the chat screen) without touching the connection.
|
|
92
87
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
88
|
+
* The web SDK builds this on `@livekit/components-react` hooks, which
|
|
89
|
+
* assume a DOM. Only the video surface comes from `@livekit/react-native`
|
|
90
|
+
* (`<VideoTrack />`, `trackRef` prop, >= 2.7).
|
|
95
91
|
*/
|
|
96
92
|
export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
97
|
-
token,
|
|
98
|
-
livekitUrl,
|
|
99
93
|
kind = 'video',
|
|
100
94
|
peerName,
|
|
101
95
|
primaryColor = DEFAULT_PRIMARY,
|
|
102
96
|
icons,
|
|
103
|
-
startWithCameraOn = true,
|
|
104
|
-
startWithMicOn = true,
|
|
105
97
|
minimized = false,
|
|
106
98
|
onToggleMinimize,
|
|
107
|
-
onConnected,
|
|
108
|
-
onError,
|
|
109
99
|
onHangup,
|
|
110
100
|
}) => {
|
|
111
101
|
const t = useT();
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const [micOn, setMicOn] = useState(startWithMicOn);
|
|
118
|
-
const [cameraOn, setCameraOn] = useState(kind === 'video' && startWithCameraOn);
|
|
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 ----------------------------------------------------------
|
|
102
|
+
const insets = useSafeAreaInsets();
|
|
103
|
+
const runtime = loadLiveKit();
|
|
104
|
+
const session = useCallSession();
|
|
105
|
+
const selfView = useDraggable();
|
|
106
|
+
const elapsed = useSharedCallElapsed(session.anchorAt);
|
|
369
107
|
|
|
370
108
|
if (!runtime) {
|
|
371
109
|
return (
|
|
@@ -376,25 +114,56 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
376
114
|
}
|
|
377
115
|
|
|
378
116
|
const { VideoTrack } = runtime;
|
|
379
|
-
const
|
|
117
|
+
const {
|
|
118
|
+
connected,
|
|
119
|
+
remoteJoined,
|
|
120
|
+
remoteVideo,
|
|
121
|
+
localVideo,
|
|
122
|
+
micOn,
|
|
123
|
+
cameraOn,
|
|
124
|
+
speakerOn,
|
|
125
|
+
deviceError,
|
|
126
|
+
remoteMicMuted,
|
|
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
133
|
|
|
134
|
+
const statusText =
|
|
135
|
+
elapsed !== null
|
|
136
|
+
? formatCallDuration(elapsed)
|
|
137
|
+
: !connected
|
|
138
|
+
? t('call.status.connecting')
|
|
139
|
+
: !remoteJoined
|
|
140
|
+
? t('call.status.waitingForPeer')
|
|
141
|
+
: t('call.status.inCall');
|
|
142
|
+
|
|
143
|
+
const peerMutedLabel = t('call.peerMuted', { name: displayName });
|
|
144
|
+
|
|
383
145
|
if (minimized) {
|
|
384
146
|
return (
|
|
385
147
|
<View style={styles.miniBar}>
|
|
148
|
+
<ProfileImagePlaceholder name={displayName} size={36} />
|
|
386
149
|
<View style={styles.miniInfo}>
|
|
387
150
|
<Text numberOfLines={1} style={styles.miniName}>
|
|
388
151
|
{displayName}
|
|
389
152
|
</Text>
|
|
390
|
-
<Text style={styles.miniStatus}>
|
|
391
|
-
{
|
|
392
|
-
? remoteJoined
|
|
393
|
-
? t('call.status.inCall')
|
|
394
|
-
: t('call.status.waitingForPeer')
|
|
395
|
-
: t('call.status.connecting')}
|
|
153
|
+
<Text numberOfLines={1} style={styles.miniStatus}>
|
|
154
|
+
{remoteMicMuted ? `${statusText} · ${peerMutedLabel}` : statusText}
|
|
396
155
|
</Text>
|
|
397
156
|
</View>
|
|
157
|
+
<TouchableOpacity
|
|
158
|
+
accessibilityRole="button"
|
|
159
|
+
accessibilityLabel={micOn ? t('call.action.muteMic') : t('call.action.unmuteMic')}
|
|
160
|
+
onPress={toggleCallMic}
|
|
161
|
+
style={[styles.miniButton, { backgroundColor: micOn ? CONTROL_IDLE : DANGER }]}
|
|
162
|
+
>
|
|
163
|
+
{micOn
|
|
164
|
+
? (icons?.micOn ?? <MicOnIcon color={TEXT_ON_DARK} size={20} />)
|
|
165
|
+
: (icons?.micOff ?? <MicOffIcon color={TEXT_ON_DARK} size={20} />)}
|
|
166
|
+
</TouchableOpacity>
|
|
398
167
|
<TouchableOpacity
|
|
399
168
|
accessibilityRole="button"
|
|
400
169
|
accessibilityLabel={t('call.action.expand')}
|
|
@@ -417,17 +186,6 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
417
186
|
|
|
418
187
|
return (
|
|
419
188
|
<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
189
|
{/* Stage */}
|
|
432
190
|
<View style={styles.stage}>
|
|
433
191
|
{showRemoteVideo ? (
|
|
@@ -437,29 +195,50 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
437
195
|
<ProfileImagePlaceholder name={displayName} size={120} />
|
|
438
196
|
<Text style={styles.stageName}>{displayName}</Text>
|
|
439
197
|
<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')}
|
|
198
|
+
{isVideo && remoteJoined && elapsed !== null
|
|
199
|
+
? t('call.status.cameraOff')
|
|
200
|
+
: statusText}
|
|
447
201
|
</Text>
|
|
448
202
|
{!connected && (
|
|
449
|
-
<ActivityIndicator
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
203
|
+
<ActivityIndicator color={primaryColor} style={styles.spinner} size="small" />
|
|
204
|
+
)}
|
|
205
|
+
{!micOn && (
|
|
206
|
+
<View style={[styles.pill, styles.pillDanger]}>
|
|
207
|
+
<Text style={styles.pillDangerText}>{t('call.selfMuted')}</Text>
|
|
208
|
+
</View>
|
|
209
|
+
)}
|
|
210
|
+
{remoteMicMuted && !showRemoteVideo && (
|
|
211
|
+
<View style={styles.pill}>
|
|
212
|
+
<Text style={styles.pillText}>{peerMutedLabel}</Text>
|
|
213
|
+
</View>
|
|
214
|
+
)}
|
|
215
|
+
</View>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Header strip: who, how long, and whether they can hear us. */}
|
|
219
|
+
{showRemoteVideo && (
|
|
220
|
+
<View style={[styles.header, { paddingTop: 14 + insets.top }]}>
|
|
221
|
+
<Text style={styles.headerName} numberOfLines={1}>
|
|
222
|
+
{displayName}
|
|
223
|
+
</Text>
|
|
224
|
+
<Text style={styles.headerStatus}>{statusText}</Text>
|
|
225
|
+
{remoteMicMuted && (
|
|
226
|
+
<View style={[styles.pill, styles.headerPill]}>
|
|
227
|
+
<MicOffIcon color={TEXT_ON_DARK} size={14} />
|
|
228
|
+
<Text style={styles.pillText}>{peerMutedLabel}</Text>
|
|
229
|
+
</View>
|
|
454
230
|
)}
|
|
455
231
|
</View>
|
|
456
232
|
)}
|
|
457
233
|
|
|
458
|
-
{/* Local preview, picture-in-picture */}
|
|
234
|
+
{/* Local preview, picture-in-picture, draggable like web's self-view. */}
|
|
459
235
|
{isVideo && cameraOn && localVideo && (
|
|
460
|
-
<View
|
|
236
|
+
<Animated.View
|
|
237
|
+
{...selfView.panHandlers}
|
|
238
|
+
style={[styles.pip, { top: 16 + insets.top }, selfView.style]}
|
|
239
|
+
>
|
|
461
240
|
<VideoTrack trackRef={localVideo} style={styles.pipVideo} objectFit="cover" mirror />
|
|
462
|
-
</View>
|
|
241
|
+
</Animated.View>
|
|
463
242
|
)}
|
|
464
243
|
|
|
465
244
|
{onToggleMinimize && (
|
|
@@ -467,23 +246,32 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
467
246
|
accessibilityRole="button"
|
|
468
247
|
accessibilityLabel={t('call.action.minimize')}
|
|
469
248
|
onPress={onToggleMinimize}
|
|
470
|
-
style={styles.minimizeButton}
|
|
249
|
+
style={[styles.minimizeButton, { top: 16 + insets.top }]}
|
|
471
250
|
>
|
|
472
251
|
<MinimizeIcon color={TEXT_ON_DARK} size={20} />
|
|
473
252
|
</TouchableOpacity>
|
|
474
253
|
)}
|
|
254
|
+
|
|
255
|
+
{!!deviceError && (
|
|
256
|
+
<TouchableOpacity
|
|
257
|
+
style={[styles.deviceErrorBanner, { top: 64 + insets.top }]}
|
|
258
|
+
onPress={dismissCallDeviceError}
|
|
259
|
+
accessibilityRole="button"
|
|
260
|
+
>
|
|
261
|
+
<Text style={styles.deviceErrorText} numberOfLines={2}>
|
|
262
|
+
{deviceError}
|
|
263
|
+
</Text>
|
|
264
|
+
</TouchableOpacity>
|
|
265
|
+
)}
|
|
475
266
|
</View>
|
|
476
267
|
|
|
477
268
|
{/* Control bar */}
|
|
478
|
-
<View style={styles.controls}>
|
|
269
|
+
<View style={[styles.controls, { paddingBottom: 20 + insets.bottom }]}>
|
|
479
270
|
<TouchableOpacity
|
|
480
271
|
accessibilityRole="button"
|
|
481
272
|
accessibilityLabel={micOn ? t('call.action.muteMic') : t('call.action.unmuteMic')}
|
|
482
|
-
onPress={
|
|
483
|
-
style={[
|
|
484
|
-
styles.controlButton,
|
|
485
|
-
{ backgroundColor: micOn ? CONTROL_IDLE : DANGER },
|
|
486
|
-
]}
|
|
273
|
+
onPress={toggleCallMic}
|
|
274
|
+
style={[styles.controlButton, { backgroundColor: micOn ? CONTROL_IDLE : DANGER }]}
|
|
487
275
|
>
|
|
488
276
|
{micOn
|
|
489
277
|
? (icons?.micOn ?? <MicOnIcon color={TEXT_ON_DARK} />)
|
|
@@ -496,11 +284,8 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
496
284
|
accessibilityLabel={
|
|
497
285
|
cameraOn ? t('call.action.stopCamera') : t('call.action.startCamera')
|
|
498
286
|
}
|
|
499
|
-
onPress={
|
|
500
|
-
style={[
|
|
501
|
-
styles.controlButton,
|
|
502
|
-
{ backgroundColor: cameraOn ? CONTROL_IDLE : DANGER },
|
|
503
|
-
]}
|
|
287
|
+
onPress={toggleCallCamera}
|
|
288
|
+
style={[styles.controlButton, { backgroundColor: cameraOn ? CONTROL_IDLE : DANGER }]}
|
|
504
289
|
>
|
|
505
290
|
{cameraOn
|
|
506
291
|
? (icons?.cameraOn ?? <CameraOnIcon color={TEXT_ON_DARK} />)
|
|
@@ -512,7 +297,7 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
512
297
|
<TouchableOpacity
|
|
513
298
|
accessibilityRole="button"
|
|
514
299
|
accessibilityLabel={t('call.action.switchCamera')}
|
|
515
|
-
onPress={
|
|
300
|
+
onPress={switchCallCamera}
|
|
516
301
|
style={[styles.controlButton, { backgroundColor: CONTROL_IDLE }]}
|
|
517
302
|
>
|
|
518
303
|
<SwitchCameraIcon color={TEXT_ON_DARK} />
|
|
@@ -521,17 +306,11 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
521
306
|
|
|
522
307
|
<TouchableOpacity
|
|
523
308
|
accessibilityRole="button"
|
|
524
|
-
accessibilityLabel={
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
onPress={toggleSpeaker}
|
|
309
|
+
accessibilityLabel={speakerOn ? t('call.action.speakerOff') : t('call.action.speakerOn')}
|
|
310
|
+
onPress={toggleCallSpeaker}
|
|
528
311
|
style={[styles.controlButton, { backgroundColor: CONTROL_IDLE }]}
|
|
529
312
|
>
|
|
530
|
-
{speakerOn ?
|
|
531
|
-
<SpeakerIcon color={TEXT_ON_DARK} />
|
|
532
|
-
) : (
|
|
533
|
-
<SpeakerOffIcon color={TEXT_ON_DARK} />
|
|
534
|
-
)}
|
|
313
|
+
{speakerOn ? <SpeakerIcon color={TEXT_ON_DARK} /> : <SpeakerOffIcon color={TEXT_ON_DARK} />}
|
|
535
314
|
</TouchableOpacity>
|
|
536
315
|
|
|
537
316
|
<TouchableOpacity
|
|
@@ -557,6 +336,7 @@ const styles = StyleSheet.create({
|
|
|
557
336
|
alignItems: 'center',
|
|
558
337
|
justifyContent: 'center',
|
|
559
338
|
gap: 12,
|
|
339
|
+
paddingHorizontal: 24,
|
|
560
340
|
},
|
|
561
341
|
stage: {
|
|
562
342
|
flex: 1,
|
|
@@ -571,18 +351,66 @@ const styles = StyleSheet.create({
|
|
|
571
351
|
color: TEXT_ON_DARK,
|
|
572
352
|
fontSize: 22,
|
|
573
353
|
marginTop: 16,
|
|
354
|
+
textAlign: 'center',
|
|
574
355
|
},
|
|
575
356
|
stageStatus: {
|
|
576
357
|
color: TEXT_MUTED,
|
|
577
358
|
fontSize: 14,
|
|
359
|
+
fontVariant: ['tabular-nums'],
|
|
578
360
|
},
|
|
579
361
|
spinner: {
|
|
580
362
|
marginTop: 8,
|
|
581
363
|
},
|
|
364
|
+
header: {
|
|
365
|
+
position: 'absolute',
|
|
366
|
+
top: 0,
|
|
367
|
+
left: 0,
|
|
368
|
+
right: 0,
|
|
369
|
+
paddingHorizontal: 64,
|
|
370
|
+
paddingBottom: 14,
|
|
371
|
+
alignItems: 'center',
|
|
372
|
+
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
373
|
+
gap: 2,
|
|
374
|
+
},
|
|
375
|
+
headerName: {
|
|
376
|
+
color: TEXT_ON_DARK,
|
|
377
|
+
fontSize: 15,
|
|
378
|
+
fontWeight: '600',
|
|
379
|
+
},
|
|
380
|
+
headerStatus: {
|
|
381
|
+
color: 'rgba(255,255,255,0.7)',
|
|
382
|
+
fontSize: 13,
|
|
383
|
+
fontVariant: ['tabular-nums'],
|
|
384
|
+
},
|
|
385
|
+
headerPill: {
|
|
386
|
+
marginTop: 4,
|
|
387
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
388
|
+
},
|
|
389
|
+
pill: {
|
|
390
|
+
flexDirection: 'row',
|
|
391
|
+
alignItems: 'center',
|
|
392
|
+
gap: 6,
|
|
393
|
+
paddingVertical: 4,
|
|
394
|
+
paddingHorizontal: 10,
|
|
395
|
+
borderRadius: 999,
|
|
396
|
+
backgroundColor: CONTROL_IDLE,
|
|
397
|
+
},
|
|
398
|
+
pillText: {
|
|
399
|
+
color: TEXT_ON_DARK,
|
|
400
|
+
fontSize: 12,
|
|
401
|
+
},
|
|
402
|
+
pillDanger: {
|
|
403
|
+
backgroundColor: 'rgba(229, 57, 53, 0.18)',
|
|
404
|
+
borderWidth: 1,
|
|
405
|
+
borderColor: 'rgba(229, 57, 53, 0.45)',
|
|
406
|
+
},
|
|
407
|
+
pillDangerText: {
|
|
408
|
+
color: '#FF8A80',
|
|
409
|
+
fontSize: 12,
|
|
410
|
+
},
|
|
582
411
|
pip: {
|
|
583
412
|
position: 'absolute',
|
|
584
413
|
right: 16,
|
|
585
|
-
top: 16,
|
|
586
414
|
width: 104,
|
|
587
415
|
height: 148,
|
|
588
416
|
borderRadius: 12,
|
|
@@ -590,6 +418,7 @@ const styles = StyleSheet.create({
|
|
|
590
418
|
backgroundColor: '#000',
|
|
591
419
|
borderWidth: 1,
|
|
592
420
|
borderColor: 'rgba(255,255,255,0.18)',
|
|
421
|
+
zIndex: 5,
|
|
593
422
|
},
|
|
594
423
|
pipVideo: {
|
|
595
424
|
flex: 1,
|
|
@@ -597,20 +426,20 @@ const styles = StyleSheet.create({
|
|
|
597
426
|
minimizeButton: {
|
|
598
427
|
position: 'absolute',
|
|
599
428
|
left: 16,
|
|
600
|
-
top: 16,
|
|
601
429
|
width: 36,
|
|
602
430
|
height: 36,
|
|
603
431
|
borderRadius: 18,
|
|
604
432
|
alignItems: 'center',
|
|
605
433
|
justifyContent: 'center',
|
|
606
434
|
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
435
|
+
zIndex: 6,
|
|
607
436
|
},
|
|
608
437
|
controls: {
|
|
609
438
|
flexDirection: 'row',
|
|
610
439
|
alignItems: 'center',
|
|
611
440
|
justifyContent: 'center',
|
|
612
441
|
gap: 16,
|
|
613
|
-
|
|
442
|
+
paddingTop: 20,
|
|
614
443
|
paddingHorizontal: 16,
|
|
615
444
|
},
|
|
616
445
|
controlButton: {
|
|
@@ -629,11 +458,10 @@ const styles = StyleSheet.create({
|
|
|
629
458
|
textAlign: 'center',
|
|
630
459
|
paddingHorizontal: 24,
|
|
631
460
|
},
|
|
632
|
-
// Dismissible, non-blocking
|
|
633
|
-
//
|
|
461
|
+
// Dismissible, non-blocking: the call is still up underneath; this only
|
|
462
|
+
// flags that camera/mic didn't come up.
|
|
634
463
|
deviceErrorBanner: {
|
|
635
464
|
position: 'absolute',
|
|
636
|
-
top: 16,
|
|
637
465
|
left: 16,
|
|
638
466
|
right: 16,
|
|
639
467
|
zIndex: 10,
|
|
@@ -650,9 +478,9 @@ const styles = StyleSheet.create({
|
|
|
650
478
|
miniBar: {
|
|
651
479
|
flexDirection: 'row',
|
|
652
480
|
alignItems: 'center',
|
|
653
|
-
gap:
|
|
654
|
-
paddingHorizontal:
|
|
655
|
-
paddingVertical:
|
|
481
|
+
gap: 10,
|
|
482
|
+
paddingHorizontal: 12,
|
|
483
|
+
paddingVertical: 10,
|
|
656
484
|
borderRadius: 16,
|
|
657
485
|
backgroundColor: SURFACE_DARK,
|
|
658
486
|
},
|
|
@@ -667,6 +495,7 @@ const styles = StyleSheet.create({
|
|
|
667
495
|
miniStatus: {
|
|
668
496
|
color: TEXT_MUTED,
|
|
669
497
|
fontSize: 12,
|
|
498
|
+
fontVariant: ['tabular-nums'],
|
|
670
499
|
},
|
|
671
500
|
miniButton: {
|
|
672
501
|
width: 40,
|