diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 289e28e56a..4466aea2e4 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -7242,12 +7242,12 @@ "description": "On the Calls Tab, when trying to join a different call when you're already in another one, this is the title of the confirmation dialog to leave the other call." }, "icu:CallsList__LeaveCallDialogBody": { - "messageformat": "You must leave the current call before joining a new call.", - "description": "On the Calls Tab, when trying to join a different call when you're already in another one, this is the body of the confirmation dialog to leave the other call." + "messageformat": "You must leave the current call before starting or joining a new call.", + "description": "When trying to join a different call when you're already in another one, this is the body of the confirmation dialog to leave the other call." }, "icu:CallsList__LeaveCallDialogButton--leave": { "messageformat": "Leave call", - "description": "On the Calls Tab, when trying to join a different call when you're already in another one, this is the button to confirm leaving the other call." + "description": "When trying to join a different call when you're already in another one, this is the button to confirm leaving the other call." }, "icu:CallsNewCall__EmptyState--noQuery": { "messageformat": "No recent conversations.", diff --git a/stylesheets/components/CallsTab.scss b/stylesheets/components/CallsTab.scss index c13f5f0be0..4ae2d16406 100644 --- a/stylesheets/components/CallsTab.scss +++ b/stylesheets/components/CallsTab.scss @@ -376,7 +376,6 @@ } .CallsNewCall__ItemActionButton--join-call-disabled { - cursor: default; opacity: 0.5; } diff --git a/ts/components/CallsList.tsx b/ts/components/CallsList.tsx index 032603e6b5..f8dab56333 100644 --- a/ts/components/CallsList.tsx +++ b/ts/components/CallsList.tsx @@ -65,7 +65,7 @@ import type { PeekNotConnectedGroupCallType, } from '../state/ducks/calling'; import { DAY, MINUTE, SECOND } from '../util/durations'; -import { ConfirmationDialog } from './ConfirmationDialog'; +import type { StartCallData } from './ConfirmLeaveCallModal'; function Timestamp({ i18n, @@ -148,7 +148,8 @@ type CallsListProps = Readonly<{ onOutgoingVideoCallInConversation: (conversationId: string) => void; onChangeCallsTabSelectedView: (selectedView: CallsTabSelectedView) => void; peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void; - startCallLinkLobbyByRoomId: (roomId: string) => void; + startCallLinkLobbyByRoomId: (options: { roomId: string }) => void; + toggleConfirmLeaveCallModal: (options: StartCallData | null) => void; togglePip: () => void; }>; @@ -179,7 +180,6 @@ export function CallsList({ getCall, getCallLink, getConversation, - hangUpActiveCall, i18n, selectedCallHistoryGroup, onCreateCallLink, @@ -188,6 +188,7 @@ export function CallsList({ onChangeCallsTabSelectedView, peekNotConnectedGroupCall, startCallLinkLobbyByRoomId, + toggleConfirmLeaveCallModal, togglePip, }: CallsListProps): JSX.Element { const infiniteLoaderRef = useRef(null); @@ -195,8 +196,6 @@ export function CallsList({ const [queryInput, setQueryInput] = useState(''); const [statusInput, setStatusInput] = useState(CallHistoryFilterStatus.All); const [searchState, setSearchState] = useState(defaultInitState); - const [isLeaveCallDialogVisible, setIsLeaveCallDialogVisible] = - useState(false); const prevOptionsRef = useRef(null); @@ -330,7 +329,7 @@ export function CallsList({ return true; } - // Direct is not supported currently + // We can't tell from CallHistory alone whether a 1:1 call is active return false; }, [getCallByPeerId] @@ -358,12 +357,14 @@ export function CallsList({ return peerId === activeCallConversationId; } - // Not supported currently + // For direct conversations, we know the call is active if it's the active call! if (mode === CallMode.Direct) { - return false; + return Boolean( + conversation && conversation?.id === activeCallConversationId + ); } - // Group + // For group and adhoc calls, a call has to have members in it (see getIsCallActive) return Boolean( isActive && conversation && @@ -773,6 +774,43 @@ export function CallsList({ strictAssert(false, 'Cannot format call'); } + const inCallAndNotThisOne = !isInCall && activeCall; + const callButton = ( + { + if (isInCall) { + togglePip(); + } else if (activeCall) { + if (isAdhoc) { + toggleConfirmLeaveCallModal({ + type: 'adhoc-roomId', + roomId: item.peerId, + }); + } else { + toggleConfirmLeaveCallModal({ + type: 'conversation', + conversationId: conversation.id, + isVideoCall: item.type !== CallType.Audio, + }); + } + } else if (isAdhoc) { + startCallLinkLobbyByRoomId({ roomId: item.peerId }); + } else if (conversation) { + if (item.type === CallType.Audio) { + onOutgoingAudioCallInConversation(conversation.id); + } else { + onOutgoingVideoCallInConversation(conversation.id); + } + } + }} + i18n={i18n} + /> + ); + return (
} - trailing={ - isCallButtonVisible ? ( - { - if (isInCall) { - togglePip(); - } else if (activeCall) { - if (isActiveVisible) { - setIsLeaveCallDialogVisible(true); - } - } else if (isAdhoc) { - startCallLinkLobbyByRoomId(item.peerId); - } else if (conversation) { - if (item.type === CallType.Audio) { - onOutgoingAudioCallInConversation(conversation.id); - } else { - onOutgoingVideoCallInConversation(conversation.id); - } - } - }} - i18n={i18n} - /> - ) : undefined - } + trailing={isCallButtonVisible ? callButton : undefined} title={ - {isLeaveCallDialogVisible && ( - { - setIsLeaveCallDialogVisible(false); - }} - title={i18n('icu:CallsList__LeaveCallDialogTitle')} - actions={[ - { - text: i18n('icu:CallsList__LeaveCallDialogButton--leave'), - style: 'affirmative', - action: () => { - hangUpActiveCall( - 'Calls Tab leave active call to join different call' - ); - }, - }, - ]} - > - {i18n('icu:CallsList__LeaveCallDialogBody')} - - )} - - ); - } else if (isActive) { + if (!isEnabled) { + tooltipContent = i18n('icu:ContactModal--already-in-call'); + } + // Note: isActive is only set for groups and adhoc calls + if (isActive) { innerContent = isInCall ? i18n('icu:CallsNewCallButton--return') : i18n('icu:joinOngoingCall'); - if (!isEnabled) { - tooltipContent = i18n('icu:CallsNewCallButtonTooltip--in-another-call'); - } + } else if (callType === CallType.Audio) { + innerContent = ( + + ); } else { innerContent = ( @@ -97,6 +99,7 @@ export function CallsNewCallButton({ className="CallsNewCall__ItemActionButtonTooltip" content={tooltipContent} direction={TooltipPlacement.Top} + popperModifiers={[offsetDistanceModifier(15)]} > {buttonContent} diff --git a/ts/components/CallsTab.tsx b/ts/components/CallsTab.tsx index b3563c50f7..c1cf2a8d1c 100644 --- a/ts/components/CallsTab.tsx +++ b/ts/components/CallsTab.tsx @@ -23,6 +23,7 @@ import type { UnreadStats } from '../util/countUnreadStats'; import type { WidthBreakpoint } from './_util'; import type { CallLinkType } from '../types/CallLink'; import type { CallStateType } from '../state/selectors/calling'; +import type { StartCallData } from './ConfirmLeaveCallModal'; enum CallsTabSidebarView { CallsListView, @@ -72,7 +73,8 @@ type CallsTabProps = Readonly<{ }) => JSX.Element; regionCode: string | undefined; savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void; - startCallLinkLobbyByRoomId: (roomId: string) => void; + startCallLinkLobbyByRoomId: (options: { roomId: string }) => void; + toggleConfirmLeaveCallModal: (options: StartCallData | null) => void; togglePip: () => void; }>; @@ -119,6 +121,7 @@ export function CallsTab({ regionCode, savePreferredLeftPaneWidth, startCallLinkLobbyByRoomId, + toggleConfirmLeaveCallModal, togglePip, }: CallsTabProps): JSX.Element { const [sidebarView, setSidebarView] = useState( @@ -282,6 +285,7 @@ export function CallsTab({ } peekNotConnectedGroupCall={peekNotConnectedGroupCall} startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId} + toggleConfirmLeaveCallModal={toggleConfirmLeaveCallModal} togglePip={togglePip} /> )} diff --git a/ts/components/ConfirmLeaveCallModal.tsx b/ts/components/ConfirmLeaveCallModal.tsx new file mode 100644 index 0000000000..298fd445e5 --- /dev/null +++ b/ts/components/ConfirmLeaveCallModal.tsx @@ -0,0 +1,59 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; + +import { ConfirmationDialog } from './ConfirmationDialog'; + +import type { LocalizerType } from '../types/Util'; +import type { + StartCallingLobbyType, + StartCallLinkLobbyByRoomIdType, + StartCallLinkLobbyType, +} from '../state/ducks/calling'; + +export type StartCallData = + | ({ + type: 'conversation'; + } & StartCallingLobbyType) + | ({ type: 'adhoc-roomId' } & StartCallLinkLobbyByRoomIdType) + | ({ type: 'adhoc-rootKey' } & StartCallLinkLobbyType); +type HousekeepingProps = { + i18n: LocalizerType; +}; +type DispatchProps = { + toggleConfirmLeaveCallModal: (options: StartCallData | null) => void; + leaveCurrentCallAndStartCallingLobby: (options: StartCallData) => void; +}; + +export type Props = { data: StartCallData } & HousekeepingProps & DispatchProps; + +export function ConfirmLeaveCallModal({ + i18n, + data, + leaveCurrentCallAndStartCallingLobby, + toggleConfirmLeaveCallModal, +}: Props): JSX.Element | null { + return ( + { + toggleConfirmLeaveCallModal(null); + }} + title={i18n('icu:CallsList__LeaveCallDialogTitle')} + actions={[ + { + text: i18n('icu:CallsList__LeaveCallDialogButton--leave'), + style: 'affirmative', + action: () => { + leaveCurrentCallAndStartCallingLobby(data); + }, + }, + ]} + > + {i18n('icu:CallsList__LeaveCallDialogBody')} + + ); +} diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index fc8a0f1344..79dbab5474 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -20,6 +20,7 @@ import { ButtonVariant } from './Button'; import { ConfirmationDialog } from './ConfirmationDialog'; import { SignalConnectionsModal } from './SignalConnectionsModal'; import { WhatsNewModal } from './WhatsNewModal'; +import type { StartCallData } from './ConfirmLeaveCallModal'; // NOTE: All types should be required for this component so that the smart // component gives you type errors when adding/removing props. @@ -35,6 +36,9 @@ export type PropsType = { // CallLinkEditModal callLinkEditModalRoomId: string | null; renderCallLinkEditModal: () => JSX.Element; + // ConfirmLeaveCallModal + confirmLeaveCallModalState: StartCallData | null; + renderConfirmLeaveCallModal: () => JSX.Element; // ContactModal contactModalState: ContactModalStateType | undefined; renderContactModal: () => JSX.Element; @@ -114,6 +118,9 @@ export function GlobalModalContainer({ // CallLinkEditModal callLinkEditModalRoomId, renderCallLinkEditModal, + // ConfirmLeaveCallModal + confirmLeaveCallModalState, + renderConfirmLeaveCallModal, // ContactModal contactModalState, renderContactModal, @@ -196,6 +203,10 @@ export function GlobalModalContainer({ // The Rest + if (confirmLeaveCallModalState) { + return renderConfirmLeaveCallModal(); + } + if (addUserToAnotherGroupModalContactId) { return renderAddUserToAnotherGroup(); } diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index 28fa68c160..fbb2e5a945 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -424,14 +424,14 @@ export function ConversationDetails({ {!conversation.isMe && ( <> onOutgoingVideoCallInConversation(conversation.id)} type="video" /> {!isGroup && ( onOutgoingAudioCallInConversation(conversation.id) @@ -733,19 +733,18 @@ export function ConversationDetails({ } function ConversationDetailsCallButton({ - disabled, + hasActiveCall, i18n, onClick, type, }: Readonly<{ - disabled: boolean; + hasActiveCall: boolean; i18n: LocalizerType; onClick: () => unknown; type: 'audio' | 'video'; }>) { const button = (