signal-desktop/ts/components/CallsTab.tsx
2024-05-22 09:24:27 -07:00

346 lines
12 KiB
TypeScript

// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useState } from 'react';
import type { LocalizerType } from '../types/I18N';
import { NavSidebar, NavSidebarActionButton } from './NavSidebar';
import { CallsList } from './CallsList';
import type { ConversationType } from '../state/ducks/conversations';
import type {
CallHistoryFilterOptions,
CallHistoryGroup,
CallHistoryPagination,
} from '../types/CallDisposition';
import { CallsNewCall } from './CallsNewCall';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import type {
ActiveCallStateType,
PeekNotConnectedGroupCallType,
} from '../state/ducks/calling';
import { ContextMenu } from './ContextMenu';
import { ConfirmationDialog } from './ConfirmationDialog';
import type { UnreadStats } from '../util/countUnreadStats';
import type { WidthBreakpoint } from './_util';
import type { CallLinkType } from '../types/CallLink';
import type { CallStateType } from '../state/selectors/calling';
enum CallsTabSidebarView {
CallsListView,
NewCallView,
}
type CallsTabProps = Readonly<{
activeCall: ActiveCallStateType | undefined;
allConversations: ReadonlyArray<ConversationType>;
otherTabsUnreadStats: UnreadStats;
getCallHistoryGroupsCount: (
options: CallHistoryFilterOptions
) => Promise<number>;
getCallHistoryGroups: (
options: CallHistoryFilterOptions,
pagination: CallHistoryPagination
) => Promise<Array<CallHistoryGroup>>;
callHistoryEdition: number;
getAdhocCall: (roomId: string) => CallStateType | undefined;
getCall: (id: string) => CallStateType | undefined;
getCallLink: (id: string) => CallLinkType | undefined;
getConversation: (id: string) => ConversationType | void;
hangUpActiveCall: (reason: string) => void;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
i18n: LocalizerType;
navTabsCollapsed: boolean;
onClearCallHistory: () => void;
onMarkCallHistoryRead: (conversationId: string, callId: string) => void;
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
preferredLeftPaneWidth: number;
renderCallLinkDetails: (
roomId: string,
callHistoryGroup: CallHistoryGroup
) => JSX.Element;
renderConversationDetails: (
conversationId: string,
callHistoryGroup: CallHistoryGroup | null
) => JSX.Element;
renderToastManager: (_: {
containerWidthBreakpoint: WidthBreakpoint;
}) => JSX.Element;
regionCode: string | undefined;
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
startCallLinkLobbyByRoomId: (roomId: string) => void;
togglePip: () => void;
}>;
export type CallsTabSelectedView =
| {
type: 'conversation';
conversationId: string;
callHistoryGroup: CallHistoryGroup | null;
}
| {
type: 'callLink';
roomId: string;
callHistoryGroup: CallHistoryGroup;
};
export function CallsTab({
activeCall,
allConversations,
otherTabsUnreadStats,
getCallHistoryGroupsCount,
getCallHistoryGroups,
callHistoryEdition,
getAdhocCall,
getCall,
getCallLink,
getConversation,
hangUpActiveCall,
hasFailedStorySends,
hasPendingUpdate,
i18n,
navTabsCollapsed,
onClearCallHistory,
onMarkCallHistoryRead,
onToggleNavTabsCollapse,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
peekNotConnectedGroupCall,
preferredLeftPaneWidth,
renderCallLinkDetails,
renderConversationDetails,
renderToastManager,
regionCode,
savePreferredLeftPaneWidth,
startCallLinkLobbyByRoomId,
togglePip,
}: CallsTabProps): JSX.Element {
const [sidebarView, setSidebarView] = useState(
CallsTabSidebarView.CallsListView
);
const [selectedView, setSelectedViewInner] =
useState<CallsTabSelectedView | null>(null);
const [selectedViewKey, setSelectedViewKey] = useState(() => 1);
const [
confirmClearCallHistoryDialogOpen,
setConfirmClearCallHistoryDialogOpen,
] = useState(false);
const updateSelectedView = useCallback(
(nextSelected: CallsTabSelectedView | null) => {
setSelectedViewInner(nextSelected);
setSelectedViewKey(key => key + 1);
},
[]
);
const updateSidebarView = useCallback(
(newSidebarView: CallsTabSidebarView) => {
setSidebarView(newSidebarView);
updateSelectedView(null);
},
[updateSelectedView]
);
useEscapeHandling(
sidebarView === CallsTabSidebarView.NewCallView
? () => {
updateSidebarView(CallsTabSidebarView.CallsListView);
}
: undefined
);
const handleOpenClearCallHistoryDialog = useCallback(() => {
setConfirmClearCallHistoryDialogOpen(true);
}, []);
const handleCloseClearCallHistoryDialog = useCallback(() => {
setConfirmClearCallHistoryDialogOpen(false);
}, []);
const handleOutgoingAudioCallInConversation = useCallback(
(conversationId: string) => {
onOutgoingAudioCallInConversation(conversationId);
updateSidebarView(CallsTabSidebarView.CallsListView);
},
[updateSidebarView, onOutgoingAudioCallInConversation]
);
const handleOutgoingVideoCallInConversation = useCallback(
(conversationId: string) => {
onOutgoingVideoCallInConversation(conversationId);
updateSidebarView(CallsTabSidebarView.CallsListView);
},
[updateSidebarView, onOutgoingVideoCallInConversation]
);
useEffect(() => {
if (selectedView?.type === 'conversation') {
selectedView.callHistoryGroup?.children.forEach(child => {
onMarkCallHistoryRead(selectedView.conversationId, child.callId);
});
}
}, [selectedView, onMarkCallHistoryRead]);
return (
<>
<div className="CallsTab">
<NavSidebar
i18n={i18n}
title={
sidebarView === CallsTabSidebarView.CallsListView
? i18n('icu:CallsTab__HeaderTitle--CallsList')
: i18n('icu:CallsTab__HeaderTitle--NewCall')
}
otherTabsUnreadStats={otherTabsUnreadStats}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
navTabsCollapsed={navTabsCollapsed}
onBack={
sidebarView === CallsTabSidebarView.NewCallView
? () => {
updateSidebarView(CallsTabSidebarView.CallsListView);
}
: null
}
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
requiresFullWidth
preferredLeftPaneWidth={preferredLeftPaneWidth}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
renderToastManager={renderToastManager}
actions={
<>
{sidebarView === CallsTabSidebarView.CallsListView && (
<>
<NavSidebarActionButton
icon={<span className="CallsTab__NewCallActionIcon" />}
label={i18n('icu:CallsTab__NewCallActionLabel')}
onClick={() => {
updateSidebarView(CallsTabSidebarView.NewCallView);
}}
/>
<ContextMenu
i18n={i18n}
menuOptions={[
{
icon: 'CallsTab__ClearCallHistoryIcon',
label: i18n('icu:CallsTab__ClearCallHistoryLabel'),
onClick: handleOpenClearCallHistoryDialog,
},
]}
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
portalToRoot
>
{({ openMenu, onKeyDown }) => {
return (
<NavSidebarActionButton
onClick={openMenu}
onKeyDown={onKeyDown}
icon={<span className="CallsTab__MoreActionsIcon" />}
label={i18n('icu:CallsTab__MoreActionsLabel')}
/>
);
}}
</ContextMenu>
</>
)}
</>
}
>
{sidebarView === CallsTabSidebarView.CallsListView && (
<CallsList
key={CallsTabSidebarView.CallsListView}
activeCall={activeCall}
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
getCallHistoryGroups={getCallHistoryGroups}
callHistoryEdition={callHistoryEdition}
getAdhocCall={getAdhocCall}
getCall={getCall}
getCallLink={getCallLink}
getConversation={getConversation}
hangUpActiveCall={hangUpActiveCall}
i18n={i18n}
selectedCallHistoryGroup={selectedView?.callHistoryGroup ?? null}
onChangeCallsTabSelectedView={updateSelectedView}
onOutgoingAudioCallInConversation={
handleOutgoingAudioCallInConversation
}
onOutgoingVideoCallInConversation={
handleOutgoingVideoCallInConversation
}
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
togglePip={togglePip}
/>
)}
{sidebarView === CallsTabSidebarView.NewCallView && (
<CallsNewCall
key={CallsTabSidebarView.NewCallView}
hasActiveCall={activeCall != null}
allConversations={allConversations}
i18n={i18n}
regionCode={regionCode}
onChangeCallsTabSelectedView={updateSelectedView}
onOutgoingAudioCallInConversation={
handleOutgoingAudioCallInConversation
}
onOutgoingVideoCallInConversation={
handleOutgoingVideoCallInConversation
}
/>
)}
</NavSidebar>
{selectedView == null ? (
<div className="CallsTab__EmptyState">
<div className="CallsTab__EmptyStateIcon" />
<p className="CallsTab__EmptyStateLabel">
{i18n('icu:CallsTab__EmptyStateText')}
</p>
</div>
) : (
<div
className="CallsTab__ConversationCallDetails"
// Force scrolling to top when selection changes
key={selectedViewKey}
>
{selectedView.type === 'conversation' &&
renderConversationDetails(
selectedView.conversationId,
selectedView.callHistoryGroup
)}
{selectedView.type === 'callLink' &&
renderCallLinkDetails(
selectedView.roomId,
selectedView.callHistoryGroup
)}
</div>
)}
</div>
{confirmClearCallHistoryDialogOpen && (
<ConfirmationDialog
dialogName="CallsTab__ConfirmClearCallHistory"
i18n={i18n}
onClose={handleCloseClearCallHistoryDialog}
title={i18n('icu:CallsTab__ConfirmClearCallHistory__Title')}
actions={[
{
style: 'negative',
text: i18n(
'icu:CallsTab__ConfirmClearCallHistory__ConfirmButton'
),
action: onClearCallHistory,
},
]}
>
{i18n('icu:CallsTab__ConfirmClearCallHistory__Body')}
</ConfirmationDialog>
)}
</>
);
}