@ethora/chat-component-rn 26.10.1 → 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.
Files changed (145) hide show
  1. package/lib/commonjs/components/MainComponents/ChatHeader.js +11 -10
  2. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  3. package/lib/commonjs/components/MainComponents/MessageContainer.js +10 -2
  4. package/lib/commonjs/components/MainComponents/MessageContainer.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/RoomList.js +8 -18
  6. package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
  7. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +17 -14
  8. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  9. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +131 -17
  10. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  11. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +6 -1
  12. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
  13. package/lib/commonjs/components/VideoCalls/CallButtons.js +3 -1
  14. package/lib/commonjs/components/VideoCalls/CallButtons.js.map +1 -1
  15. package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +123 -111
  16. package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -1
  17. package/lib/commonjs/components/VideoCalls/VideoCallSession.js +207 -346
  18. package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -1
  19. package/lib/commonjs/components/VideoCalls/callActions.js +61 -0
  20. package/lib/commonjs/components/VideoCalls/callActions.js.map +1 -0
  21. package/lib/commonjs/components/VideoCalls/callSessionController.js +431 -0
  22. package/lib/commonjs/components/VideoCalls/callSessionController.js.map +1 -0
  23. package/lib/commonjs/components/VideoCalls/useDraggable.js +59 -0
  24. package/lib/commonjs/components/VideoCalls/useDraggable.js.map +1 -0
  25. package/lib/commonjs/helpers/createRoomFromApi.js +4 -0
  26. package/lib/commonjs/helpers/createRoomFromApi.js.map +1 -1
  27. package/lib/commonjs/helpers/incomingCallFromPush.js +1 -1
  28. package/lib/commonjs/helpers/incomingCallFromPush.js.map +1 -1
  29. package/lib/commonjs/helpers/roomUserCount.js +7 -1
  30. package/lib/commonjs/helpers/roomUserCount.js.map +1 -1
  31. package/lib/commonjs/helpers/uniqueMembers.js +36 -0
  32. package/lib/commonjs/helpers/uniqueMembers.js.map +1 -0
  33. package/lib/commonjs/hooks/useLeaveRoom.js +52 -0
  34. package/lib/commonjs/hooks/useLeaveRoom.js.map +1 -0
  35. package/lib/commonjs/hooks/useLogout.js +15 -10
  36. package/lib/commonjs/hooks/useLogout.js.map +1 -1
  37. package/lib/commonjs/i18n/strings.js +18 -0
  38. package/lib/commonjs/i18n/strings.js.map +1 -1
  39. package/lib/commonjs/networking/api-requests/rooms.api.js +36 -1
  40. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  41. package/lib/commonjs/networking/callTokenStanza.js +47 -3
  42. package/lib/commonjs/networking/callTokenStanza.js.map +1 -1
  43. package/lib/commonjs/networking/xmppClient.js +9 -2
  44. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  45. package/lib/commonjs/version.js +1 -1
  46. package/lib/module/components/MainComponents/ChatHeader.js +13 -12
  47. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  48. package/lib/module/components/MainComponents/MessageContainer.js +10 -2
  49. package/lib/module/components/MainComponents/MessageContainer.js.map +1 -1
  50. package/lib/module/components/MainComponents/RoomList.js +8 -18
  51. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  52. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +18 -15
  53. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  54. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +132 -18
  55. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  56. package/lib/module/components/RoomComponents/ChatRoomItem.js +6 -1
  57. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  58. package/lib/module/components/VideoCalls/CallButtons.js +3 -1
  59. package/lib/module/components/VideoCalls/CallButtons.js.map +1 -1
  60. package/lib/module/components/VideoCalls/VideoCallOverlay.js +125 -113
  61. package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -1
  62. package/lib/module/components/VideoCalls/VideoCallSession.js +208 -348
  63. package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -1
  64. package/lib/module/components/VideoCalls/callActions.js +55 -0
  65. package/lib/module/components/VideoCalls/callActions.js.map +1 -0
  66. package/lib/module/components/VideoCalls/callSessionController.js +420 -0
  67. package/lib/module/components/VideoCalls/callSessionController.js.map +1 -0
  68. package/lib/module/components/VideoCalls/useDraggable.js +55 -0
  69. package/lib/module/components/VideoCalls/useDraggable.js.map +1 -0
  70. package/lib/module/helpers/createRoomFromApi.js +4 -0
  71. package/lib/module/helpers/createRoomFromApi.js.map +1 -1
  72. package/lib/module/helpers/incomingCallFromPush.js +1 -1
  73. package/lib/module/helpers/incomingCallFromPush.js.map +1 -1
  74. package/lib/module/helpers/roomUserCount.js +7 -1
  75. package/lib/module/helpers/roomUserCount.js.map +1 -1
  76. package/lib/module/helpers/uniqueMembers.js +31 -0
  77. package/lib/module/helpers/uniqueMembers.js.map +1 -0
  78. package/lib/module/hooks/useLeaveRoom.js +48 -0
  79. package/lib/module/hooks/useLeaveRoom.js.map +1 -0
  80. package/lib/module/hooks/useLogout.js +15 -10
  81. package/lib/module/hooks/useLogout.js.map +1 -1
  82. package/lib/module/i18n/strings.js +18 -0
  83. package/lib/module/i18n/strings.js.map +1 -1
  84. package/lib/module/networking/api-requests/rooms.api.js +35 -1
  85. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  86. package/lib/module/networking/callTokenStanza.js +47 -3
  87. package/lib/module/networking/callTokenStanza.js.map +1 -1
  88. package/lib/module/networking/xmppClient.js +8 -1
  89. package/lib/module/networking/xmppClient.js.map +1 -1
  90. package/lib/module/version.js +1 -1
  91. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  92. package/lib/typescript/components/MainComponents/MessageContainer.d.ts.map +1 -1
  93. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  94. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  95. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
  96. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  97. package/lib/typescript/components/VideoCalls/CallButtons.d.ts.map +1 -1
  98. package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts +0 -6
  99. package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts.map +1 -1
  100. package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts +9 -20
  101. package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
  102. package/lib/typescript/components/VideoCalls/callActions.d.ts +15 -0
  103. package/lib/typescript/components/VideoCalls/callActions.d.ts.map +1 -0
  104. package/lib/typescript/components/VideoCalls/callSessionController.d.ts +61 -0
  105. package/lib/typescript/components/VideoCalls/callSessionController.d.ts.map +1 -0
  106. package/lib/typescript/components/VideoCalls/useDraggable.d.ts +19 -0
  107. package/lib/typescript/components/VideoCalls/useDraggable.d.ts.map +1 -0
  108. package/lib/typescript/helpers/createRoomFromApi.d.ts.map +1 -1
  109. package/lib/typescript/helpers/roomUserCount.d.ts +1 -0
  110. package/lib/typescript/helpers/roomUserCount.d.ts.map +1 -1
  111. package/lib/typescript/helpers/uniqueMembers.d.ts +11 -0
  112. package/lib/typescript/helpers/uniqueMembers.d.ts.map +1 -0
  113. package/lib/typescript/hooks/useLeaveRoom.d.ts +9 -0
  114. package/lib/typescript/hooks/useLeaveRoom.d.ts.map +1 -0
  115. package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
  116. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  117. package/lib/typescript/networking/api-requests/rooms.api.d.ts +8 -0
  118. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  119. package/lib/typescript/networking/callTokenStanza.d.ts.map +1 -1
  120. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  121. package/lib/typescript/version.d.ts +1 -1
  122. package/package.json +1 -1
  123. package/src/components/MainComponents/ChatHeader.tsx +10 -8
  124. package/src/components/MainComponents/MessageContainer.tsx +14 -1
  125. package/src/components/MainComponents/RoomList.tsx +8 -14
  126. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +16 -13
  127. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +167 -36
  128. package/src/components/RoomComponents/ChatRoomItem.tsx +8 -1
  129. package/src/components/VideoCalls/CallButtons.tsx +5 -1
  130. package/src/components/VideoCalls/VideoCallOverlay.tsx +156 -163
  131. package/src/components/VideoCalls/VideoCallSession.tsx +207 -378
  132. package/src/components/VideoCalls/callActions.ts +63 -0
  133. package/src/components/VideoCalls/callSessionController.ts +421 -0
  134. package/src/components/VideoCalls/useDraggable.ts +46 -0
  135. package/src/helpers/createRoomFromApi.tsx +2 -0
  136. package/src/helpers/incomingCallFromPush.ts +2 -2
  137. package/src/helpers/roomUserCount.ts +6 -2
  138. package/src/helpers/uniqueMembers.ts +26 -0
  139. package/src/hooks/useLeaveRoom.ts +42 -0
  140. package/src/hooks/useLogout.tsx +15 -10
  141. package/src/i18n/strings.ts +18 -0
  142. package/src/networking/api-requests/rooms.api.ts +28 -1
  143. package/src/networking/callTokenStanza.ts +46 -6
  144. package/src/networking/xmppClient.ts +10 -1
  145. package/src/version.ts +1 -1
