Calls Tab & Group Call Disposition

This commit is contained in:
Jamie Kyle 2023-08-08 17:53:06 -07:00 committed by GitHub
parent 620e85ca01
commit 1eaabb6734
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
139 changed files with 9182 additions and 2721 deletions

View file

@ -1,9 +1,9 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useCallback, useMemo, useState } from 'react';
import React, { useEffect, useCallback, useMemo } from 'react';
import classNames from 'classnames';
import { clamp, isNumber, noop } from 'lodash';
import { isNumber } from 'lodash';
import type { LeftPaneHelper, ToFindType } from './leftPane/LeftPaneHelper';
import { FindDirection } from './leftPane/LeftPaneHelper';
@ -27,15 +27,8 @@ import { usePrevious } from '../hooks/usePrevious';
import { missingCaseError } from '../util/missingCaseError';
import type { DurationInSeconds } from '../util/durations';
import type { WidthBreakpoint } from './_util';
import { getConversationListWidthBreakpoint } from './_util';
import { getNavSidebarWidthBreakpoint } from './_util';
import * as KeyboardLayout from '../services/keyboardLayout';
import {
MIN_WIDTH,
SNAP_WIDTH,
MIN_FULL_WIDTH,
MAX_WIDTH,
getWidthFromPreferredWidth,
} from '../util/leftPaneWidth';
import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid';
import type { ShowConversationType } from '../state/ducks/conversations';
import type { PropsType as UnsupportedOSDialogPropsType } from '../state/smart/UnsupportedOSDialog';
@ -50,6 +43,12 @@ import type {
SaveAvatarToDiskActionType,
} from '../types/Avatar';
import { SizeObserver } from '../hooks/useSizeObserver';
import {
NavSidebar,
NavSidebarActionButton,
NavSidebarSearchHeader,
} from './NavSidebar';
import { ContextMenu } from './ContextMenu';
export enum LeftPaneMode {
Inbox,
@ -114,6 +113,7 @@ export type PropsType = {
composeReplaceAvatar: ReplaceAvatarActionType;
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
createGroup: () => void;
navTabsCollapsed: boolean;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
removeConversation: (conversationId: string) => void;
@ -132,10 +132,10 @@ export type PropsType = {
startSettingGroupMetadata: () => void;
toggleComposeEditingAvatar: () => unknown;
toggleConversationInChooseMembers: (conversationId: string) => void;
toggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
updateSearchTerm: (_: string) => void;
// Render Props
renderMainHeader: () => JSX.Element;
renderMessageSearchResult: (id: string) => JSX.Element;
renderNetworkStatus: (
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
@ -178,14 +178,15 @@ export function LeftPane({
isUpdateDownloaded,
isContactManagementEnabled,
modeSpecificProps,
navTabsCollapsed,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
preferredWidthFromStorage,
removeConversation,
renderCaptchaDialog,
renderCrashReportDialog,
renderExpiredBuildDialog,
renderMainHeader,
renderMessageSearchResult,
renderNetworkStatus,
renderUnsupportedOSDialog,
@ -195,6 +196,7 @@ export function LeftPane({
searchInConversation,
selectedConversationId,
targetedMessageId,
toggleNavTabsCollapse,
setChallengeStatus,
setComposeGroupAvatar,
setComposeGroupExpireTimer,
@ -215,12 +217,6 @@ export function LeftPane({
unsupportedOSDialogType,
updateSearchTerm,
}: PropsType): JSX.Element {
const [preferredWidth, setPreferredWidth] = useState(
// This clamp is present just in case we get a bogus value from storage.
clamp(preferredWidthFromStorage, MIN_WIDTH, MAX_WIDTH)
);
const [isResizing, setIsResizing] = useState(false);
const previousModeSpecificProps = usePrevious(
modeSpecificProps,
modeSpecificProps
@ -421,76 +417,6 @@ export function LeftPane({
startSearch,
]);
const requiresFullWidth = helper.requiresFullWidth();
useEffect(() => {
if (!isResizing) {
return noop;
}
const onMouseMove = (event: MouseEvent) => {
let width: number;
const isRTL = i18n.getLocaleDirection() === 'rtl';
const x = isRTL ? window.innerWidth - event.clientX : event.clientX;
if (requiresFullWidth) {
width = Math.max(x, MIN_FULL_WIDTH);
} else if (x < SNAP_WIDTH) {
width = MIN_WIDTH;
} else {
width = clamp(x, MIN_FULL_WIDTH, MAX_WIDTH);
}
setPreferredWidth(Math.min(width, MAX_WIDTH));
event.preventDefault();
};
const stopResizing = () => {
setIsResizing(false);
};
document.body.addEventListener('mousemove', onMouseMove);
document.body.addEventListener('mouseup', stopResizing);
document.body.addEventListener('mouseleave', stopResizing);
return () => {
document.body.removeEventListener('mousemove', onMouseMove);
document.body.removeEventListener('mouseup', stopResizing);
document.body.removeEventListener('mouseleave', stopResizing);
};
}, [i18n, isResizing, requiresFullWidth]);
useEffect(() => {
if (!isResizing) {
return noop;
}
document.body.classList.add('is-resizing-left-pane');
return () => {
document.body.classList.remove('is-resizing-left-pane');
};
}, [isResizing]);
useEffect(() => {
if (isResizing || preferredWidth === preferredWidthFromStorage) {
return;
}
const timeout = setTimeout(() => {
savePreferredLeftPaneWidth(preferredWidth);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [
isResizing,
preferredWidth,
preferredWidthFromStorage,
savePreferredLeftPaneWidth,
]);
const preRowsNode = helper.getPreRowsNode({
clearConversationSearch,
clearGroupCreationError,
@ -553,11 +479,7 @@ export function LeftPane({
// It also ensures that we scroll to the top when switching views.
const listKey = preRowsNode ? 1 : 0;
const width = getWidthFromPreferredWidth(preferredWidth, {
requiresFullWidth,
});
const widthBreakpoint = getConversationListWidthBreakpoint(width);
const widthBreakpoint = getNavSidebarWidthBreakpoint(300);
const commonDialogProps = {
i18n,
@ -614,127 +536,171 @@ export function LeftPane({
}
return (
<nav
className={classNames(
'module-left-pane',
isResizing && 'module-left-pane--is-resizing',
`module-left-pane--width-${widthBreakpoint}`,
modeSpecificProps.mode === LeftPaneMode.ChooseGroupMembers &&
'module-left-pane--mode-choose-group-members',
modeSpecificProps.mode === LeftPaneMode.Compose &&
'module-left-pane--mode-compose'
)}
style={{ width }}
>
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
<div className="module-left-pane__header">
{helper.getHeaderContents({
i18n,
showInbox,
startComposing,
showChooseGroupMembers,
}) || renderMainHeader()}
</div>
{helper.getSearchInput({
clearConversationSearch,
clearSearch,
i18n,
onChangeComposeSearchTerm: event => {
setComposeSearchTerm(event.target.value);
},
updateSearchTerm,
showConversation,
})}
<div className="module-left-pane__dialogs">
{dialogs.map(({ key, dialog }) => (
<React.Fragment key={key}>{dialog}</React.Fragment>
))}
</div>
{preRowsNode && <React.Fragment key={0}>{preRowsNode}</React.Fragment>}
<SizeObserver>
{(ref, size) => (
<div className="module-left-pane__list--measure" ref={ref}>
<div className="module-left-pane__list--wrapper">
<div
aria-live="polite"
className="module-left-pane__list"
data-supertab
key={listKey}
role="presentation"
tabIndex={-1}
>
<ConversationList
dimensions={{
width,
height: size?.height || 0,
}}
getPreferredBadge={getPreferredBadge}
getRow={getRow}
i18n={i18n}
onClickArchiveButton={showArchivedConversations}
onClickContactCheckbox={(
conversationId: string,
disabledReason: undefined | ContactCheckboxDisabledReason
) => {
switch (disabledReason) {
case undefined:
toggleConversationInChooseMembers(conversationId);
break;
case ContactCheckboxDisabledReason.AlreadyAdded:
case ContactCheckboxDisabledReason.MaximumContactsSelected:
// These are no-ops.
break;
default:
throw missingCaseError(disabledReason);
}
}}
showUserNotFoundModal={showUserNotFoundModal}
setIsFetchingUUID={setIsFetchingUUID}
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
showConversation={showConversation}
blockConversation={blockConversation}
onSelectConversation={onSelectConversation}
onOutgoingAudioCallInConversation={
onOutgoingAudioCallInConversation
}
onOutgoingVideoCallInConversation={
onOutgoingVideoCallInConversation
}
removeConversation={
isContactManagementEnabled ? removeConversation : undefined
}
renderMessageSearchResult={renderMessageSearchResult}
rowCount={helper.getRowCount()}
scrollBehavior={scrollBehavior}
scrollToRowIndex={rowIndexToScrollTo}
scrollable={isScrollable}
shouldRecomputeRowHeights={shouldRecomputeRowHeights}
showChooseGroupMembers={showChooseGroupMembers}
theme={theme}
<NavSidebar
title="Chats"
hideHeader={
modeSpecificProps.mode === LeftPaneMode.Archive ||
modeSpecificProps.mode === LeftPaneMode.Compose ||
modeSpecificProps.mode === LeftPaneMode.ChooseGroupMembers ||
modeSpecificProps.mode === LeftPaneMode.SetGroupMetadata
}
i18n={i18n}
navTabsCollapsed={navTabsCollapsed}
onToggleNavTabsCollapse={toggleNavTabsCollapse}
preferredLeftPaneWidth={preferredWidthFromStorage}
requiresFullWidth={false}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
actions={
<>
<NavSidebarActionButton
label={i18n('icu:newConversation')}
icon={<span className="module-left-pane__startComposingIcon" />}
onClick={startComposing}
/>
<ContextMenu
i18n={i18n}
menuOptions={[
{
label: i18n('icu:avatarMenuViewArchive'),
onClick: showArchivedConversations,
},
]}
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
portalToRoot
>
{({ openMenu, onKeyDown }) => {
return (
<NavSidebarActionButton
onClick={openMenu}
onKeyDown={onKeyDown}
icon={<span className="module-left-pane__moreActionsIcon" />}
label="More Actions"
/>
);
}}
</ContextMenu>
</>
}
>
<nav
className={classNames(
'module-left-pane',
modeSpecificProps.mode === LeftPaneMode.ChooseGroupMembers &&
'module-left-pane--mode-choose-group-members',
modeSpecificProps.mode === LeftPaneMode.Compose &&
'module-left-pane--mode-compose'
)}
>
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
<div className="module-left-pane__header">
{helper.getHeaderContents({
i18n,
showInbox,
startComposing,
showChooseGroupMembers,
})}
</div>
<NavSidebarSearchHeader>
{helper.getSearchInput({
clearConversationSearch,
clearSearch,
i18n,
onChangeComposeSearchTerm: event => {
setComposeSearchTerm(event.target.value);
},
updateSearchTerm,
showConversation,
})}
</NavSidebarSearchHeader>
<div className="module-left-pane__dialogs">
{dialogs.map(({ key, dialog }) => (
<React.Fragment key={key}>{dialog}</React.Fragment>
))}
</div>
{preRowsNode && <React.Fragment key={0}>{preRowsNode}</React.Fragment>}
<SizeObserver>
{(ref, size) => (
<div className="module-left-pane__list--measure" ref={ref}>
<div className="module-left-pane__list--wrapper">
<div
aria-live="polite"
className="module-left-pane__list"
data-supertab
key={listKey}
role="presentation"
tabIndex={-1}
>
<ConversationList
dimensions={size ?? undefined}
getPreferredBadge={getPreferredBadge}
getRow={getRow}
i18n={i18n}
onClickArchiveButton={showArchivedConversations}
onClickContactCheckbox={(
conversationId: string,
disabledReason: undefined | ContactCheckboxDisabledReason
) => {
switch (disabledReason) {
case undefined:
toggleConversationInChooseMembers(conversationId);
break;
case ContactCheckboxDisabledReason.AlreadyAdded:
case ContactCheckboxDisabledReason.MaximumContactsSelected:
// These are no-ops.
break;
default:
throw missingCaseError(disabledReason);
}
}}
showUserNotFoundModal={showUserNotFoundModal}
setIsFetchingUUID={setIsFetchingUUID}
lookupConversationWithoutUuid={
lookupConversationWithoutUuid
}
showConversation={showConversation}
blockConversation={blockConversation}
onSelectConversation={onSelectConversation}
onOutgoingAudioCallInConversation={
onOutgoingAudioCallInConversation
}
onOutgoingVideoCallInConversation={
onOutgoingVideoCallInConversation
}
removeConversation={
isContactManagementEnabled
? removeConversation
: undefined
}
renderMessageSearchResult={renderMessageSearchResult}
rowCount={helper.getRowCount()}
scrollBehavior={scrollBehavior}
scrollToRowIndex={rowIndexToScrollTo}
scrollable={isScrollable}
shouldRecomputeRowHeights={shouldRecomputeRowHeights}
showChooseGroupMembers={showChooseGroupMembers}
theme={theme}
/>
</div>
</div>
</div>
</div>
)}
</SizeObserver>
{footerContents && (
<div className="module-left-pane__footer">{footerContents}</div>
)}
</SizeObserver>
{footerContents && (
<div className="module-left-pane__footer">{footerContents}</div>
)}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div
className="module-left-pane__resize-grab-area"
onMouseDown={() => {
setIsResizing(true);
}}
/>
{challengeStatus !== 'idle' &&
renderCaptchaDialog({
onSkip() {
setChallengeStatus('idle');
},
})}
{crashReportCount > 0 && renderCrashReportDialog()}
</nav>
{challengeStatus !== 'idle' &&
renderCaptchaDialog({
onSkip() {
setChallengeStatus('idle');
},
})}
{crashReportCount > 0 && renderCrashReportDialog()}
</nav>
</NavSidebar>
);
}