signal-desktop/ts/components/IncomingCallBar.tsx

313 lines
8.2 KiB
TypeScript
Raw Normal View History

2021-05-07 22:21:10 +00:00
// Copyright 2020-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactChild } from 'react';
2022-05-10 18:14:08 +00:00
import React, { useCallback, useEffect, useRef } from 'react';
2022-12-09 20:37:45 +00:00
import { Avatar, AvatarSize } from './Avatar';
import { Tooltip } from './Tooltip';
2021-08-20 16:06:15 +00:00
import { Intl } from './Intl';
import { Theme } from '../util/theme';
2021-08-20 16:06:15 +00:00
import { getParticipantName } from '../util/callingGetParticipantName';
2020-06-04 18:16:19 +00:00
import { ContactName } from './conversation/ContactName';
2021-08-20 16:06:15 +00:00
import { Emojify } from './conversation/Emojify';
import type { LocalizerType } from '../types/Util';
2021-08-06 00:17:05 +00:00
import { AvatarColors } from '../types/Colors';
2021-08-20 16:06:15 +00:00
import { CallMode } from '../types/Calling';
import type { ConversationType } from '../state/ducks/conversations';
import type { AcceptCallType, DeclineCallType } from '../state/ducks/calling';
2021-08-20 16:06:15 +00:00
import { missingCaseError } from '../util/missingCaseError';
2022-05-10 18:14:08 +00:00
import {
useIncomingCallShortcuts,
useKeyboardShortcuts,
} from '../hooks/useKeyboardShortcuts';
2020-06-04 18:16:19 +00:00
export type PropsType = {
acceptCall: (_: AcceptCallType) => void;
declineCall: (_: DeclineCallType) => void;
i18n: LocalizerType;
2021-05-07 22:21:10 +00:00
conversation: Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'id'
| 'isMe'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
2021-08-20 16:06:15 +00:00
| 'type'
2021-05-07 22:21:10 +00:00
>;
2021-08-20 16:06:15 +00:00
bounceAppIconStart(): unknown;
bounceAppIconStop(): unknown;
notifyForCall(conversationTitle: string, isVideoCall: boolean): unknown;
} & (
| {
callMode: CallMode.Direct;
isVideoCall: boolean;
}
| {
callMode: CallMode.Group;
otherMembersRung: Array<Pick<ConversationType, 'firstName' | 'title'>>;
ringer: Pick<ConversationType, 'firstName' | 'title'>;
}
);
2020-06-04 18:16:19 +00:00
type CallButtonProps = {
classSuffix: string;
tabIndex: number;
tooltipContent: string;
onClick: () => void;
};
2022-11-18 00:45:19 +00:00
function CallButton({
2020-06-04 18:16:19 +00:00
classSuffix,
onClick,
tabIndex,
tooltipContent,
2022-11-18 00:45:19 +00:00
}: CallButtonProps): JSX.Element {
2020-06-04 18:16:19 +00:00
return (
<Tooltip content={tooltipContent} theme={Theme.Dark}>
2020-11-19 18:11:35 +00:00
<button
aria-label={tooltipContent}
2021-08-05 23:31:34 +00:00
className={`IncomingCallBar__button IncomingCallBar__button--${classSuffix}`}
2020-11-19 18:11:35 +00:00
onClick={onClick}
tabIndex={tabIndex}
type="button"
2020-06-04 18:16:19 +00:00
>
<div />
2020-11-19 18:11:35 +00:00
</button>
</Tooltip>
2020-06-04 18:16:19 +00:00
);
2022-11-18 00:45:19 +00:00
}
2020-06-04 18:16:19 +00:00
2022-11-18 00:45:19 +00:00
function GroupCallMessage({
2020-06-04 18:16:19 +00:00
i18n,
2021-08-20 16:06:15 +00:00
otherMembersRung,
ringer,
}: Readonly<{
i18n: LocalizerType;
otherMembersRung: Array<Pick<ConversationType, 'firstName' | 'title'>>;
ringer: Pick<ConversationType, 'firstName' | 'title'>;
2022-11-18 00:45:19 +00:00
}>): JSX.Element {
2021-08-20 16:06:15 +00:00
// As an optimization, we only process the first two names.
const [first, second] = otherMembersRung
.slice(0, 2)
.map(member => <Emojify text={getParticipantName(member)} />);
const ringerNode = <Emojify text={getParticipantName(ringer)} />;
switch (otherMembersRung.length) {
case 0:
return (
<Intl
id="incomingGroupCall__ringing-you"
i18n={i18n}
components={{ ringer: ringerNode }}
/>
);
case 1:
return (
<Intl
id="incomingGroupCall__ringing-1-other"
i18n={i18n}
components={{
ringer: ringerNode,
otherMember: first,
}}
/>
);
case 2:
return (
<Intl
id="incomingGroupCall__ringing-2-others"
i18n={i18n}
components={{
ringer: ringerNode,
first,
second,
}}
/>
);
case 3:
return (
<Intl
id="incomingGroupCall__ringing-3-others"
i18n={i18n}
components={{
ringer: ringerNode,
first,
second,
}}
/>
);
default:
return (
<Intl
id="incomingGroupCall__ringing-many"
i18n={i18n}
components={{
ringer: ringerNode,
first,
second,
remaining: String(otherMembersRung.length - 2),
}}
/>
);
}
2022-11-18 00:45:19 +00:00
}
2021-08-20 16:06:15 +00:00
2022-11-18 00:45:19 +00:00
export function IncomingCallBar(props: PropsType): JSX.Element | null {
2021-08-20 16:06:15 +00:00
const {
acceptCall,
bounceAppIconStart,
bounceAppIconStop,
conversation,
declineCall,
i18n,
notifyForCall,
} = props;
2020-06-04 18:16:19 +00:00
const {
id: conversationId,
2021-05-07 22:21:10 +00:00
acceptedMessageRequest,
2020-06-04 18:16:19 +00:00
avatarPath,
2020-07-24 01:35:32 +00:00
color,
2021-05-07 22:21:10 +00:00
isMe,
2020-06-04 18:16:19 +00:00
phoneNumber,
profileName,
2021-05-07 22:21:10 +00:00
sharedGroupNames,
title,
2021-08-20 16:06:15 +00:00
type: conversationType,
} = conversation;
2020-06-04 18:16:19 +00:00
2021-08-20 16:06:15 +00:00
let isVideoCall: boolean;
let headerNode: ReactChild;
let messageNode: ReactChild;
switch (props.callMode) {
case CallMode.Direct:
({ isVideoCall } = props);
headerNode = <ContactName title={title} />;
2021-08-20 16:06:15 +00:00
messageNode = i18n(
isVideoCall ? 'incomingVideoCall' : 'incomingAudioCall'
);
break;
case CallMode.Group: {
const { otherMembersRung, ringer } = props;
isVideoCall = true;
headerNode = <Emojify text={title} />;
messageNode = (
<GroupCallMessage
i18n={i18n}
otherMembersRung={otherMembersRung}
ringer={ringer}
/>
);
break;
}
default:
throw missingCaseError(props);
}
// We don't want to re-notify if the title changes.
const initialTitleRef = useRef<string>(title);
useEffect(() => {
const initialTitle = initialTitleRef.current;
notifyForCall(initialTitle, isVideoCall);
}, [isVideoCall, notifyForCall]);
useEffect(() => {
bounceAppIconStart();
return () => {
bounceAppIconStop();
};
}, [bounceAppIconStart, bounceAppIconStop]);
2022-05-10 18:14:08 +00:00
const acceptVideoCall = useCallback(() => {
acceptCall({ conversationId, asVideoCall: true });
}, [acceptCall, conversationId]);
const acceptAudioCall = useCallback(() => {
acceptCall({ conversationId, asVideoCall: false });
}, [acceptCall, conversationId]);
const declineIncomingCall = useCallback(() => {
declineCall({ conversationId });
}, [conversationId, declineCall]);
const incomingCallShortcuts = useIncomingCallShortcuts(
acceptAudioCall,
acceptVideoCall,
declineIncomingCall
);
useKeyboardShortcuts(incomingCallShortcuts);
2020-06-04 18:16:19 +00:00
return (
2021-08-05 23:31:34 +00:00
<div className="IncomingCallBar__container">
<div className="IncomingCallBar__bar">
2021-08-20 16:06:15 +00:00
<div className="IncomingCallBar__conversation">
<div className="IncomingCallBar__conversation--avatar">
2021-08-05 23:31:34 +00:00
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
badge={undefined}
2021-08-06 00:17:05 +00:00
color={color || AvatarColors[0]}
2021-08-05 23:31:34 +00:00
noteToSelf={false}
2021-08-20 16:06:15 +00:00
conversationType={conversationType}
2021-08-05 23:31:34 +00:00
i18n={i18n}
isMe={isMe}
2020-07-24 01:35:32 +00:00
phoneNumber={phoneNumber}
2020-06-04 18:16:19 +00:00
profileName={profileName}
2020-07-24 01:35:32 +00:00
title={title}
2021-08-05 23:31:34 +00:00
sharedGroupNames={sharedGroupNames}
2022-12-09 20:37:45 +00:00
size={AvatarSize.FORTY_EIGHT}
2020-06-04 18:16:19 +00:00
/>
</div>
2021-08-20 16:06:15 +00:00
<div className="IncomingCallBar__conversation--name">
<div className="IncomingCallBar__conversation--name-header">
{headerNode}
2021-08-05 23:31:34 +00:00
</div>
2021-08-20 16:06:15 +00:00
<div
dir="auto"
className="IncomingCallBar__conversation--message-text"
>
{messageNode}
2021-08-05 23:31:34 +00:00
</div>
2020-06-04 18:16:19 +00:00
</div>
</div>
2021-08-05 23:31:34 +00:00
<div className="IncomingCallBar__actions">
<CallButton
classSuffix="decline"
2022-05-10 18:14:08 +00:00
onClick={declineIncomingCall}
tabIndex={0}
tooltipContent={i18n('declineCall')}
/>
2021-08-05 23:31:34 +00:00
{isVideoCall ? (
<>
<CallButton
classSuffix="accept-video-as-audio"
2022-05-10 18:14:08 +00:00
onClick={acceptAudioCall}
2021-08-05 23:31:34 +00:00
tabIndex={0}
tooltipContent={i18n('acceptCallWithoutVideo')}
/>
<CallButton
classSuffix="accept-video"
2022-05-10 18:14:08 +00:00
onClick={acceptVideoCall}
2021-08-05 23:31:34 +00:00
tabIndex={0}
tooltipContent={i18n('acceptCall')}
/>
</>
) : (
<CallButton
classSuffix="accept-audio"
2022-05-10 18:14:08 +00:00
onClick={acceptAudioCall}
tabIndex={0}
tooltipContent={i18n('acceptCall')}
/>
2021-08-05 23:31:34 +00:00
)}
</div>
2020-06-04 18:16:19 +00:00
</div>
</div>
);
2022-11-18 00:45:19 +00:00
}