@@ -71,7 +71,11 @@ export const CallButtons: FC = () => {
71
71
  // Don't show "deleted" / "Deleted User" / "Unknown" as the dial
72
72
  // screen title when the chat title is one of the server sentinels.
73
73
  // Prefer the peer's first+last name, falling back to the bare chat name.
74
- const rawTitle = String(currentRoom.name || '').trim();
74
+ // `title` is what the header shows ("John Pauliller"); on new-arch
75
+ // private rooms `name` is the bare room id.
76
+ const rawTitle = String(
77
+ (currentRoom as any).title || currentRoom.name || ''
78
+ ).trim();
75
79
  const isBadTitle =
76
80
  !rawTitle ||
77
81
  ['deleted', 'deleted user', 'unknown', 'null'].includes(
@@ -1,5 +1,7 @@
1
1
  import React, {
2
+ Component,
2
3
  FC,
4
+ ReactNode,
3
5
  useCallback,
4
6
  useEffect,
5
7
  useMemo,
@@ -19,23 +21,20 @@ import {
19
21
  View,
20
22
  } from 'react-native';
21
23
  import { useDispatch, useSelector } from 'react-redux';
22
- import { RootState, store } from '../../roomStore';
24
+ import { RootState } from '../../roomStore';
23
25
  import {
24
26
  acceptIncomingCall,
25
- declineIncomingCall,
26
- endCall,
27
27
  resetCall,
28
28
  setCallError,
29
- setCallPhase,
30
29
  } from '../../roomStore/callSlice';
31
- import { addRoomMessage } from '../../roomStore/roomsSlice';
32
- import { buildLocalCallLogMessage } from '../../helpers/callLogMessage';
33
30
  import { useChatSettingState } from '../../hooks/useChatSettingState';
34
- import {
35
- CallSignalState,
36
- sendCallStateSignal,
37
- } from '../../networking/callTokenStanza';
31
+ import { sendCallStateSignal } from '../../networking/callTokenStanza';
32
+ import { useTheme } from '../../hooks/useTheme';
33
+ import type { ChatTheme } from '../../theme/theme';
38
34
  import { VideoCallSession } from './VideoCallSession';
35
+ import { startCallSession } from './callSessionController';
36
+ import { declineActiveCall, terminateActiveCall } from './callActions';
37
+ import { useDraggable } from './useDraggable';
39
38
  import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlaceholder';
40
39
  import {
41
40
  AudioCallIcon,
@@ -47,8 +46,6 @@ import { useT } from '../../i18n/useT';
47
46
  import { useCallKeep } from './useCallKeep';
48
47
 
49
48
  const DEFAULT_PRIMARY = '#0052CD';
50
- const TEXT_PRIMARY = '#141414';
51
- const TEXT_MUTED = '#8c8c8c';
52
49
  const DANGER = '#E53935';
53
50
  const ACCEPT = '#10B981';
54
51
 
@@ -146,6 +143,7 @@ interface RingingCardProps {
146
143
  onDecline?: () => void;
147
144
  onHangup?: () => void;
148
145
  onDismiss?: () => void;
146
+ theme: ChatTheme;
149
147
  }
150
148
 
151
149
  const RingingCard: FC<RingingCardProps> = ({
@@ -161,12 +159,13 @@ const RingingCard: FC<RingingCardProps> = ({
161
159
  onDecline,
162
160
  onHangup,
163
161
  onDismiss,
162
+ theme,
164
163
  }) => {
165
164
  const t = useT();
166
165
 
167
166
  return (
168
- <View style={styles.ringingCard}>
169
- <Text style={styles.ringingTitle}>{title}</Text>
167
+ <View style={[styles.ringingCard, { backgroundColor: theme.surface }]}>
168
+ <Text style={[styles.ringingTitle, { color: theme.textSecondary }]}>{title}</Text>
170
169
 
171
170
  <View style={styles.avatarWrap}>
172
171
  {variant !== 'error' && (
@@ -179,17 +178,17 @@ const RingingCard: FC<RingingCardProps> = ({
179
178
  </View>
180
179
 
181
180
  <View style={styles.ringingIdentity}>
182
- <Text style={styles.ringingName}>
181
+ <Text style={[styles.ringingName, { color: theme.text }]}>
183
182
  {roomName ||
184
183
  (variant === 'incoming' ? t('call.unknownCaller') : t('call.title'))}
185
184
  </Text>
186
185
  <View style={styles.ringingSubtitleRow}>
187
186
  {kind === 'audio' ? (
188
- <AudioCallIcon color={TEXT_MUTED} size={16} />
187
+ <AudioCallIcon color={theme.textSecondary} size={16} />
189
188
  ) : (
190
- <VideoCallIcon color={TEXT_MUTED} size={16} />
189
+ <VideoCallIcon color={theme.textSecondary} size={16} />
191
190
  )}
192
- <Text style={styles.ringingSubtitle}>{subtitle}</Text>
191
+ <Text style={[styles.ringingSubtitle, { color: theme.textSecondary }]}>{subtitle}</Text>
193
192
  </View>
194
193
  </View>
195
194
 
@@ -251,22 +250,58 @@ const RingingCard: FC<RingingCardProps> = ({
251
250
  );
252
251
  };
253
252
 
253
+ /**
254
+ * A crash in the call UI must not take the chat down with it (web wraps
255
+ * the overlay in a silent ChatErrorBoundary). Renders nothing on error and
256
+ * clears itself on the next call.
257
+ */
258
+ class CallErrorBoundary extends Component<
259
+ { resetKey: string; children: ReactNode },
260
+ { failed: boolean }
261
+ > {
262
+ state = { failed: false };
263
+
264
+ static getDerivedStateFromError() {
265
+ return { failed: true };
266
+ }
267
+
268
+ componentDidUpdate(prev: { resetKey: string }) {
269
+ if (prev.resetKey !== this.props.resetKey && this.state.failed) {
270
+ this.setState({ failed: false });
271
+ }
272
+ }
273
+
274
+ componentDidCatch(error: unknown) {
275
+ console.warn('[calls] call UI crashed', error);
276
+ }
277
+
278
+ render() {
279
+ return this.state.failed ? null : this.props.children;
280
+ }
281
+ }
282
+
254
283
  /**
255
284
  * Call orchestration surface. Mounted once, above the chat, by XmppProvider,
256
285
  * so an incoming call rings on whatever screen the user happens to be on
257
286
  * rather than only inside <Chat>. Renders nothing while `call.phase` is
258
287
  * 'idle'.
288
+ *
289
+ * The LiveKit connection itself lives in callSessionController.ts, so
290
+ * switching between the full-screen Modal and the minimized panel (two
291
+ * different trees here) no longer drops the call.
259
292
  */
260
- export const VideoCallOverlay: FC = () => {
293
+ const VideoCallOverlayInner: FC = () => {
261
294
  const dispatch = useDispatch();
262
295
  const call = useSelector((state: RootState) => state.call);
263
296
  const { config } = useChatSettingState();
297
+ const theme = useTheme();
264
298
  const t = useT();
299
+ const panel = useDraggable();
265
300
 
266
301
  const videoCallsConfig = config?.videoCalls;
267
302
  const enabled = videoCallsConfig?.enabled === true;
268
303
  const livekitUrl = (videoCallsConfig?.livekitUrl || '').trim();
269
- const primaryColor = config?.colors?.primary || DEFAULT_PRIMARY;
304
+ const primaryColor = theme.primary || config?.colors?.primary || DEFAULT_PRIMARY;
270
305
 
271
306
  const isOpen = enabled && call.phase !== 'idle';
272
307
 
@@ -283,18 +318,35 @@ export const VideoCallOverlay: FC = () => {
283
318
  !!livekitUrl;
284
319
  const isMinimized = minimized && canRenderSession;
285
320
 
321
+ // Join the LiveKit room once the call has a token. Idempotent, and the
322
+ // controller tears itself down when the call slice leaves these phases.
323
+ useEffect(() => {
324
+ if (!enabled || !canRenderSession || !call.token) {return;}
325
+ startCallSession({
326
+ token: call.token,
327
+ livekitUrl,
328
+ kind: call.kind,
329
+ startWithMicOn: videoCallsConfig?.startWithMicOn,
330
+ startWithCameraOn: videoCallsConfig?.startWithCameraOn,
331
+ });
332
+ // eslint-disable-next-line react-hooks/exhaustive-deps
333
+ }, [enabled, canRenderSession, call.token, livekitUrl, call.kind]);
334
+
286
335
  // Reset the minimized flag whenever the call ends, so the next call
287
336
  // opens full-screen.
288
337
  useEffect(() => {
289
338
  if (call.phase === 'idle') {setMinimized(false);}
290
339
  }, [call.phase]);
291
340
 
292
- // Give up on an unanswered outgoing dial.
341
+ // Give up on an unanswered outgoing dial. Unlike web (which only shows
342
+ // the error), also tell the callee, who would otherwise keep ringing:
343
+ // there is no incoming ring timeout on either platform.
293
344
  useEffect(() => {
294
345
  if (call.phase !== 'requesting' || call.direction !== 'outgoing') {
295
346
  return;
296
347
  }
297
348
  const timer = setTimeout(() => {
349
+ sendCallStateSignal('cancelled');
298
350
  dispatch(setCallError(t('call.error.noAnswerTimeout')));
299
351
  }, OUTGOING_CALL_TIMEOUT_MS);
300
352
  return () => clearTimeout(timer);
@@ -311,53 +363,15 @@ export const VideoCallOverlay: FC = () => {
311
363
  return () => Vibration.cancel();
312
364
  }, [call.direction, call.phase]);
313
365
 
314
- const terminateCall = useCallback(
315
- (state: CallSignalState) => {
316
- // Write a local "call ended" log entry BEFORE endCall() wipes the
317
- // call slice. The authoritative entry is a server call-state
318
- // broadcast, but that isn't guaranteed to arrive, so we synthesize
319
- // our own. It's deduped against any server copy by callId in
320
- // addRoomMessage.
321
- const snap = store.getState().call;
322
- if (snap.roomJid && (snap.callId || snap.connectedAt)) {
323
- const durationMs = snap.connectedAt
324
- ? Math.max(0, Date.now() - snap.connectedAt)
325
- : 0;
326
- dispatch(
327
- addRoomMessage({
328
- roomJID: snap.roomJid,
329
- message: buildLocalCallLogMessage({
330
- callId: snap.callId || '',
331
- direction:
332
- snap.direction === 'incoming' ? 'incoming' : 'outgoing',
333
- durationMs,
334
- kind: snap.kind === 'audio' ? 'audio' : 'video',
335
- selfXmppUsername:
336
- store.getState().chatSettingStore.user?.xmppUsername || '',
337
- }),
338
- })
339
- );
340
- }
341
- sendCallStateSignal(state);
342
- dispatch(endCall());
343
- },
344
- [dispatch]
345
- );
346
-
347
- const declineCall = useCallback(() => {
348
- sendCallStateSignal('declined');
349
- dispatch(declineIncomingCall());
350
- }, [dispatch]);
351
-
352
366
  const hangupCurrent = useCallback(() => {
353
367
  if (call.direction === 'incoming' && call.phase === 'ringing-incoming') {
354
- declineCall();
368
+ declineActiveCall();
355
369
  } else if (call.phase === 'error') {
356
370
  dispatch(resetCall());
357
371
  } else {
358
- terminateCall(call.phase === 'requesting' ? 'cancelled' : 'ended');
372
+ terminateActiveCall(call.phase === 'requesting' ? 'cancelled' : 'ended');
359
373
  }
360
- }, [call.direction, call.phase, declineCall, terminateCall, dispatch]);
374
+ }, [call.direction, call.phase, dispatch]);
361
375
 
362
376
  // Android hardware back = hang up, the RN equivalent of the web Esc
363
377
  // handler. A minimized panel shouldn't swallow back: the user is using
@@ -402,29 +416,30 @@ export const VideoCallOverlay: FC = () => {
402
416
  const showIncomingDecision =
403
417
  call.direction === 'incoming' && call.phase === 'ringing-incoming';
404
418
 
419
+ const session = (
420
+ <VideoCallSession
421
+ kind={call.kind}
422
+ peerName={call.roomName}
423
+ primaryColor={primaryColor}
424
+ icons={videoCallsConfig?.icons}
425
+ minimized={isMinimized}
426
+ onToggleMinimize={() => setMinimized((m) => !m)}
427
+ onHangup={() => terminateActiveCall('ended')}
428
+ />
429
+ );
430
+
405
431
  // The minimized session is NOT a Modal: a modal is by definition on top
406
432
  // of everything and swallows touches, which is the opposite of what
407
433
  // minimize is for (keep talking, keep using the chat). It renders as a
408
- // floating absolutely-positioned bar instead.
409
- if (canRenderSession && isMinimized) {
434
+ // floating bar the user can drag out of the way, like web's panel.
435
+ if (isMinimized) {
410
436
  return (
411
- <View style={styles.floatingPanel} pointerEvents="box-none">
412
- <VideoCallSession
413
- token={call.token as string}
414
- livekitUrl={livekitUrl}
415
- kind={call.kind}
416
- peerName={call.roomName}
417
- primaryColor={primaryColor}
418
- icons={videoCallsConfig?.icons}
419
- startWithCameraOn={videoCallsConfig?.startWithCameraOn}
420
- startWithMicOn={videoCallsConfig?.startWithMicOn}
421
- minimized
422
- onToggleMinimize={() => setMinimized((m) => !m)}
423
- onConnected={() => dispatch(setCallPhase('in-call'))}
424
- onError={(message) => dispatch(setCallError(message))}
425
- onHangup={() => terminateCall('ended')}
426
- />
427
- </View>
437
+ <Animated.View
438
+ {...panel.panHandlers}
439
+ style={[styles.floatingPanel, panel.style]}
440
+ >
441
+ {session}
442
+ </Animated.View>
428
443
  );
429
444
  }
430
445
 
@@ -434,101 +449,83 @@ export const VideoCallOverlay: FC = () => {
434
449
  transparent
435
450
  animationType="fade"
436
451
  statusBarTranslucent
452
+ supportedOrientations={['portrait', 'landscape']}
437
453
  onRequestClose={hangupCurrent}
438
454
  >
439
- <View style={styles.backdrop}>
440
- {canRenderSession ? (
441
- <View
442
- style={
443
- call.kind === 'audio' ? styles.audioSessionCard : styles.sessionCard
444
- }
445
- >
446
- <VideoCallSession
447
- token={call.token as string}
448
- livekitUrl={livekitUrl}
455
+ {canRenderSession ? (
456
+ // Full-bleed: on a phone the call owns the screen, like the native
457
+ // dialer. The audio call keeps the same dark canvas as video.
458
+ <View style={styles.sessionFull}>{session}</View>
459
+ ) : (
460
+ <View style={[styles.backdrop, { backgroundColor: theme.overlay }]}>
461
+ {showIncomingDecision ? (
462
+ <RingingCard
463
+ title={ringingHeader}
464
+ subtitle={ringingSubtitle}
465
+ roomName={call.roomName}
449
466
  kind={call.kind}
450
- peerName={call.roomName}
467
+ variant="incoming"
451
468
  primaryColor={primaryColor}
452
469
  icons={videoCallsConfig?.icons}
453
- startWithCameraOn={videoCallsConfig?.startWithCameraOn}
454
- startWithMicOn={videoCallsConfig?.startWithMicOn}
455
- minimized={false}
456
- onToggleMinimize={() => setMinimized((m) => !m)}
457
- onConnected={() => dispatch(setCallPhase('in-call'))}
458
- onError={(message) => dispatch(setCallError(message))}
459
- onHangup={() => terminateCall('ended')}
470
+ theme={theme}
471
+ onAccept={() => dispatch(acceptIncomingCall())}
472
+ onDecline={declineActiveCall}
460
473
  />
461
- </View>
462
- ) : showIncomingDecision ? (
463
- <RingingCard
464
- title={ringingHeader}
465
- subtitle={ringingSubtitle}
466
- roomName={call.roomName}
467
- kind={call.kind}
468
- variant="incoming"
469
- primaryColor={primaryColor}
470
- icons={videoCallsConfig?.icons}
471
- onAccept={() => dispatch(acceptIncomingCall())}
472
- onDecline={declineCall}
473
- />
474
- ) : call.phase === 'error' ? (
475
- <RingingCard
476
- title={ringingHeader}
477
- subtitle={ringingSubtitle}
478
- roomName={call.roomName}
479
- kind={call.kind}
480
- variant="error"
481
- primaryColor={primaryColor}
482
- errorMessage={
483
- call.error ||
484
- (!livekitUrl ? t('call.error.missingLivekitUrl') : null)
485
- }
486
- onDismiss={() => dispatch(resetCall())}
487
- />
488
- ) : (
489
- <RingingCard
490
- title={ringingHeader}
491
- subtitle={ringingSubtitle}
492
- roomName={call.roomName}
493
- kind={call.kind}
494
- variant="outgoing"
495
- primaryColor={primaryColor}
496
- icons={videoCallsConfig?.icons}
497
- onHangup={() => terminateCall('cancelled')}
498
- />
499
- )}
500
- </View>
474
+ ) : call.phase === 'error' ? (
475
+ <RingingCard
476
+ title={ringingHeader}
477
+ subtitle={ringingSubtitle}
478
+ roomName={call.roomName}
479
+ kind={call.kind}
480
+ variant="error"
481
+ primaryColor={primaryColor}
482
+ theme={theme}
483
+ errorMessage={
484
+ call.error ||
485
+ (!livekitUrl ? t('call.error.missingLivekitUrl') : null)
486
+ }
487
+ onDismiss={() => dispatch(resetCall())}
488
+ />
489
+ ) : (
490
+ <RingingCard
491
+ title={ringingHeader}
492
+ subtitle={ringingSubtitle}
493
+ roomName={call.roomName}
494
+ kind={call.kind}
495
+ variant="outgoing"
496
+ primaryColor={primaryColor}
497
+ icons={videoCallsConfig?.icons}
498
+ theme={theme}
499
+ onHangup={() => terminateActiveCall('cancelled')}
500
+ />
501
+ )}
502
+ </View>
503
+ )}
501
504
  </Modal>
502
505
  );
503
506
  };
504
507
 
508
+ export const VideoCallOverlay: FC = () => {
509
+ const callId = useSelector(
510
+ (state: RootState) => state.call.callId || String(state.call.startedAt || '')
511
+ );
512
+ return (
513
+ <CallErrorBoundary resetKey={callId}>
514
+ <VideoCallOverlayInner />
515
+ </CallErrorBoundary>
516
+ );
517
+ };
518
+
505
519
  const styles = StyleSheet.create({
506
520
  backdrop: {
507
521
  flex: 1,
508
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
509
522
  alignItems: 'center',
510
523
  justifyContent: 'center',
511
524
  padding: 16,
512
525
  },
513
- sessionCard: {
514
- width: '100%',
515
- height: '100%',
516
- borderRadius: 24,
517
- backgroundColor: '#fff',
518
- overflow: 'hidden',
519
- },
520
- // Audio calls don't need the full video canvas: a fixed tall card leaves
521
- // a dead gap around the avatar and strands the controls far from
522
- // anything else, so this one shrink-wraps its content.
523
- audioSessionCard: {
524
- width: '100%',
525
- maxHeight: 600,
526
- flexGrow: 0,
527
- flexShrink: 1,
528
- minHeight: 420,
529
- borderRadius: 24,
530
- backgroundColor: '#fff',
531
- overflow: 'hidden',
526
+ sessionFull: {
527
+ flex: 1,
528
+ backgroundColor: '#0B1220',
532
529
  },
533
530
  floatingPanel: {
534
531
  position: 'absolute',
@@ -543,13 +540,11 @@ const styles = StyleSheet.create({
543
540
  maxWidth: 420,
544
541
  padding: 32,
545
542
  borderRadius: 24,
546
- backgroundColor: '#fff',
547
543
  alignItems: 'center',
548
544
  gap: 24,
549
545
  },
550
546
  ringingTitle: {
551
547
  fontSize: 14,
552
- color: TEXT_MUTED,
553
548
  letterSpacing: 0.2,
554
549
  },
555
550
  avatarWrap: {
@@ -571,7 +566,6 @@ const styles = StyleSheet.create({
571
566
  },
572
567
  ringingName: {
573
568
  fontSize: 24,
574
- color: TEXT_PRIMARY,
575
569
  textAlign: 'center',
576
570
  },
577
571
  ringingSubtitleRow: {
@@ -580,7 +574,6 @@ const styles = StyleSheet.create({
580
574
  gap: 6,
581
575
  },
582
576
  ringingSubtitle: {
583
- color: TEXT_MUTED,
584
577
  fontSize: 14,
585
578
  },
586
579
  errorBox: {