signal-desktop/ts/components/CallsTab.tsx

347 lines
12 KiB
TypeScript
Raw Normal View History

2023-08-09 00:53:06 +00:00
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useState } from 'react';
2023-08-09 00:53:06 +00:00
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';
2023-08-09 00:53:06 +00:00
import { ContextMenu } from './ContextMenu';
import { ConfirmationDialog } from './ConfirmationDialog';
import type { UnreadStats } from '../util/countUnreadStats';
import type { WidthBreakpoint } from './_util';
2024-04-01 19:19:35 +00:00
import type { CallLinkType } from '../types/CallLink';
import type { CallStateType } from '../state/selectors/calling';
2023-08-09 00:53:06 +00:00
enum CallsTabSidebarView {
CallsListView,
NewCallView,
}
type CallsTabProps = Readonly<{
activeCall: ActiveCallStateType | undefined;
allConversations: ReadonlyArray<ConversationType>;
2023-08-21 20:12:27 +00:00
otherTabsUnreadStats: UnreadStats;
2023-08-09 00:53:06 +00:00
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;
2024-04-01 19:19:35 +00:00
getCallLink: (id: string) => CallLinkType | undefined;
2023-08-09 00:53:06 +00:00
getConversation: (id: string) => ConversationType | void;
hangUpActiveCall: (reason: string) => void;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
2023-08-09 00:53:06 +00:00
i18n: LocalizerType;
navTabsCollapsed: boolean;
onClearCallHistory: () => void;
onMarkCallHistoryRead: (conversationId: string, callId: string) => void;
2023-08-09 00:53:06 +00:00
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
2023-08-09 00:53:06 +00:00
preferredLeftPaneWidth: number;
2024-05-22 16:24:27 +00:00
renderCallLinkDetails: (
roomId: string,
callHistoryGroup: CallHistoryGroup
) => JSX.Element;
2023-08-09 00:53:06 +00:00
renderConversationDetails: (
conversationId: string,
callHistoryGroup: CallHistoryGroup | null
) => JSX.Element;
renderToastManager: (_: {
containerWidthBreakpoint: WidthBreakpoint;
}) => JSX.Element;
2023-08-09 00:53:06 +00:00
regionCode: string | undefined;
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
2024-04-01 19:19:35 +00:00
startCallLinkLobbyByRoomId: (roomId: string) => void;
togglePip: () => void;
2023-08-09 00:53:06 +00:00
}>;
2024-05-22 16:24:27 +00:00
export type CallsTabSelectedView =
| {
type: 'conversation';
conversationId: string;
callHistoryGroup: CallHistoryGroup | null;
}
| {
type: 'callLink';
roomId: string;
callHistoryGroup: CallHistoryGroup;
};
2023-08-09 00:53:06 +00:00
export function CallsTab({
activeCall,
allConversations,
2023-08-21 20:12:27 +00:00
otherTabsUnreadStats,
2023-08-09 00:53:06 +00:00
getCallHistoryGroupsCount,
getCallHistoryGroups,
callHistoryEdition,
getAdhocCall,
getCall,
2024-04-01 19:19:35 +00:00
getCallLink,
2023-08-09 00:53:06 +00:00
getConversation,
hangUpActiveCall,
hasFailedStorySends,
hasPendingUpdate,
2023-08-09 00:53:06 +00:00
i18n,
navTabsCollapsed,
onClearCallHistory,
onMarkCallHistoryRead,
2023-08-09 00:53:06 +00:00
onToggleNavTabsCollapse,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
peekNotConnectedGroupCall,
2023-08-09 00:53:06 +00:00
preferredLeftPaneWidth,
2024-05-22 16:24:27 +00:00
renderCallLinkDetails,
2023-08-09 00:53:06 +00:00
renderConversationDetails,
renderToastManager,
2023-08-09 00:53:06 +00:00
regionCode,
savePreferredLeftPaneWidth,
2024-04-01 19:19:35 +00:00
startCallLinkLobbyByRoomId,
togglePip,
2023-08-09 00:53:06 +00:00
}: CallsTabProps): JSX.Element {
const [sidebarView, setSidebarView] = useState(
CallsTabSidebarView.CallsListView
);
2024-05-22 16:24:27 +00:00
const [selectedView, setSelectedViewInner] =
useState<CallsTabSelectedView | null>(null);
const [selectedViewKey, setSelectedViewKey] = useState(() => 1);
2023-08-09 00:53:06 +00:00
const [
confirmClearCallHistoryDialogOpen,
setConfirmClearCallHistoryDialogOpen,
] = useState(false);
2024-05-22 16:24:27 +00:00
const updateSelectedView = useCallback(
(nextSelected: CallsTabSelectedView | null) => {
setSelectedViewInner(nextSelected);
setSelectedViewKey(key => key + 1);
2023-08-09 00:53:06 +00:00
},
[]
);
2024-05-22 16:24:27 +00:00
const updateSidebarView = useCallback(
(newSidebarView: CallsTabSidebarView) => {
setSidebarView(newSidebarView);
updateSelectedView(null);
2023-08-09 00:53:06 +00:00
},
2024-05-22 16:24:27 +00:00
[updateSelectedView]
2023-08-09 00:53:06 +00:00
);
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(() => {
2024-05-22 16:24:27 +00:00
if (selectedView?.type === 'conversation') {
selectedView.callHistoryGroup?.children.forEach(child => {
onMarkCallHistoryRead(selectedView.conversationId, child.callId);
});
}
2024-05-22 16:24:27 +00:00
}, [selectedView, onMarkCallHistoryRead]);
2023-08-09 00:53:06 +00:00
return (
<>
<div className="CallsTab">
<NavSidebar
i18n={i18n}
title={
sidebarView === CallsTabSidebarView.CallsListView
? i18n('icu:CallsTab__HeaderTitle--CallsList')
: i18n('icu:CallsTab__HeaderTitle--NewCall')
}
2023-08-21 20:12:27 +00:00
otherTabsUnreadStats={otherTabsUnreadStats}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
2023-08-09 00:53:06 +00:00
navTabsCollapsed={navTabsCollapsed}
onBack={
sidebarView === CallsTabSidebarView.NewCallView
? () => {
updateSidebarView(CallsTabSidebarView.CallsListView);
}
: null
}
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
requiresFullWidth
preferredLeftPaneWidth={preferredLeftPaneWidth}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
renderToastManager={renderToastManager}
2023-08-09 00:53:06 +00:00
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}
2023-08-09 00:53:06 +00:00
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
getCallHistoryGroups={getCallHistoryGroups}
callHistoryEdition={callHistoryEdition}
getAdhocCall={getAdhocCall}
getCall={getCall}
2024-04-01 19:19:35 +00:00
getCallLink={getCallLink}
2023-08-09 00:53:06 +00:00
getConversation={getConversation}
hangUpActiveCall={hangUpActiveCall}
2023-08-09 00:53:06 +00:00
i18n={i18n}
2024-05-22 16:24:27 +00:00
selectedCallHistoryGroup={selectedView?.callHistoryGroup ?? null}
onChangeCallsTabSelectedView={updateSelectedView}
onOutgoingAudioCallInConversation={
handleOutgoingAudioCallInConversation
}
onOutgoingVideoCallInConversation={
handleOutgoingVideoCallInConversation
}
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
2024-04-01 19:19:35 +00:00
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
togglePip={togglePip}
2023-08-09 00:53:06 +00:00
/>
)}
{sidebarView === CallsTabSidebarView.NewCallView && (
<CallsNewCall
key={CallsTabSidebarView.NewCallView}
hasActiveCall={activeCall != null}
2023-08-09 00:53:06 +00:00
allConversations={allConversations}
i18n={i18n}
regionCode={regionCode}
2024-05-22 16:24:27 +00:00
onChangeCallsTabSelectedView={updateSelectedView}
2023-08-09 00:53:06 +00:00
onOutgoingAudioCallInConversation={
handleOutgoingAudioCallInConversation
}
onOutgoingVideoCallInConversation={
handleOutgoingVideoCallInConversation
}
/>
)}
</NavSidebar>
2024-05-22 16:24:27 +00:00
{selectedView == null ? (
2023-08-09 00:53:06 +00:00
<div className="CallsTab__EmptyState">
<div className="CallsTab__EmptyStateIcon" />
<p className="CallsTab__EmptyStateLabel">
{i18n('icu:CallsTab__EmptyStateText')}
</p>
2023-08-09 00:53:06 +00:00
</div>
) : (
<div
className="CallsTab__ConversationCallDetails"
2024-05-22 16:24:27 +00:00
// Force scrolling to top when selection changes
key={selectedViewKey}
2023-08-09 00:53:06 +00:00
>
2024-05-22 16:24:27 +00:00
{selectedView.type === 'conversation' &&
renderConversationDetails(
selectedView.conversationId,
selectedView.callHistoryGroup
)}
{selectedView.type === 'callLink' &&
renderCallLinkDetails(
selectedView.roomId,
selectedView.callHistoryGroup
)}
2023-08-09 00:53:06 +00:00
</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>
)}
</>
);
}