@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
@@ -44,15 +44,25 @@ import { runLogoutFlow } from '../../Menu/HeaderRoomListMenu';
44
44
  import { useLogout } from '../../../hooks/useLogout';
45
45
  import {
46
46
  ApiRoom,
47
+ createChatCall,
47
48
  ownAppId,
48
49
  postPrivateRoom,
49
50
  } from '../../../networking/api-requests/rooms.api';
50
51
  import EncryptionCard from '../../MainComponents/EncryptionCard';
52
+ import { useSelector } from 'react-redux';
53
+ import { store, type RootState } from '../../../roomStore';
54
+ import { setCallError, startOutgoingCall } from '../../../roomStore/callSlice';
55
+ import { sendCallInviteSignal } from '../../../networking/callTokenStanza';
56
+ import { AudioCallIcon, VideoCallIcon } from '../../VideoCalls/CallIcons';
51
57
  import { accountNameOf } from '../../../helpers/accountName';
52
58
  import { setChatLanguage } from '../../../services/languageSettings';
53
59
  import { composeName } from '../../../helpers/displayName';
54
60
  import { isUnresolvedSenderId } from '../../../helpers/isUnresolvedSenderId';
55
61
 
62
+ /** The user id part of `${appId}_${userId}[@host]` (or a bare id). */
63
+ const userIdOf = (value?: string | null): string =>
64
+ String(value || '').split('@')[0].split('_').pop() || '';
65
+
56
66
  /** A name, unless it is only the account's id standing in for one. */
57
67
  const nameOrNothing = (name?: string | null): string => {
58
68
  const trimmed = String(name ?? '').trim();
@@ -239,7 +249,7 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
239
249
  usersArrayLength
240
250
  );
241
251
 
242
- if (!normalizedChat || !client) {return;}
252
+ if (!normalizedChat || !client) {return null;}
243
253
 
244
254
  dispatch(addRoomViaApi({ room: normalizedChat, xmpp: client }));
245
255
  dispatch(setCurrentRoom({ roomJID: normalizedChat?.jid || '' }));
@@ -251,11 +261,93 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
251
261
  type: 'success',
252
262
  duration: 3000,
253
263
  });
264
+ return normalizedChat;
254
265
  } catch (error) {
255
266
  console.error('Error handling room creation:', error);
267
+ return null;
256
268
  }
257
269
  };
258
270
 
