@ethora/chat-component-rn 26.10.2 → 26.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/lib/commonjs/components/Header/HeaderRoomList.js +72 -69
  2. package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
  3. package/lib/commonjs/components/MainComponents/ChatHeader.js +13 -11
  4. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/MessageContainer.js +10 -2
  6. package/lib/commonjs/components/MainComponents/MessageContainer.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/RoomList.js +8 -18
  8. package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
  9. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +17 -14
  10. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  11. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +131 -17
  12. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  13. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +6 -1
  14. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
  15. package/lib/commonjs/components/VideoCalls/ActiveCallBar.js +203 -0
  16. package/lib/commonjs/components/VideoCalls/ActiveCallBar.js.map +1 -0
  17. package/lib/commonjs/components/VideoCalls/CallButtons.js +3 -1
  18. package/lib/commonjs/components/VideoCalls/CallButtons.js.map +1 -1
  19. package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +123 -129
  20. package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -1
  21. package/lib/commonjs/components/VideoCalls/VideoCallSession.js +212 -414
  22. package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -1
  23. package/lib/commonjs/components/VideoCalls/callActions.js +61 -0
  24. package/lib/commonjs/components/VideoCalls/callActions.js.map +1 -0
  25. package/lib/commonjs/components/VideoCalls/callSessionController.js +461 -0
  26. package/lib/commonjs/components/VideoCalls/callSessionController.js.map +1 -0
  27. package/lib/commonjs/components/VideoCalls/callUiStore.js +91 -0
  28. package/lib/commonjs/components/VideoCalls/callUiStore.js.map +1 -0
  29. package/lib/commonjs/components/VideoCalls/livekitRuntime.js +1 -0
  30. package/lib/commonjs/components/VideoCalls/livekitRuntime.js.map +1 -1
  31. package/lib/commonjs/components/VideoCalls/useDraggable.js +59 -0
  32. package/lib/commonjs/components/VideoCalls/useDraggable.js.map +1 -0
  33. package/lib/commonjs/helpers/createRoomFromApi.js +4 -0
  34. package/lib/commonjs/helpers/createRoomFromApi.js.map +1 -1
  35. package/lib/commonjs/helpers/incomingCallFromPush.js +1 -1
  36. package/lib/commonjs/helpers/incomingCallFromPush.js.map +1 -1
  37. package/lib/commonjs/helpers/roomUserCount.js +7 -1
  38. package/lib/commonjs/helpers/roomUserCount.js.map +1 -1
  39. package/lib/commonjs/helpers/uniqueMembers.js +36 -0
  40. package/lib/commonjs/helpers/uniqueMembers.js.map +1 -0
  41. package/lib/commonjs/hooks/useLeaveRoom.js +52 -0
  42. package/lib/commonjs/hooks/useLeaveRoom.js.map +1 -0
  43. package/lib/commonjs/i18n/strings.js +24 -0
  44. package/lib/commonjs/i18n/strings.js.map +1 -1
  45. package/lib/commonjs/networking/api-requests/rooms.api.js +36 -1
  46. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  47. package/lib/commonjs/networking/callTokenStanza.js +47 -3
  48. package/lib/commonjs/networking/callTokenStanza.js.map +1 -1
  49. package/lib/commonjs/networking/xmppClient.js +9 -2
  50. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  51. package/lib/commonjs/version.js +1 -1
  52. package/lib/module/components/Header/HeaderRoomList.js +73 -70
  53. package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
  54. package/lib/module/components/MainComponents/ChatHeader.js +15 -13
  55. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  56. package/lib/module/components/MainComponents/MessageContainer.js +10 -2
  57. package/lib/module/components/MainComponents/MessageContainer.js.map +1 -1
  58. package/lib/module/components/MainComponents/RoomList.js +8 -18
  59. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  60. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +18 -15
  61. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  62. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +132 -18
  63. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  64. package/lib/module/components/RoomComponents/ChatRoomItem.js +6 -1
  65. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  66. package/lib/module/components/VideoCalls/ActiveCallBar.js +195 -0
  67. package/lib/module/components/VideoCalls/ActiveCallBar.js.map +1 -0
  68. package/lib/module/components/VideoCalls/CallButtons.js +3 -1
  69. package/lib/module/components/VideoCalls/CallButtons.js.map +1 -1
  70. package/lib/module/components/VideoCalls/VideoCallOverlay.js +125 -131
  71. package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -1
  72. package/lib/module/components/VideoCalls/VideoCallSession.js +213 -417
  73. package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -1
  74. package/lib/module/components/VideoCalls/callActions.js +55 -0
  75. package/lib/module/components/VideoCalls/callActions.js.map +1 -0
  76. package/lib/module/components/VideoCalls/callSessionController.js +450 -0
  77. package/lib/module/components/VideoCalls/callSessionController.js.map +1 -0
  78. package/lib/module/components/VideoCalls/callUiStore.js +84 -0
  79. package/lib/module/components/VideoCalls/callUiStore.js.map +1 -0
  80. package/lib/module/components/VideoCalls/livekitRuntime.js +1 -0
  81. package/lib/module/components/VideoCalls/livekitRuntime.js.map +1 -1
  82. package/lib/module/components/VideoCalls/useDraggable.js +55 -0
  83. package/lib/module/components/VideoCalls/useDraggable.js.map +1 -0
  84. package/lib/module/helpers/createRoomFromApi.js +4 -0
  85. package/lib/module/helpers/createRoomFromApi.js.map +1 -1
  86. package/lib/module/helpers/incomingCallFromPush.js +1 -1
  87. package/lib/module/helpers/incomingCallFromPush.js.map +1 -1
  88. package/lib/module/helpers/roomUserCount.js +7 -1
  89. package/lib/module/helpers/roomUserCount.js.map +1 -1
  90. package/lib/module/helpers/uniqueMembers.js +31 -0
  91. package/lib/module/helpers/uniqueMembers.js.map +1 -0
  92. package/lib/module/hooks/useLeaveRoom.js +48 -0
  93. package/lib/module/hooks/useLeaveRoom.js.map +1 -0
  94. package/lib/module/i18n/strings.js +24 -0
  95. package/lib/module/i18n/strings.js.map +1 -1
  96. package/lib/module/networking/api-requests/rooms.api.js +35 -1
  97. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  98. package/lib/module/networking/callTokenStanza.js +47 -3
  99. package/lib/module/networking/callTokenStanza.js.map +1 -1
  100. package/lib/module/networking/xmppClient.js +8 -1
  101. package/lib/module/networking/xmppClient.js.map +1 -1
  102. package/lib/module/version.js +1 -1
  103. package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
  104. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  105. package/lib/typescript/components/MainComponents/MessageContainer.d.ts.map +1 -1
  106. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  107. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  108. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
  109. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  110. package/lib/typescript/components/VideoCalls/ActiveCallBar.d.ts +21 -0
  111. package/lib/typescript/components/VideoCalls/ActiveCallBar.d.ts.map +1 -0
  112. package/lib/typescript/components/VideoCalls/CallButtons.d.ts.map +1 -1
  113. package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts +0 -6
  114. package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts.map +1 -1
  115. package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts +15 -22
  116. package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
  117. package/lib/typescript/components/VideoCalls/callActions.d.ts +15 -0
  118. package/lib/typescript/components/VideoCalls/callActions.d.ts.map +1 -0
  119. package/lib/typescript/components/VideoCalls/callSessionController.d.ts +63 -0
  120. package/lib/typescript/components/VideoCalls/callSessionController.d.ts.map +1 -0
  121. package/lib/typescript/components/VideoCalls/callUiStore.d.ts +24 -0
  122. package/lib/typescript/components/VideoCalls/callUiStore.d.ts.map +1 -0
  123. package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts +2 -0
  124. package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts.map +1 -1
  125. package/lib/typescript/components/VideoCalls/useDraggable.d.ts +19 -0
  126. package/lib/typescript/components/VideoCalls/useDraggable.d.ts.map +1 -0
  127. package/lib/typescript/helpers/createRoomFromApi.d.ts.map +1 -1
  128. package/lib/typescript/helpers/roomUserCount.d.ts +1 -0
  129. package/lib/typescript/helpers/roomUserCount.d.ts.map +1 -1
  130. package/lib/typescript/helpers/uniqueMembers.d.ts +11 -0
  131. package/lib/typescript/helpers/uniqueMembers.d.ts.map +1 -0
  132. package/lib/typescript/hooks/useLeaveRoom.d.ts +9 -0
  133. package/lib/typescript/hooks/useLeaveRoom.d.ts.map +1 -0
  134. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  135. package/lib/typescript/networking/api-requests/rooms.api.d.ts +8 -0
  136. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  137. package/lib/typescript/networking/callTokenStanza.d.ts.map +1 -1
  138. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  139. package/lib/typescript/version.d.ts +1 -1
  140. package/package.json +1 -1
  141. package/src/components/Header/HeaderRoomList.tsx +66 -61
  142. package/src/components/MainComponents/ChatHeader.tsx +13 -8
  143. package/src/components/MainComponents/MessageContainer.tsx +14 -1
  144. package/src/components/MainComponents/RoomList.tsx +8 -14
  145. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +16 -13
  146. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +167 -36
  147. package/src/components/RoomComponents/ChatRoomItem.tsx +8 -1
  148. package/src/components/VideoCalls/ActiveCallBar.tsx +176 -0
  149. package/src/components/VideoCalls/CallButtons.tsx +5 -1
  150. package/src/components/VideoCalls/VideoCallOverlay.tsx +153 -183
  151. package/src/components/VideoCalls/VideoCallSession.tsx +227 -439
  152. package/src/components/VideoCalls/callActions.ts +63 -0
  153. package/src/components/VideoCalls/callSessionController.ts +439 -0
  154. package/src/components/VideoCalls/callUiStore.ts +76 -0
  155. package/src/components/VideoCalls/livekitRuntime.ts +3 -0
  156. package/src/components/VideoCalls/useDraggable.ts +46 -0
  157. package/src/helpers/createRoomFromApi.tsx +2 -0
  158. package/src/helpers/incomingCallFromPush.ts +2 -2
  159. package/src/helpers/roomUserCount.ts +6 -2
  160. package/src/helpers/uniqueMembers.ts +26 -0
  161. package/src/hooks/useLeaveRoom.ts +42 -0
  162. package/src/i18n/strings.ts +24 -0
  163. package/src/networking/api-requests/rooms.api.ts +28 -1
  164. package/src/networking/callTokenStanza.ts +46 -6
  165. package/src/networking/xmppClient.ts +10 -1
  166. package/src/version.ts +1 -1
