// 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 } from '../state/ducks/calling'; import { ContextMenu } from './ContextMenu'; import { ConfirmationDialog } from './ConfirmationDialog'; import type { UnreadStats } from '../util/countUnreadStats'; 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>>; getConversation: (id: string) => ConversationType | 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; preferredLeftPaneWidth: number; renderConversationDetails: ( conversationId: string, callHistoryGroup: CallHistoryGroup | null ) => JSX.Element; regionCode: string | undefined; savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void; }>; export function CallsTab({ activeCall, allConversations, otherTabsUnreadStats, getCallHistoryGroupsCount, getCallHistoryGroups, getConversation, hasFailedStorySends, hasPendingUpdate, i18n, navTabsCollapsed, onClearCallHistory, onMarkCallHistoryRead, onToggleNavTabsCollapse, onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, preferredLeftPaneWidth, renderConversationDetails, regionCode, savePreferredLeftPaneWidth, }: CallsTabProps): JSX.Element { const [sidebarView, setSidebarView] = useState( CallsTabSidebarView.CallsListView ); const [selected, setSelected] = useState<{ conversationId: string; callHistoryGroup: CallHistoryGroup | null; } | null>(null); const [ confirmClearCallHistoryDialogOpen, setConfirmClearCallHistoryDialogOpen, ] = useState(false); const updateSidebarView = useCallback( (newSidebarView: CallsTabSidebarView) => { setSidebarView(newSidebarView); setSelected(null); }, [] ); const handleSelectCallHistoryGroup = useCallback( (conversationId: string, callHistoryGroup: CallHistoryGroup) => { setSelected({ conversationId, callHistoryGroup, }); }, [] ); const handleSelectConversation = useCallback((conversationId: string) => { setSelected({ conversationId, callHistoryGroup: null }); }, []); 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 (selected?.callHistoryGroup != null) { selected.callHistoryGroup.children.forEach(child => { onMarkCallHistoryRead(selected.conversationId, child.callId); }); } }, [selected, 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} 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} hasActiveCall={activeCall != null} getCallHistoryGroupsCount={getCallHistoryGroupsCount} getCallHistoryGroups={getCallHistoryGroups} getConversation={getConversation} i18n={i18n} selectedCallHistoryGroup={selected?.callHistoryGroup ?? null} onSelectCallHistoryGroup={handleSelectCallHistoryGroup} onOutgoingAudioCallInConversation={ handleOutgoingAudioCallInConversation } onOutgoingVideoCallInConversation={ handleOutgoingVideoCallInConversation } /> )} {sidebarView === CallsTabSidebarView.NewCallView && ( <CallsNewCall key={CallsTabSidebarView.NewCallView} hasActiveCall={activeCall != null} allConversations={allConversations} i18n={i18n} regionCode={regionCode} onSelectConversation={handleSelectConversation} onOutgoingAudioCallInConversation={ handleOutgoingAudioCallInConversation } onOutgoingVideoCallInConversation={ handleOutgoingVideoCallInConversation } /> )} </NavSidebar> {selected == 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 a new conversation is selected. key={selected.conversationId} > {renderConversationDetails( selected.conversationId, selected.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> )} </> ); }