271
+ // Open (or create) the 1:1 room with the profile's user. Shared by
272
+ // "Message" and the call buttons, which need the room's JID to dial.
273
+ const ensurePrivateRoom = useCallback(async (
274
+ e2ee = false
275
+ ): Promise<{
276
+ jid: string;
277
+ bareName: string;
278
+ peerXmppUsername: string | null;
279
+ peerDisplay: string;
280
+ } | null> => {
281
+ const peerDisplay = nameOrNothing(selectedUser?.name);
282
+ if (config?.newArch) {
283
+ const target = selectedUser?.userJID ?? (selectedUser?.id || '');
284
+ // Reuse the 1:1 room we already have with this user instead of
285
+ // POSTing /chats/private again: repeating it for an existing room
286
+ // has been seen to inflate the server's usersCnt and member list.
287
+ const peerLocal = String(target).split('@')[0];
288
+ const existing = Object.values(store.getState().rooms.rooms || {}).find(
289
+ (room: any) =>
290
+ room?.type === 'private' &&
291
+ !!peerLocal &&
292
+ [...(room.roomMembers || []), ...(room.members || [])].some(
293
+ // REST rows carry the bare user id, stanza rows `${appId}_${id}`:
294
+ // compare on the user id part.
295
+ (m: any) => userIdOf(m?.xmppUsername || m?._id) === userIdOf(peerLocal)
296
+ )
297
+ ) as any;
298
+ if (existing?.jid && !e2ee) {
299
+ dispatch(setCurrentRoom({ roomJID: existing.jid }));
300
+ return {
301
+ jid: existing.jid,
302
+ bareName: existing.jid.split('@')[0],
303
+ peerXmppUsername: peerLocal || null,
304
+ peerDisplay: peerDisplay || nameOrNothing(existing.title),
305
+ };
306
+ }
307
+ const newRoom = await postPrivateRoom(target, e2ee);
308
+ const created = await handleRoomCreation(newRoom, 2);
309
+ if (!created?.jid) {return null;}
310
+ return {
311
+ jid: created.jid,
312
+ bareName: created.jid.split('@')[0],
313
+ peerXmppUsername: String(target).split('@')[0] || null,
314
+ peerDisplay: peerDisplay || nameOrNothing((created as any).title),
315
+ };
316
+ }
317
+
318
+ const selectedUserUsername = walletToUsername(selectedUser?.id || '');
319
+ const myUsername = walletToUsername(user.defaultWallet.walletAddress);
320
+
321
+ const roomJid = [myUsername, selectedUserUsername]
322
+ .sort()
323
+ .join('.')
324
+ .toLowerCase();
325
+
326
+ const combinedUsersName = [
327
+ user.firstName,
328
+ selectedUser?.name?.split(' ')?.[0] || '',
329
+ ]
330
+ .sort()
331
+ .join(' and ');
332
+
333
+ const newRoomJid =
334
+ (await client?.createPrivateRoomStanza(
335
+ combinedUsersName,
336
+ `Private chat ${combinedUsersName}`,
337
+ roomJid
338
+ )) || '';
339
+
340
+ if (!newRoomJid) {return null;}
341
+ await client?.inviteRoomRequestStanza(selectedUserUsername, newRoomJid);
342
+ await client?.getRoomsStanza();
343
+ return {
344
+ jid: newRoomJid,
345
+ bareName: newRoomJid.split('@')[0],
346
+ peerXmppUsername: selectedUserUsername || null,
347
+ peerDisplay,
348
+ };
349
+ }, [selectedUser, config?.newArch, client, user]); // eslint-disable-line react-hooks/exhaustive-deps
350
+
259
351
  const openPrivateRoom = useCallback(async (e2ee: boolean) => {
260
352
  showToast({
261
353
  id: Date.now().toString(),
@@ -264,43 +356,62 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
264
356
  type: 'info',
265
357
  duration: 3000,
266
358
  });
267
- if (config?.newArch) {
268
- const newRoom = await postPrivateRoom(
269
- selectedUser?.userJID ?? (selectedUser?.id || ''),
270
- e2ee
271
- );
272
- handleRoomCreation(newRoom, 2);
273
- } else {
274
- const selectedUserUsername = walletToUsername(selectedUser?.id || '');
275
- const myUsername = walletToUsername(user.defaultWallet.walletAddress);
276
-
277
- const roomJid = [myUsername, selectedUserUsername]
278
- .sort()
279
- .join('.')
280
- .toLowerCase();
281
-
282
- const combinedUsersName = [
283
- user.firstName,
284
- selectedUser?.name?.split(' ')?.[0] || '',
285
- ]
286
- .sort()
287
- .join(' and ');
288
-
289
- const newRoomJid =
290
- (await client?.createPrivateRoomStanza(
291
- combinedUsersName,
292
- `Private chat ${combinedUsersName}`,
293
- roomJid
294
- )) || '';
295
-
296
- if (newRoomJid) {
297
- await client?.inviteRoomRequestStanza(selectedUserUsername, newRoomJid);
298
- await client?.getRoomsStanza();
299
- }
359
+ await ensurePrivateRoom(e2ee);
360
+ dispatch(setActiveModal(undefined));
361
+ }, [ensurePrivateRoom, t]); // eslint-disable-line react-hooks/exhaustive-deps
362
+
363
+ // Calls from the profile, as on web: open the 1:1 room, then dial it the
364
+ // same way the chat header does.
365
+ const videoCallsConfig = config?.videoCalls;
366
+ const canCall =
367
+ videoCallsConfig?.enabled === true &&
368
+ Boolean(videoCallsConfig?.livekitUrl?.trim()) &&
369
+ (videoCallsConfig?.allowedRoomTypes || ['private']).includes('private');
370
+ const isAudioCallsEnabled = canCall && videoCallsConfig?.enableAudioCalls === true;
371
+ const isCallBusy = useSelector((state: RootState) => state.call.phase !== 'idle');
372
+
373
+ const handleCall = useCallback(async (kind: 'audio' | 'video') => {
374
+ if (isCallBusy) {return;}
375
+ let room: Awaited<ReturnType<typeof ensurePrivateRoom>> = null;
376
+ try {
377
+ room = await ensurePrivateRoom();
378
+ } catch (error) {
379
+ console.warn('Could not open the private chat for a call:', error);
380
+ }
381
+ if (!room) {return;}
382
+
383
+ dispatch(
384
+ startOutgoingCall({
385
+ roomJid: room.jid,
386
+ roomName: room.peerDisplay || room.bareName,
387
+ roomBareName: room.bareName,
388
+ kind,
389
+ peerXmppUsername: room.peerXmppUsername,
390
+ })
391
+ );
392
+
393
+ // The server drops `kind` on the relayed call-token, so signal the peer
394
+ // directly first, as CallButtons does.
395
+ if (room.peerXmppUsername) {
396
+ sendCallInviteSignal(kind, {
397
+ peerXmppUsername: room.peerXmppUsername,
398
+ roomBareName: room.bareName,
399
+ });
300
400
  }
301
401
 
402
+ // Close the profile so the call screen isn't behind it.
302
403
  dispatch(setActiveModal(undefined));
303
- }, [selectedUser, t]); // eslint-disable-line react-hooks/exhaustive-deps
404
+
405
+ try {
406
+ await createChatCall(room.bareName, { kind });
407
+ } catch (error) {
408
+ dispatch(
409
+ setCallError(
410
+ error instanceof Error ? error.message : t('call.error.connectFailed')
411
+ )
412
+ );
413
+ }
414
+ }, [ensurePrivateRoom, isCallBusy, t]); // eslint-disable-line react-hooks/exhaustive-deps
304
415
 