@@ -1,10 +1,11 @@
1
1
  import React, {
2
+ Component,
2
3
  FC,
4
+ ReactNode,
3
5
  useCallback,
4
6
  useEffect,
5
7
  useMemo,
6
8
  useRef,
7
- useState,
8
9
  } from 'react';
9
10
  import {
10
11
  Animated,
@@ -19,23 +20,21 @@ import {
19
20
  View,
20
21
  } from 'react-native';
21
22
  import { useDispatch, useSelector } from 'react-redux';
22
- import { RootState, store } from '../../roomStore';
23
+ import { RootState } from '../../roomStore';
23
24
  import {
24
25
  acceptIncomingCall,
25
- declineIncomingCall,
26
- endCall,
27
26
  resetCall,
28
27
  setCallError,
29
- setCallPhase,
30
28
  } from '../../roomStore/callSlice';
31
- import { addRoomMessage } from '../../roomStore/roomsSlice';
32
- import { buildLocalCallLogMessage } from '../../helpers/callLogMessage';
33
29
  import { useChatSettingState } from '../../hooks/useChatSettingState';
34
- import {
35
- CallSignalState,
36
- sendCallStateSignal,
37
- } from '../../networking/callTokenStanza';
30
+ import { sendCallStateSignal } from '../../networking/callTokenStanza';
31
+ import { useTheme } from '../../hooks/useTheme';
32
+ import type { ChatTheme } from '../../theme/theme';
38
33
  import { VideoCallSession } from './VideoCallSession';
34
+ import { startCallSession } from './callSessionController';
35
+ import { declineActiveCall, terminateActiveCall } from './callActions';
36
+ import { DockedCallBar } from './ActiveCallBar';
37
+ import { setCallMinimized, useCallUi } from './callUiStore';
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,57 @@ 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 header strip
291
+ * (two different trees) 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();
265
299
 
266
300
  const videoCallsConfig = config?.videoCalls;
267
301
  const enabled = videoCallsConfig?.enabled === true;
268
302
  const livekitUrl = (videoCallsConfig?.livekitUrl || '').trim();
269
- const primaryColor = config?.colors?.primary || DEFAULT_PRIMARY;
303
+ const primaryColor = theme.primary || config?.colors?.primary || DEFAULT_PRIMARY;
270
304
 
271
305
  const isOpen = enabled && call.phase !== 'idle';
272
306
 
@@ -275,7 +309,8 @@ export const VideoCallOverlay: FC = () => {
275
309
  // fallback and always renders.
276
310
  useCallKeep();
277
311
 
278
- const [minimized, setMinimized] = useState(false);
312
+ // Shared with the header strip (ActiveCallBar), which expands it again.
313
+ const { minimized } = useCallUi();
279
314
 
280
315
  const canRenderSession =
281
316
  !!call.token &&
@@ -283,18 +318,29 @@ export const VideoCallOverlay: FC = () => {
283
318
  !!livekitUrl;
284
319
  const isMinimized = minimized && canRenderSession;
285
320
 
286
- // Reset the minimized flag whenever the call ends, so the next call
287
- // opens full-screen.
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.
288
323
  useEffect(() => {
289
- if (call.phase === 'idle') {setMinimized(false);}
290
- }, [call.phase]);
291
-
292
- // Give up on an unanswered outgoing dial.
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
+
335
+ // Give up on an unanswered outgoing dial. Unlike web (which only shows
336
+ // the error), also tell the callee, who would otherwise keep ringing:
337
+ // there is no incoming ring timeout on either platform.
293
338
  useEffect(() => {
294
339
  if (call.phase !== 'requesting' || call.direction !== 'outgoing') {
295
340
  return;
296
341
  }
297
342
  const timer = setTimeout(() => {
343
+ sendCallStateSignal('cancelled');
298
344
  dispatch(setCallError(t('call.error.noAnswerTimeout')));
299
345
  }, OUTGOING_CALL_TIMEOUT_MS);
300
346
  return () => clearTimeout(timer);
@@ -311,53 +357,15 @@ export const VideoCallOverlay: FC = () => {
311
357
  return () => Vibration.cancel();
312
358
  }, [call.direction, call.phase]);
313
359
 
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
360
  const hangupCurrent = useCallback(() => {
353
361
  if (call.direction === 'incoming' && call.phase === 'ringing-incoming') {
354
- declineCall();
362
+ declineActiveCall();
355
363
  } else if (call.phase === 'error') {
356
364
  dispatch(resetCall());
357
365
  } else {
358
- terminateCall(call.phase === 'requesting' ? 'cancelled' : 'ended');
366
+ terminateActiveCall(call.phase === 'requesting' ? 'cancelled' : 'ended');
359
367
  }
360
- }, [call.direction, call.phase, declineCall, terminateCall, dispatch]);
368
+ }, [call.direction, call.phase, dispatch]);
361
369
 
362
370
  // Android hardware back = hang up, the RN equivalent of the web Esc
363
371
  // handler. A minimized panel shouldn't swallow back: the user is using
@@ -402,30 +410,22 @@ export const VideoCallOverlay: FC = () => {
402
410
  const showIncomingDecision =
403
411
  call.direction === 'incoming' && call.phase === 'ringing-incoming';
404
412
 
405
- // The minimized session is NOT a Modal: a modal is by definition on top
406
- // of everything and swallows touches, which is the opposite of what
407
- // minimize is for (keep talking, keep using the chat). It renders as a
408
- // floating absolutely-positioned bar instead.
409
- if (canRenderSession && isMinimized) {
410
- 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>
428
- );
413
+ const session = (
414
+ <VideoCallSession
415
+ kind={call.kind}
416
+ peerName={call.roomName}
417
+ primaryColor={primaryColor}
418
+ icons={videoCallsConfig?.icons}
419
+ onToggleMinimize={() => setCallMinimized(!minimized)}
420
+ onHangup={() => terminateActiveCall('ended')}
421
+ />
422
+ );
423
+
424
+ // Minimized, the call is a strip under the visible SDK header
425
+ // (HeaderCallBar), Telegram-style, so the chat stays fully usable. With
426
+ // no SDK header on screen the same strip docks to the top instead.
427
+ if (isMinimized) {
428
+ return <DockedCallBar />;
429
429
  }
430
430
 
431
431
  return (
@@ -434,122 +434,94 @@ export const VideoCallOverlay: FC = () => {
434
434
  transparent
435
435
  animationType="fade"
436
436
  statusBarTranslucent
437
+ supportedOrientations={['portrait', 'landscape']}
437
438
  onRequestClose={hangupCurrent}
438
439
  >
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}
440
+ {canRenderSession ? (
441
+ // Full-bleed: on a phone the call owns the screen, like the native
442
+ // dialer. The audio call keeps the same dark canvas as video.
443
+ <View style={styles.sessionFull}>{session}</View>
444
+ ) : (
445
+ <View style={[styles.backdrop, { backgroundColor: theme.overlay }]}>
446
+ {showIncomingDecision ? (
447
+ <RingingCard
448
+ title={ringingHeader}
449
+ subtitle={ringingSubtitle}
450
+ roomName={call.roomName}
449
451
  kind={call.kind}
450
- peerName={call.roomName}
452
+ variant="incoming"
451
453
  primaryColor={primaryColor}
452
454
  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')}
455
+ theme={theme}
456
+ onAccept={() => dispatch(acceptIncomingCall())}
457
+ onDecline={declineActiveCall}
460
458
  />
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>
459
+ ) : call.phase === 'error' ? (
460
+ <RingingCard
461
+ title={ringingHeader}
462
+ subtitle={ringingSubtitle}
463
+ roomName={call.roomName}
464
+ kind={call.kind}
465
+ variant="error"
466
+ primaryColor={primaryColor}
467
+ theme={theme}
468
+ errorMessage={
469
+ call.error ||
470
+ (!livekitUrl ? t('call.error.missingLivekitUrl') : null)
471
+ }
472
+ onDismiss={() => dispatch(resetCall())}
473
+ />
474
+ ) : (
475
+ <RingingCard
476
+ title={ringingHeader}
477
+ subtitle={ringingSubtitle}
478
+ roomName={call.roomName}
479
+ kind={call.kind}
480
+ variant="outgoing"
481
+ primaryColor={primaryColor}
482
+ icons={videoCallsConfig?.icons}
483
+ theme={theme}
484
+ onHangup={() => terminateActiveCall('cancelled')}
485
+ />
486
+ )}
487
+ </View>
488
+ )}
501
489
  </Modal>
502
490
  );
503
491
  };
504
492
 
493
+ export const VideoCallOverlay: FC = () => {
494
+ const callId = useSelector(
495
+ (state: RootState) => state.call.callId || String(state.call.startedAt || '')
496
+ );
497
+ return (
498
+ <CallErrorBoundary resetKey={callId}>
499
+ <VideoCallOverlayInner />
500
+ </CallErrorBoundary>
501
+ );
502
+ };
503
+
505
504
  const styles = StyleSheet.create({
506
505
  backdrop: {
507
506
  flex: 1,
508
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
509
507
  alignItems: 'center',
510
508
  justifyContent: 'center',
511
509
  padding: 16,
512
510
  },
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',
532
- },
533
- floatingPanel: {
534
- position: 'absolute',
535
- right: 16,
536
- bottom: 24,
537
- left: 16,
538
- zIndex: 1001,
539
- elevation: 12,
511
+ sessionFull: {
512
+ flex: 1,
513
+ backgroundColor: '#0B1220',
540
514
  },
541
515
  ringingCard: {
542
516
  width: '100%',
543
517
  maxWidth: 420,
544
518
  padding: 32,
545
519
  borderRadius: 24,
546
- backgroundColor: '#fff',
547
520
  alignItems: 'center',
548
521
  gap: 24,
549
522
  },
550
523
  ringingTitle: {
551
524
  fontSize: 14,
552
- color: TEXT_MUTED,
553
525
  letterSpacing: 0.2,
554
526
  },
555
527
  avatarWrap: {
@@ -571,7 +543,6 @@ const styles = StyleSheet.create({
571
543
  },
572
544
  ringingName: {
573
545
  fontSize: 24,
574
- color: TEXT_PRIMARY,
575
546
  textAlign: 'center',
576
547
  },
577
548
  ringingSubtitleRow: {
@@ -580,7 +551,6 @@ const styles = StyleSheet.create({
580
551
  gap: 6,
581
552
  },
582
553
  ringingSubtitle: {
583
- color: TEXT_MUTED,
584
554
  fontSize: 14,
585
555
  },
586
556
  errorBox: {