@ethora/chat-component-rn 26.10.3 → 26.10.5
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 +2 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/ActiveCallBar.js +248 -0
- package/lib/commonjs/components/VideoCalls/ActiveCallBar.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/CallIcons.js +41 -16
- package/lib/commonjs/components/VideoCalls/CallIcons.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +193 -37
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js +71 -109
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/callSessionController.js +74 -21
- package/lib/commonjs/components/VideoCalls/callSessionController.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/callUiStore.js +120 -0
- package/lib/commonjs/components/VideoCalls/callUiStore.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/livekitRuntime.js +10 -1
- package/lib/commonjs/components/VideoCalls/livekitRuntime.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/useCallKeep.js +197 -28
- package/lib/commonjs/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/useOngoingCallNotification.js +88 -0
- package/lib/commonjs/components/VideoCalls/useOngoingCallNotification.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/usePipTile.js +232 -0
- package/lib/commonjs/components/VideoCalls/usePipTile.js.map +1 -0
- package/lib/commonjs/helpers/updateMessagesTillLast.js +3 -1
- package/lib/commonjs/helpers/updateMessagesTillLast.js.map +1 -1
- package/lib/commonjs/i18n/strings.js +54 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/main.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 +2 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/VideoCalls/ActiveCallBar.js +240 -0
- package/lib/module/components/VideoCalls/ActiveCallBar.js.map +1 -0
- package/lib/module/components/VideoCalls/CallIcons.js +38 -15
- package/lib/module/components/VideoCalls/CallIcons.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallOverlay.js +195 -39
- package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallSession.js +72 -111
- package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -1
- package/lib/module/components/VideoCalls/callSessionController.js +71 -19
- package/lib/module/components/VideoCalls/callSessionController.js.map +1 -1
- package/lib/module/components/VideoCalls/callUiStore.js +111 -0
- package/lib/module/components/VideoCalls/callUiStore.js.map +1 -0
- package/lib/module/components/VideoCalls/livekitRuntime.js +10 -1
- package/lib/module/components/VideoCalls/livekitRuntime.js.map +1 -1
- package/lib/module/components/VideoCalls/useCallKeep.js +198 -29
- package/lib/module/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/module/components/VideoCalls/useOngoingCallNotification.js +84 -0
- package/lib/module/components/VideoCalls/useOngoingCallNotification.js.map +1 -0
- package/lib/module/components/VideoCalls/usePipTile.js +228 -0
- package/lib/module/components/VideoCalls/usePipTile.js.map +1 -0
- package/lib/module/helpers/updateMessagesTillLast.js +3 -1
- package/lib/module/helpers/updateMessagesTillLast.js.map +1 -1
- package/lib/module/i18n/strings.js +54 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/main.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/VideoCalls/ActiveCallBar.d.ts +27 -0
- package/lib/typescript/components/VideoCalls/ActiveCallBar.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/CallIcons.d.ts +9 -0
- package/lib/typescript/components/VideoCalls/CallIcons.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts +7 -3
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/callSessionController.d.ts +9 -0
- package/lib/typescript/components/VideoCalls/callSessionController.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/callUiStore.d.ts +37 -0
- package/lib/typescript/components/VideoCalls/callUiStore.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts +7 -0
- package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts +12 -0
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/useOngoingCallNotification.d.ts +10 -0
- package/lib/typescript/components/VideoCalls/useOngoingCallNotification.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/usePipTile.d.ts +46 -0
- package/lib/typescript/components/VideoCalls/usePipTile.d.ts.map +1 -0
- package/lib/typescript/helpers/updateMessagesTillLast.d.ts.map +1 -1
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +1 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +40 -0
- package/lib/typescript/types/types.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 +3 -0
- package/src/components/VideoCalls/ActiveCallBar.tsx +222 -0
- package/src/components/VideoCalls/CallIcons.tsx +36 -12
- package/src/components/VideoCalls/VideoCallOverlay.tsx +211 -37
- package/src/components/VideoCalls/VideoCallSession.tsx +84 -93
- package/src/components/VideoCalls/callSessionController.ts +73 -24
- package/src/components/VideoCalls/callUiStore.ts +106 -0
- package/src/components/VideoCalls/livekitRuntime.ts +16 -0
- package/src/components/VideoCalls/useCallKeep.ts +204 -33
- package/src/components/VideoCalls/useOngoingCallNotification.ts +113 -0
- package/src/components/VideoCalls/usePipTile.ts +214 -0
- package/src/helpers/updateMessagesTillLast.tsx +2 -1
- package/src/i18n/strings.ts +54 -0
- package/src/main.ts +3 -0
- package/src/types/types.ts +48 -0
- package/src/version.ts +1 -1
- package/lib/commonjs/components/VideoCalls/useDraggable.js +0 -59
- package/lib/commonjs/components/VideoCalls/useDraggable.js.map +0 -1
- package/lib/module/components/VideoCalls/useDraggable.js +0 -55
- package/lib/module/components/VideoCalls/useDraggable.js.map +0 -1
- package/lib/typescript/components/VideoCalls/useDraggable.d.ts +0 -19
- package/lib/typescript/components/VideoCalls/useDraggable.d.ts.map +0 -1
- package/src/components/VideoCalls/useDraggable.ts +0 -46
|
@@ -6,21 +6,23 @@ import React, {
|
|
|
6
6
|
useEffect,
|
|
7
7
|
useMemo,
|
|
8
8
|
useRef,
|
|
9
|
-
useState,
|
|
10
9
|
} from 'react';
|
|
11
10
|
import {
|
|
12
11
|
Animated,
|
|
13
12
|
BackHandler,
|
|
14
13
|
Easing,
|
|
15
14
|
Modal,
|
|
15
|
+
PanResponder,
|
|
16
16
|
Platform,
|
|
17
17
|
StyleSheet,
|
|
18
18
|
Text,
|
|
19
19
|
TouchableOpacity,
|
|
20
|
+
useWindowDimensions,
|
|
20
21
|
Vibration,
|
|
21
22
|
View,
|
|
22
23
|
} from 'react-native';
|
|
23
24
|
import { useDispatch, useSelector } from 'react-redux';
|
|
25
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
24
26
|
import { RootState } from '../../roomStore';
|
|
25
27
|
import {
|
|
26
28
|
acceptIncomingCall,
|
|
@@ -34,7 +36,14 @@ import type { ChatTheme } from '../../theme/theme';
|
|
|
34
36
|
import { VideoCallSession } from './VideoCallSession';
|
|
35
37
|
import { startCallSession } from './callSessionController';
|
|
36
38
|
import { declineActiveCall, terminateActiveCall } from './callActions';
|
|
37
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
CALL_BAR_HEIGHT,
|
|
41
|
+
CALL_BAR_MARGIN_TOP,
|
|
42
|
+
CALL_BAR_MARGIN_X,
|
|
43
|
+
CALL_BAR_RADIUS,
|
|
44
|
+
DockedCallBar,
|
|
45
|
+
} from './ActiveCallBar';
|
|
46
|
+
import { setCallMinimized, useCallUi } from './callUiStore';
|
|
38
47
|
import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlaceholder';
|
|
39
48
|
import {
|
|
40
49
|
AudioCallIcon,
|
|
@@ -44,6 +53,7 @@ import {
|
|
|
44
53
|
import { VideoCallIcons } from '../../types/types';
|
|
45
54
|
import { useT } from '../../i18n/useT';
|
|
46
55
|
import { useCallKeep } from './useCallKeep';
|
|
56
|
+
import { useOngoingCallNotification } from './useOngoingCallNotification';
|
|
47
57
|
|
|
48
58
|
const DEFAULT_PRIMARY = '#0052CD';
|
|
49
59
|
const DANGER = '#E53935';
|
|
@@ -250,6 +260,160 @@ const RingingCard: FC<RingingCardProps> = ({
|
|
|
250
260
|
);
|
|
251
261
|
};
|
|
252
262
|
|
|
263
|
+
/**
|
|
264
|
+
* The full-screen call, collapsible into the header strip, Telegram-style:
|
|
265
|
+
* pulling it up morphs the whole screen into the green bubble under the
|
|
266
|
+
* header (position, width, height and corner radius all travel together,
|
|
267
|
+
* and the bubble's colour fades in on the way), so it visibly *becomes* the
|
|
268
|
+
* strip rather than leaving and the strip popping in. Let go past a quarter
|
|
269
|
+
* of the way, or flick, and it finishes; released early it springs back.
|
|
270
|
+
* The minimize button runs the same morph; expanding from the strip runs it
|
|
271
|
+
* backwards.
|
|
272
|
+
*
|
|
273
|
+
* Buttons and the draggable video tile handle their own touches; a drag
|
|
274
|
+
* anywhere else on the screen is ours, and only its upward part counts.
|
|
275
|
+
*/
|
|
276
|
+
const CollapsibleCallSurface: FC<{
|
|
277
|
+
enterFromTop: boolean;
|
|
278
|
+
barColor: string;
|
|
279
|
+
onCollapsed: () => void;
|
|
280
|
+
children: (collapse: () => void) => ReactNode;
|
|
281
|
+
}> = ({ enterFromTop, barColor, onCollapsed, children }) => {
|
|
282
|
+
const { width, height } = useWindowDimensions();
|
|
283
|
+
const insets = useSafeAreaInsets();
|
|
284
|
+
const { barAnchorY } = useCallUi();
|
|
285
|
+
// Where the bubble sits: under the hosting header, or under the status
|
|
286
|
+
// bar when no header is on screen (the docked fallback).
|
|
287
|
+
const target = {
|
|
288
|
+
top: (barAnchorY ?? insets.top) + CALL_BAR_MARGIN_TOP,
|
|
289
|
+
side: CALL_BAR_MARGIN_X,
|
|
290
|
+
height: CALL_BAR_HEIGHT,
|
|
291
|
+
};
|
|
292
|
+
// 0 = full screen, 1 = the bubble.
|
|
293
|
+
const progress = useRef(new Animated.Value(enterFromTop ? 1 : 0)).current;
|
|
294
|
+
const collapsingRef = useRef(false);
|
|
295
|
+
const onCollapsedRef = useRef(onCollapsed);
|
|
296
|
+
onCollapsedRef.current = onCollapsed;
|
|
297
|
+
const heightRef = useRef(height);
|
|
298
|
+
heightRef.current = height;
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!enterFromTop) {return;}
|
|
302
|
+
Animated.timing(progress, {
|
|
303
|
+
toValue: 0,
|
|
304
|
+
duration: 300,
|
|
305
|
+
easing: Easing.out(Easing.cubic),
|
|
306
|
+
useNativeDriver: false,
|
|
307
|
+
}).start();
|
|
308
|
+
// Mount-only: the entrance plays once.
|
|
309
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
310
|
+
}, []);
|
|
311
|
+
|
|
312
|
+
const collapse = useCallback(() => {
|
|
313
|
+
if (collapsingRef.current) {return;}
|
|
314
|
+
collapsingRef.current = true;
|
|
315
|
+
Animated.timing(progress, {
|
|
316
|
+
toValue: 1,
|
|
317
|
+
duration: 280,
|
|
318
|
+
easing: Easing.out(Easing.cubic),
|
|
319
|
+
useNativeDriver: false,
|
|
320
|
+
}).start(() => onCollapsedRef.current());
|
|
321
|
+
}, [progress]);
|
|
322
|
+
|
|
323
|
+
const panHandlers = useMemo(() => {
|
|
324
|
+
const springBack = () =>
|
|
325
|
+
Animated.spring(progress, {
|
|
326
|
+
toValue: 0,
|
|
327
|
+
useNativeDriver: false,
|
|
328
|
+
damping: 22,
|
|
329
|
+
stiffness: 260,
|
|
330
|
+
}).start();
|
|
331
|
+
// How far the finger travels for the full morph.
|
|
332
|
+
const travel = () => heightRef.current * 0.6;
|
|
333
|
+
return PanResponder.create({
|
|
334
|
+
// Claim the touch as soon as it starts on the call screen itself
|
|
335
|
+
// (buttons and the video tile are deeper and win their own touches).
|
|
336
|
+
// Waiting for a move does not work here: the Modal's content sits
|
|
337
|
+
// under the host's screens in the React tree, and an ancestor there
|
|
338
|
+
// (a Pressable / keyboard-dismiss wrapper) takes the touch on start,
|
|
339
|
+
// after which no descendant is ever asked again.
|
|
340
|
+
onStartShouldSetPanResponder: () => !collapsingRef.current,
|
|
341
|
+
onMoveShouldSetPanResponder: () => !collapsingRef.current,
|
|
342
|
+
onPanResponderTerminationRequest: () => false,
|
|
343
|
+
onPanResponderMove: (_event, gesture) => {
|
|
344
|
+
progress.setValue(Math.min(1, Math.max(0, -gesture.dy / travel())));
|
|
345
|
+
},
|
|
346
|
+
onPanResponderRelease: (_event, gesture) => {
|
|
347
|
+
if (-gesture.dy / travel() > 0.25 || gesture.vy < -0.8) {
|
|
348
|
+
collapse();
|
|
349
|
+
} else {
|
|
350
|
+
springBack();
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
onPanResponderTerminate: springBack,
|
|
354
|
+
}).panHandlers;
|
|
355
|
+
}, [progress, collapse]);
|
|
356
|
+
|
|
357
|
+
const at = (from: number, to: number) =>
|
|
358
|
+
progress.interpolate({ inputRange: [0, 1], outputRange: [from, to] });
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<Animated.View
|
|
362
|
+
{...panHandlers}
|
|
363
|
+
style={[
|
|
364
|
+
styles.collapsible,
|
|
365
|
+
{
|
|
366
|
+
top: at(0, target.top),
|
|
367
|
+
left: at(0, target.side),
|
|
368
|
+
width: at(width, width - target.side * 2),
|
|
369
|
+
height: at(height, target.height),
|
|
370
|
+
// Rounded as soon as it starts moving (a card, like the screen's
|
|
371
|
+
// own corners), then tightening to the bubble's radius.
|
|
372
|
+
borderRadius: progress.interpolate({
|
|
373
|
+
inputRange: [0, 0.08, 0.7, 1],
|
|
374
|
+
outputRange: [0, 36, 28, CALL_BAR_RADIUS],
|
|
375
|
+
extrapolate: 'clamp',
|
|
376
|
+
}),
|
|
377
|
+
},
|
|
378
|
+
]}
|
|
379
|
+
>
|
|
380
|
+
{/* Fixed full-screen size: the content is clipped, never re-laid out
|
|
381
|
+
per frame (the video views would stutter). It fades as the bubble's
|
|
382
|
+
colour takes over. */}
|
|
383
|
+
<Animated.View
|
|
384
|
+
style={[
|
|
385
|
+
styles.sessionFixed,
|
|
386
|
+
{
|
|
387
|
+
width,
|
|
388
|
+
height,
|
|
389
|
+
opacity: progress.interpolate({
|
|
390
|
+
inputRange: [0, 0.55, 0.85],
|
|
391
|
+
outputRange: [1, 0.85, 0],
|
|
392
|
+
extrapolate: 'clamp',
|
|
393
|
+
}),
|
|
394
|
+
},
|
|
395
|
+
]}
|
|
396
|
+
>
|
|
397
|
+
{children(collapse)}
|
|
398
|
+
</Animated.View>
|
|
399
|
+
<Animated.View
|
|
400
|
+
pointerEvents="none"
|
|
401
|
+
style={[
|
|
402
|
+
StyleSheet.absoluteFill,
|
|
403
|
+
{
|
|
404
|
+
backgroundColor: barColor,
|
|
405
|
+
opacity: progress.interpolate({
|
|
406
|
+
inputRange: [0, 0.35, 0.9],
|
|
407
|
+
outputRange: [0, 0, 1],
|
|
408
|
+
extrapolate: 'clamp',
|
|
409
|
+
}),
|
|
410
|
+
},
|
|
411
|
+
]}
|
|
412
|
+
/>
|
|
413
|
+
</Animated.View>
|
|
414
|
+
);
|
|
415
|
+
};
|
|
416
|
+
|
|
253
417
|
/**
|
|
254
418
|
* A crash in the call UI must not take the chat down with it (web wraps
|
|
255
419
|
* the overlay in a silent ChatErrorBoundary). Renders nothing on error and
|
|
@@ -287,8 +451,8 @@ class CallErrorBoundary extends Component<
|
|
|
287
451
|
* 'idle'.
|
|
288
452
|
*
|
|
289
453
|
* The LiveKit connection itself lives in callSessionController.ts, so
|
|
290
|
-
* switching between the full-screen Modal and the minimized
|
|
291
|
-
* different trees
|
|
454
|
+
* switching between the full-screen Modal and the minimized header strip
|
|
455
|
+
* (two different trees) no longer drops the call.
|
|
292
456
|
*/
|
|
293
457
|
const VideoCallOverlayInner: FC = () => {
|
|
294
458
|
const dispatch = useDispatch();
|
|
@@ -296,7 +460,6 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
296
460
|
const { config } = useChatSettingState();
|
|
297
461
|
const theme = useTheme();
|
|
298
462
|
const t = useT();
|
|
299
|
-
const panel = useDraggable();
|
|
300
463
|
|
|
301
464
|
const videoCallsConfig = config?.videoCalls;
|
|
302
465
|
const enabled = videoCallsConfig?.enabled === true;
|
|
@@ -309,14 +472,25 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
309
472
|
// callkeep. No-op otherwise, the in-app ring screen below is the
|
|
310
473
|
// fallback and always renders.
|
|
311
474
|
useCallKeep();
|
|
475
|
+
// Host-provided system notification (Android) with hang up / mute /
|
|
476
|
+
// camera buttons while the call runs.
|
|
477
|
+
useOngoingCallNotification(enabled ? videoCallsConfig?.ongoingCallNotification : undefined);
|
|
312
478
|
|
|
313
|
-
|
|
479
|
+
// Shared with the header strip (ActiveCallBar), which expands it again.
|
|
480
|
+
const { minimized, systemRinging } = useCallUi();
|
|
314
481
|
|
|
315
482
|
const canRenderSession =
|
|
316
483
|
!!call.token &&
|
|
317
484
|
(call.phase === 'connecting' || call.phase === 'in-call') &&
|
|
318
485
|
!!livekitUrl;
|
|
319
486
|
const isMinimized = minimized && canRenderSession;
|
|
487
|
+
// Expanding from the header strip slides the call down from the top
|
|
488
|
+
// instead of fading it in.
|
|
489
|
+
const wasMinimizedRef = useRef(false);
|
|
490
|
+
const enterFromTop = wasMinimizedRef.current && !isMinimized;
|
|
491
|
+
useEffect(() => {
|
|
492
|
+
wasMinimizedRef.current = isMinimized;
|
|
493
|
+
}, [isMinimized]);
|
|
320
494
|
|
|
321
495
|
// Join the LiveKit room once the call has a token. Idempotent, and the
|
|
322
496
|
// controller tears itself down when the call slice leaves these phases.
|
|
@@ -332,12 +506,6 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
332
506
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
333
507
|
}, [enabled, canRenderSession, call.token, livekitUrl, call.kind]);
|
|
334
508
|
|
|
335
|
-
// Reset the minimized flag whenever the call ends, so the next call
|
|
336
|
-
// opens full-screen.
|
|
337
|
-
useEffect(() => {
|
|
338
|
-
if (call.phase === 'idle') {setMinimized(false);}
|
|
339
|
-
}, [call.phase]);
|
|
340
|
-
|
|
341
509
|
// Give up on an unanswered outgoing dial. Unlike web (which only shows
|
|
342
510
|
// the error), also tell the callee, who would otherwise keep ringing:
|
|
343
511
|
// there is no incoming ring timeout on either platform.
|
|
@@ -358,10 +526,11 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
358
526
|
useEffect(() => {
|
|
359
527
|
const isRinging =
|
|
360
528
|
call.direction === 'incoming' && call.phase === 'ringing-incoming';
|
|
361
|
-
|
|
529
|
+
// CallKit rings (and vibrates) on its own.
|
|
530
|
+
if (!isRinging || systemRinging) {return;}
|
|
362
531
|
Vibration.vibrate(RING_VIBRATION_PATTERN, true);
|
|
363
532
|
return () => Vibration.cancel();
|
|
364
|
-
}, [call.direction, call.phase]);
|
|
533
|
+
}, [call.direction, call.phase, systemRinging]);
|
|
365
534
|
|
|
366
535
|
const hangupCurrent = useCallback(() => {
|
|
367
536
|
if (call.direction === 'incoming' && call.phase === 'ringing-incoming') {
|
|
@@ -416,38 +585,35 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
416
585
|
const showIncomingDecision =
|
|
417
586
|
call.direction === 'incoming' && call.phase === 'ringing-incoming';
|
|
418
587
|
|
|
419
|
-
|
|
588
|
+
// The system's incoming-call banner is up (iOS CallKit); answering or
|
|
589
|
+
// declining it drives the call from there.
|
|
590
|
+
if (showIncomingDecision && systemRinging) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const renderSession = (collapse: () => void) => (
|
|
420
595
|
<VideoCallSession
|
|
421
596
|
kind={call.kind}
|
|
422
597
|
peerName={call.roomName}
|
|
423
598
|
primaryColor={primaryColor}
|
|
424
599
|
icons={videoCallsConfig?.icons}
|
|
425
|
-
|
|
426
|
-
onToggleMinimize={() => setMinimized((m) => !m)}
|
|
600
|
+
onToggleMinimize={collapse}
|
|
427
601
|
onHangup={() => terminateActiveCall('ended')}
|
|
428
602
|
/>
|
|
429
603
|
);
|
|
430
604
|
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
// floating bar the user can drag out of the way, like web's panel.
|
|
605
|
+
// Minimized, the call is a strip under the visible SDK header
|
|
606
|
+
// (HeaderCallBar), Telegram-style, so the chat stays fully usable. With
|
|
607
|
+
// no SDK header on screen the same strip docks to the top instead.
|
|
435
608
|
if (isMinimized) {
|
|
436
|
-
return
|
|
437
|
-
<Animated.View
|
|
438
|
-
{...panel.panHandlers}
|
|
439
|
-
style={[styles.floatingPanel, panel.style]}
|
|
440
|
-
>
|
|
441
|
-
{session}
|
|
442
|
-
</Animated.View>
|
|
443
|
-
);
|
|
609
|
+
return <DockedCallBar />;
|
|
444
610
|
}
|
|
445
611
|
|
|
446
612
|
return (
|
|
447
613
|
<Modal
|
|
448
614
|
visible
|
|
449
615
|
transparent
|
|
450
|
-
animationType=
|
|
616
|
+
animationType={enterFromTop ? 'none' : 'fade'}
|
|
451
617
|
statusBarTranslucent
|
|
452
618
|
supportedOrientations={['portrait', 'landscape']}
|
|
453
619
|
onRequestClose={hangupCurrent}
|
|
@@ -455,7 +621,13 @@ const VideoCallOverlayInner: FC = () => {
|
|
|
455
621
|
{canRenderSession ? (
|
|
456
622
|
// Full-bleed: on a phone the call owns the screen, like the native
|
|
457
623
|
// dialer. The audio call keeps the same dark canvas as video.
|
|
458
|
-
<
|
|
624
|
+
<CollapsibleCallSurface
|
|
625
|
+
enterFromTop={enterFromTop}
|
|
626
|
+
barColor={theme.success}
|
|
627
|
+
onCollapsed={() => setCallMinimized(true)}
|
|
628
|
+
>
|
|
629
|
+
{renderSession}
|
|
630
|
+
</CollapsibleCallSurface>
|
|
459
631
|
) : (
|
|
460
632
|
<View style={[styles.backdrop, { backgroundColor: theme.overlay }]}>
|
|
461
633
|
{showIncomingDecision ? (
|
|
@@ -527,13 +699,15 @@ const styles = StyleSheet.create({
|
|
|
527
699
|
flex: 1,
|
|
528
700
|
backgroundColor: '#0B1220',
|
|
529
701
|
},
|
|
530
|
-
|
|
702
|
+
// No flex here: `flex: 1` would override the fixed height and re-lay the
|
|
703
|
+
// call out on every frame of the morph.
|
|
704
|
+
sessionFixed: {
|
|
705
|
+
backgroundColor: '#0B1220',
|
|
706
|
+
},
|
|
707
|
+
collapsible: {
|
|
531
708
|
position: 'absolute',
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
left: 16,
|
|
535
|
-
zIndex: 1001,
|
|
536
|
-
elevation: 12,
|
|
709
|
+
overflow: 'hidden',
|
|
710
|
+
backgroundColor: '#0B1220',
|
|
537
711
|
},
|
|
538
712
|
ringingCard: {
|
|
539
713
|
width: '100%',
|
|
@@ -2,6 +2,7 @@ import React, { FC, useEffect, useState } from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
ActivityIndicator,
|
|
4
4
|
Animated,
|
|
5
|
+
Pressable,
|
|
5
6
|
StyleSheet,
|
|
6
7
|
Text,
|
|
7
8
|
TouchableOpacity,
|
|
@@ -13,11 +14,12 @@ import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlacehold
|
|
|
13
14
|
import {
|
|
14
15
|
CameraOffIcon,
|
|
15
16
|
CameraOnIcon,
|
|
17
|
+
FillVideoIcon,
|
|
18
|
+
FitVideoIcon,
|
|
16
19
|
HangUpIcon,
|
|
17
20
|
MicOffIcon,
|
|
18
21
|
MicOnIcon,
|
|
19
22
|
MinimizeIcon,
|
|
20
|
-
ExpandIcon,
|
|
21
23
|
SpeakerIcon,
|
|
22
24
|
SpeakerOffIcon,
|
|
23
25
|
SwitchCameraIcon,
|
|
@@ -31,7 +33,7 @@ import {
|
|
|
31
33
|
toggleCallSpeaker,
|
|
32
34
|
useCallSession,
|
|
33
35
|
} from './callSessionController';
|
|
34
|
-
import {
|
|
36
|
+
import { usePipTile } from './usePipTile';
|
|
35
37
|
import { useT } from '../../i18n/useT';
|
|
36
38
|
|
|
37
39
|
const DEFAULT_PRIMARY = '#0052CD';
|
|
@@ -47,9 +49,7 @@ export interface VideoCallSessionProps {
|
|
|
47
49
|
peerName?: string | null;
|
|
48
50
|
primaryColor?: string;
|
|
49
51
|
icons?: VideoCallIcons;
|
|
50
|
-
/**
|
|
51
|
-
minimized?: boolean;
|
|
52
|
-
/** Toggle between full and minimized layouts. */
|
|
52
|
+
/** Minimize to the header strip (ActiveCallBar). */
|
|
53
53
|
onToggleMinimize?: () => void;
|
|
54
54
|
onHangup: () => void;
|
|
55
55
|
}
|
|
@@ -59,7 +59,7 @@ export interface VideoCallSessionProps {
|
|
|
59
59
|
* `joinedAt` anchor so both sides show the same time (web's
|
|
60
60
|
* useSharedCallElapsed). Null until the peer is in.
|
|
61
61
|
*/
|
|
62
|
-
const useSharedCallElapsed = (anchorAt: number | null): number | null => {
|
|
62
|
+
export const useSharedCallElapsed = (anchorAt: number | null): number | null => {
|
|
63
63
|
const [, tick] = useState(0);
|
|
64
64
|
useEffect(() => {
|
|
65
65
|
if (anchorAt === null) {return;}
|
|
@@ -94,7 +94,6 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
94
94
|
peerName,
|
|
95
95
|
primaryColor = DEFAULT_PRIMARY,
|
|
96
96
|
icons,
|
|
97
|
-
minimized = false,
|
|
98
97
|
onToggleMinimize,
|
|
99
98
|
onHangup,
|
|
100
99
|
}) => {
|
|
@@ -102,8 +101,14 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
102
101
|
const insets = useSafeAreaInsets();
|
|
103
102
|
const runtime = loadLiveKit();
|
|
104
103
|
const session = useCallSession();
|
|
105
|
-
const
|
|
104
|
+
const pipTile = usePipTile({ topInset: insets.top });
|
|
106
105
|
const elapsed = useSharedCallElapsed(session.anchorAt);
|
|
106
|
+
// Telegram-style: tap the small tile to swap it with the big one.
|
|
107
|
+
const [swapped, setSwapped] = useState(false);
|
|
108
|
+
// The peer's video is shown whole by default: a web camera is landscape,
|
|
109
|
+
// and filling a portrait phone crops most of it away. The button next to
|
|
110
|
+
// minimize stretches it to fill (the old behaviour) and back.
|
|
111
|
+
const [fillRemote, setFillRemote] = useState(false);
|
|
107
112
|
|
|
108
113
|
if (!runtime) {
|
|
109
114
|
return (
|
|
@@ -124,12 +129,29 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
124
129
|
speakerOn,
|
|
125
130
|
deviceError,
|
|
126
131
|
remoteMicMuted,
|
|
132
|
+
facingMode,
|
|
133
|
+
localVideoEpoch,
|
|
127
134
|
} = session;
|
|
128
135
|
// The call slice's name (the chat title) first, as web does: LiveKit's
|
|
129
136
|
// participant name is often just the opaque xmpp identity.
|
|
130
137
|
const displayName = peerName || session.remoteName || t('call.unknownCaller');
|
|
131
138
|
const isVideo = kind === 'video';
|
|
132
139
|
const showRemoteVideo = isVideo && !!remoteVideo;
|
|
140
|
+
const localLive = isVideo && cameraOn && !!localVideo;
|
|
141
|
+
// Swapping only means something while both sides have a picture; if
|
|
142
|
+
// either goes away, fall back to the default layout on its own.
|
|
143
|
+
const canSwap = showRemoteVideo && localLive;
|
|
144
|
+
const isSwapped = swapped && canSwap;
|
|
145
|
+
const mainVideo = isSwapped ? localVideo : showRemoteVideo ? remoteVideo : null;
|
|
146
|
+
const pipVideo = isSwapped ? remoteVideo : localLive ? localVideo : null;
|
|
147
|
+
const mainIsRemote = !!mainVideo && mainVideo === remoteVideo;
|
|
148
|
+
const mainFit = mainIsRemote && !fillRemote ? 'contain' : 'cover';
|
|
149
|
+
// Only the front camera is mirrored, as in every camera app.
|
|
150
|
+
const selfMirror = facingMode === 'user';
|
|
151
|
+
// The local view remounts when the camera is re-acquired (switching
|
|
152
|
+
// front/back), so it never keeps rendering the stopped stream.
|
|
153
|
+
const videoKey = (ref: any) =>
|
|
154
|
+
ref === localVideo ? `local-${localVideoEpoch}` : 'remote';
|
|
133
155
|
|
|
134
156
|
const statusText =
|
|
135
157
|
elapsed !== null
|
|
@@ -142,54 +164,19 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
142
164
|
|
|
143
165
|
const peerMutedLabel = t('call.peerMuted', { name: displayName });
|
|
144
166
|
|
|
145
|
-
if (minimized) {
|
|
146
|
-
return (
|
|
147
|
-
<View style={styles.miniBar}>
|
|
148
|
-
<ProfileImagePlaceholder name={displayName} size={36} />
|
|
149
|
-
<View style={styles.miniInfo}>
|
|
150
|
-
<Text numberOfLines={1} style={styles.miniName}>
|
|
151
|
-
{displayName}
|
|
152
|
-
</Text>
|
|
153
|
-
<Text numberOfLines={1} style={styles.miniStatus}>
|
|
154
|
-
{remoteMicMuted ? `${statusText} · ${peerMutedLabel}` : statusText}
|
|
155
|
-
</Text>
|
|
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>
|
|
167
|
-
<TouchableOpacity
|
|
168
|
-
accessibilityRole="button"
|
|
169
|
-
accessibilityLabel={t('call.action.expand')}
|
|
170
|
-
onPress={onToggleMinimize}
|
|
171
|
-
style={[styles.miniButton, { backgroundColor: CONTROL_IDLE }]}
|
|
172
|
-
>
|
|
173
|
-
<ExpandIcon color={TEXT_ON_DARK} size={20} />
|
|
174
|
-
</TouchableOpacity>
|
|
175
|
-
<TouchableOpacity
|
|
176
|
-
accessibilityRole="button"
|
|
177
|
-
accessibilityLabel={t('call.action.hangUp')}
|
|
178
|
-
onPress={onHangup}
|
|
179
|
-
style={[styles.miniButton, { backgroundColor: DANGER }]}
|
|
180
|
-
>
|
|
181
|
-
{icons?.hangup ?? <HangUpIcon color={TEXT_ON_DARK} size={20} />}
|
|
182
|
-
</TouchableOpacity>
|
|
183
|
-
</View>
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
167
|
return (
|
|
188
168
|
<View style={styles.canvas}>
|
|
189
169
|
{/* Stage */}
|
|
190
|
-
<View style={styles.stage}>
|
|
191
|
-
{
|
|
192
|
-
<VideoTrack
|
|
170
|
+
<View style={styles.stage} onLayout={pipTile.onStageLayout}>
|
|
171
|
+
{mainVideo ? (
|
|
172
|
+
<VideoTrack
|
|
173
|
+
key={videoKey(mainVideo)}
|
|
174
|
+
trackRef={mainVideo}
|
|
175
|
+
style={styles.remoteVideo}
|
|
176
|
+
objectFit={mainFit}
|
|
177
|
+
mirror={isSwapped && selfMirror}
|
|
178
|
+
zOrder={0}
|
|
179
|
+
/>
|
|
193
180
|
) : (
|
|
194
181
|
<View style={styles.centered}>
|
|
195
182
|
<ProfileImagePlaceholder name={displayName} size={120} />
|
|
@@ -216,7 +203,7 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
216
203
|
)}
|
|
217
204
|
|
|
218
205
|
{/* Header strip: who, how long, and whether they can hear us. */}
|
|
219
|
-
{
|
|
206
|
+
{mainVideo && (
|
|
220
207
|
<View style={[styles.header, { paddingTop: 14 + insets.top }]}>
|
|
221
208
|
<Text style={styles.headerName} numberOfLines={1}>
|
|
222
209
|
{displayName}
|
|
@@ -231,13 +218,29 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
231
218
|
</View>
|
|
232
219
|
)}
|
|
233
220
|
|
|
234
|
-
{/*
|
|
235
|
-
|
|
221
|
+
{/* Picture-in-picture: drag it and it snaps to a corner, pinch it
|
|
222
|
+
for the large size, tap it to swap it with the main video. */}
|
|
223
|
+
{pipVideo && (
|
|
236
224
|
<Animated.View
|
|
237
|
-
{...
|
|
238
|
-
style={[styles.pip,
|
|
225
|
+
{...pipTile.panHandlers}
|
|
226
|
+
style={[styles.pip, pipTile.style]}
|
|
239
227
|
>
|
|
240
|
-
<
|
|
228
|
+
<Pressable
|
|
229
|
+
style={styles.pipVideo}
|
|
230
|
+
disabled={!canSwap}
|
|
231
|
+
onPress={() => setSwapped((value) => !value)}
|
|
232
|
+
accessibilityRole="button"
|
|
233
|
+
accessibilityLabel={t('call.action.swapVideo')}
|
|
234
|
+
>
|
|
235
|
+
<VideoTrack
|
|
236
|
+
key={videoKey(pipVideo)}
|
|
237
|
+
trackRef={pipVideo}
|
|
238
|
+
style={styles.pipVideo}
|
|
239
|
+
objectFit="cover"
|
|
240
|
+
mirror={!isSwapped && selfMirror}
|
|
241
|
+
zOrder={1}
|
|
242
|
+
/>
|
|
243
|
+
</Pressable>
|
|
241
244
|
</Animated.View>
|
|
242
245
|
)}
|
|
243
246
|
|
|
@@ -252,6 +255,23 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
252
255
|
</TouchableOpacity>
|
|
253
256
|
)}
|
|
254
257
|
|
|
258
|
+
{mainIsRemote && (
|
|
259
|
+
<TouchableOpacity
|
|
260
|
+
accessibilityRole="button"
|
|
261
|
+
accessibilityLabel={
|
|
262
|
+
fillRemote ? t('call.action.fitVideo') : t('call.action.fillVideo')
|
|
263
|
+
}
|
|
264
|
+
onPress={() => setFillRemote((value) => !value)}
|
|
265
|
+
style={[styles.minimizeButton, styles.fitButton, { top: 16 + insets.top }]}
|
|
266
|
+
>
|
|
267
|
+
{fillRemote ? (
|
|
268
|
+
<FitVideoIcon color={TEXT_ON_DARK} size={20} />
|
|
269
|
+
) : (
|
|
270
|
+
<FillVideoIcon color={TEXT_ON_DARK} size={20} />
|
|
271
|
+
)}
|
|
272
|
+
</TouchableOpacity>
|
|
273
|
+
)}
|
|
274
|
+
|
|
255
275
|
{!!deviceError && (
|
|
256
276
|
<TouchableOpacity
|
|
257
277
|
style={[styles.deviceErrorBanner, { top: 64 + insets.top }]}
|
|
@@ -366,7 +386,7 @@ const styles = StyleSheet.create({
|
|
|
366
386
|
top: 0,
|
|
367
387
|
left: 0,
|
|
368
388
|
right: 0,
|
|
369
|
-
paddingHorizontal:
|
|
389
|
+
paddingHorizontal: 108,
|
|
370
390
|
paddingBottom: 14,
|
|
371
391
|
alignItems: 'center',
|
|
372
392
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
@@ -409,10 +429,6 @@ const styles = StyleSheet.create({
|
|
|
409
429
|
fontSize: 12,
|
|
410
430
|
},
|
|
411
431
|
pip: {
|
|
412
|
-
position: 'absolute',
|
|
413
|
-
right: 16,
|
|
414
|
-
width: 104,
|
|
415
|
-
height: 148,
|
|
416
432
|
borderRadius: 12,
|
|
417
433
|
overflow: 'hidden',
|
|
418
434
|
backgroundColor: '#000',
|
|
@@ -434,6 +450,10 @@ const styles = StyleSheet.create({
|
|
|
434
450
|
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
435
451
|
zIndex: 6,
|
|
436
452
|
},
|
|
453
|
+
// Next to minimize, top-left: the top-right corner belongs to the tile.
|
|
454
|
+
fitButton: {
|
|
455
|
+
left: 60,
|
|
456
|
+
},
|
|
437
457
|
controls: {
|
|
438
458
|
flexDirection: 'row',
|
|
439
459
|
alignItems: 'center',
|
|
@@ -475,35 +495,6 @@ const styles = StyleSheet.create({
|
|
|
475
495
|
fontSize: 13,
|
|
476
496
|
textAlign: 'center',
|
|
477
497
|
},
|
|
478
|
-
miniBar: {
|
|
479
|
-
flexDirection: 'row',
|
|
480
|
-
alignItems: 'center',
|
|
481
|
-
gap: 10,
|
|
482
|
-
paddingHorizontal: 12,
|
|
483
|
-
paddingVertical: 10,
|
|
484
|
-
borderRadius: 16,
|
|
485
|
-
backgroundColor: SURFACE_DARK,
|
|
486
|
-
},
|
|
487
|
-
miniInfo: {
|
|
488
|
-
flex: 1,
|
|
489
|
-
minWidth: 0,
|
|
490
|
-
},
|
|
491
|
-
miniName: {
|
|
492
|
-
color: TEXT_ON_DARK,
|
|
493
|
-
fontSize: 15,
|
|
494
|
-
},
|
|
495
|
-
miniStatus: {
|
|
496
|
-
color: TEXT_MUTED,
|
|
497
|
-
fontSize: 12,
|
|
498
|
-
fontVariant: ['tabular-nums'],
|
|
499
|
-
},
|
|
500
|
-
miniButton: {
|
|
501
|
-
width: 40,
|
|
502
|
-
height: 40,
|
|
503
|
-
borderRadius: 20,
|
|
504
|
-
alignItems: 'center',
|
|
505
|
-
justifyContent: 'center',
|
|
506
|
-
},
|
|
507
498
|
});
|
|
508
499
|
|
|
509
500
|
export default VideoCallSession;
|