305
416
  const handlePrivateMessage = useCallback(
306
417
  () => openPrivateRoom(false),
@@ -374,6 +485,26 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
374
485
  ]
375
486
  : []),
376
487
  ]),
488
+ ...(canCall && !isCallBusy
489
+ ? [
490
+ {
491
+ key: 'video-call',
492
+ label: isAudioCallsEnabled ? t('action.videoCall') : t('action.call'),
493
+ icon: (color: string) => <VideoCallIcon color={color} />,
494
+ onPress: () => void handleCall('video'),
495
+ },
496
+ ...(isAudioCallsEnabled
497
+ ? [
498
+ {
499
+ key: 'audio-call',
500
+ label: t('action.audioCall'),
501
+ icon: (color: string) => <AudioCallIcon color={color} />,
502
+ onPress: () => void handleCall('audio'),
503
+ },
504
+ ]
505
+ : []),
506
+ ]
507
+ : []),
377
508
  {
378
509
  key: 'share',
379
510
  label: t('action.share'),
@@ -381,7 +512,7 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
381
512
  onPress: handleShare,
382
513
  },
383
514
  ],
384
- [isOwnProfile, t, handleShare, handlePrivateMessage, handleEncryptedPrivateMessage, canStartEncrypted, handleLogout] // eslint-disable-line react-hooks/exhaustive-deps
515
+ [isOwnProfile, t, handleShare, handlePrivateMessage, handleEncryptedPrivateMessage, canStartEncrypted, handleLogout, canCall, isCallBusy, isAudioCallsEnabled, handleCall] // eslint-disable-line react-hooks/exhaustive-deps
385
516
  );
386
517
 
387
518
  // Collapsed, the hero's round buttons are gone — the "…" menu carries
@@ -22,6 +22,7 @@ import { useTheme } from '../../hooks/useTheme';
22
22
  import { resolveTheme } from '../../theme/theme';
23
23
  import { BellOffIcon, LockIcon } from '../../assets/icons';
24
24
  import { useT } from '../../i18n/useT';
25
+ import { formatCallLogLabel } from '../../helpers/callLogMessage';
25
26
  import { composeName } from '../../helpers/displayName';
26
27
  import { isUnresolvedSenderId } from '../../helpers/isUnresolvedSenderId';
27
28
 
@@ -75,13 +76,19 @@ const ChatRoomItem: React.FC<ChatRoomItemProps> = ({
75
76
 
76
77
  return {
77
78
  ...message,
79
+ // A call-log entry's body is English frozen at receive time; rebuild
80
+ // it from the callLog meta so the preview follows the current
81
+ // language like the transcript does (mirrors web's ChatRoomItem).
82
+ body: message.callLog
83
+ ? formatCallLogLabel(message.callLog, t, message.body)
84
+ : message.body,
78
85
  user: {
79
86
  ...message.user,
80
87
  name: safeName,
81
88
  },
82
89
  };
83
90
  },
84
- [usersSet]
91
+ [usersSet, t]
85
92
  );
86
93
 
87
94
  const lastMessage = useMemo(() => {
@@ -0,0 +1,176 @@
1
+ import React, { FC, useEffect, useState } from 'react';
2
+ import { Pressable, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
+ import { useSelector } from 'react-redux';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+ import { RootState } from '../../roomStore';
6
+ import { useChatSettingState } from '../../hooks/useChatSettingState';
7
+ import { useTheme } from '../../hooks/useTheme';
8
+ import { useT } from '../../i18n/useT';
9
+ import { ExpandIcon, HangUpIcon, MicOffIcon, MicOnIcon } from './CallIcons';
10
+ import { toggleCallMic, useCallSession } from './callSessionController';
11
+ import { terminateActiveCall } from './callActions';
12
+ import { registerCallBarHost, setCallMinimized, useCallUi } from './callUiStore';
13
+ import { formatCallDuration, useSharedCallElapsed } from './VideoCallSession';
14
+
15
+ const DANGER = '#E53935';
16
+ const ON_BAR = '#FFFFFF';
17
+ const CONTROL = 'rgba(255, 255, 255, 0.2)';
18
+
19
+ /** True while a call is up and minimized, i.e. the strip should show. */
20
+ const useShowCallBar = (): boolean => {
21
+ const { minimized } = useCallUi();
22
+ const active = useSelector(
23
+ (state: RootState) =>
24
+ !!state.call.token &&
25
+ (state.call.phase === 'connecting' || state.call.phase === 'in-call')
26
+ );
27
+ const { config } = useChatSettingState();
28
+ return minimized && active && config?.videoCalls?.enabled === true;
29
+ };
30
+
31
+ /**
32
+ * The minimized call: a strip with who, how long, mic, expand and hang up.
33
+ * Tapping anywhere on it (outside the buttons) brings the call back
34
+ * full-screen, like Telegram's "return to call" bar.
35
+ */
36
+ export const ActiveCallBar: FC<{ topInset?: number }> = ({ topInset = 0 }) => {
37
+ const t = useT();
38
+ const theme = useTheme();
39
+ const { icons } = useChatSettingState().config?.videoCalls || {};
40
+ const peerName = useSelector((state: RootState) => state.call.roomName);
41
+ const session = useCallSession();
42
+ const elapsed = useSharedCallElapsed(session.anchorAt);
43
+
44
+ const name = peerName || session.remoteName || t('call.unknownCaller');
45
+ const status =
46
+ elapsed !== null
47
+ ? formatCallDuration(elapsed)
48
+ : !session.connected
49
+ ? t('call.status.connecting')
50
+ : !session.remoteJoined
51
+ ? t('call.status.waitingForPeer')
52
+ : t('call.status.inCall');
53
+ const statusLine = session.remoteMicMuted
54
+ ? `${status} · ${t('call.peerMuted', { name })}`
55
+ : status;
56
+ const expand = () => setCallMinimized(false);
57
+
58
+ return (
59
+ <Pressable
60
+ onPress={expand}
61
+ accessibilityRole="button"
62
+ accessibilityLabel={t('call.action.expand')}
63
+ style={[styles.bar, { backgroundColor: theme.success, paddingTop: 8 + topInset }]}
64
+ >
65
+ <View style={styles.info}>
66
+ <Text numberOfLines={1} style={styles.name}>
67
+ {name}
68
+ </Text>
69
+ <Text numberOfLines={1} style={styles.status}>
70
+ {statusLine}
71
+ </Text>
72
+ </View>
73
+ <TouchableOpacity
74
+ accessibilityRole="button"
75
+ accessibilityLabel={session.micOn ? t('call.action.muteMic') : t('call.action.unmuteMic')}
76
+ onPress={toggleCallMic}
77
+ style={[styles.button, { backgroundColor: session.micOn ? CONTROL : DANGER }]}
78
+ hitSlop={6}
79
+ >
80
+ {session.micOn
81
+ ? (icons?.micOn ?? <MicOnIcon color={ON_BAR} size={18} />)
82
+ : (icons?.micOff ?? <MicOffIcon color={ON_BAR} size={18} />)}
83
+ </TouchableOpacity>
84
+ <TouchableOpacity
85
+ accessibilityRole="button"
86
+ accessibilityLabel={t('call.action.expand')}
87
+ onPress={expand}
88
+ style={[styles.button, { backgroundColor: CONTROL }]}
89
+ hitSlop={6}
90
+ >
91
+ <ExpandIcon color={ON_BAR} size={18} />
92
+ </TouchableOpacity>
93
+ <TouchableOpacity
94
+ accessibilityRole="button"
95
+ accessibilityLabel={t('call.action.hangUp')}
96
+ onPress={() => terminateActiveCall('ended')}
97
+ style={[styles.button, { backgroundColor: DANGER }]}
98
+ hitSlop={6}
99
+ >
100
+ {icons?.hangup ?? <HangUpIcon color={ON_BAR} size={18} />}
101
+ </TouchableOpacity>
102
+ </Pressable>
103
+ );
104
+ };
105
+
106
+ /**
107
+ * Mounted under each SDK header (chat and room list). Registers the header
108
+ * as a place for the strip and renders it while this header is the current
109
+ * host and a call is minimized; renders nothing otherwise.
110
+ */
111
+ export const HeaderCallBar: FC = () => {
112
+ const [hostId, setHostId] = useState<number | null>(null);
113
+ const { barHost } = useCallUi();
114
+ const show = useShowCallBar();
115
+
116
+ // Returns its own unregister, which is the effect's cleanup.
117
+ useEffect(() => registerCallBarHost(setHostId), []);
118
+
119
+ if (!show || hostId === null || barHost !== hostId) {return null;}
120
+ return <ActiveCallBar />;
121
+ };
122
+
123
+ /**
124
+ * The overlay's fallback when no SDK header is on screen: the same strip,
125
+ * docked to the top under the status bar.
126
+ */
127
+ export const DockedCallBar: FC = () => {
128
+ const insets = useSafeAreaInsets();
129
+ const { barHost } = useCallUi();
130
+ const show = useShowCallBar();
131
+ if (!show || barHost !== null) {return null;}
132
+ return (
133
+ <View style={styles.docked} pointerEvents="box-none">
134
+ <ActiveCallBar topInset={insets.top} />
135
+ </View>
136
+ );
137
+ };
138
+
139
+ const styles = StyleSheet.create({
140
+ bar: {
141
+ flexDirection: 'row',
142
+ alignItems: 'center',
143
+ gap: 10,
144
+ paddingHorizontal: 16,
145
+ paddingBottom: 8,
146
+ },
147
+ info: {
148
+ flex: 1,
149
+ minWidth: 0,
150
+ },
151
+ name: {
152
+ color: ON_BAR,
153
+ fontSize: 15,
154
+ fontWeight: '600',
155
+ },
156
+ status: {
157
+ color: 'rgba(255, 255, 255, 0.85)',
158
+ fontSize: 12,
159
+ fontVariant: ['tabular-nums'],
160
+ },
161
+ button: {
162
+ width: 34,
163
+ height: 34,
164
+ borderRadius: 17,
165
+ alignItems: 'center',
166
+ justifyContent: 'center',
167
+ },
168
+ docked: {
169
+ position: 'absolute',
170
+ top: 0,
171
+ left: 0,
172
+ right: 0,
173
+ zIndex: 1001,
174
+ elevation: 12,
175
+ },
176
+ });
@@ -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(