Properly style call buttons across app, when already in a call
This commit is contained in:
parent
3c25092f50
commit
c251867699
39 changed files with 610 additions and 189 deletions
|
@ -7587,11 +7587,15 @@ button.module-calling-participants-list__contact {
|
|||
@include font-body-1-bold();
|
||||
}
|
||||
.module-message__action--outgoing {
|
||||
color: $color-white;
|
||||
background-color: rgba($color-white, 0.22);
|
||||
&:hover {
|
||||
background-color: rgba($color-white, 0.36);
|
||||
}
|
||||
}
|
||||
.module-message__action--outgoing--in-another-call {
|
||||
color: rgba($color-white, 0.5);
|
||||
}
|
||||
.module-message__action--incoming {
|
||||
@include light-theme {
|
||||
color: $color-link;
|
||||
|
@ -7608,6 +7612,14 @@ button.module-calling-participants-list__contact {
|
|||
}
|
||||
}
|
||||
}
|
||||
.module-message__action--incoming--in-another-call {
|
||||
@include light-theme {
|
||||
color: rgba($color-link, 0.5);
|
||||
}
|
||||
@include dark-theme {
|
||||
color: rgba($color-white, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__link-preview__call-link-icon {
|
||||
display: flex;
|
||||
|
|
|
@ -71,6 +71,15 @@
|
|||
background: fade-out($background-color, 0.6);
|
||||
}
|
||||
|
||||
&--discouraged {
|
||||
@include light-theme {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@include dark-theme {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
@include hover-and-active-states($background-color, $color-black);
|
||||
}
|
||||
|
@ -96,10 +105,16 @@
|
|||
&--affirmative {
|
||||
color: $color-ultramarine;
|
||||
}
|
||||
&--affirmative--discouraged {
|
||||
color: fade-out($color-ultramarine, 0.5);
|
||||
}
|
||||
|
||||
&--destructive {
|
||||
color: $color-accent-red;
|
||||
}
|
||||
&--destructive--discouraged {
|
||||
color: fade-out($color-ultramarine, 0.5);
|
||||
}
|
||||
|
||||
@include hover-and-active-states($background-color, $color-black);
|
||||
}
|
||||
|
@ -119,10 +134,16 @@
|
|||
&--affirmative {
|
||||
color: $color-ultramarine-light;
|
||||
}
|
||||
&--affirmative--discouraged {
|
||||
color: fade-out($color-ultramarine-light, 0.5);
|
||||
}
|
||||
|
||||
&--destructive {
|
||||
color: $color-accent-red;
|
||||
}
|
||||
&--destructive--discouraged {
|
||||
color: fade-out($color-accent-red, 0.5);
|
||||
}
|
||||
|
||||
@include hover-and-active-states($background-color, $color-white);
|
||||
}
|
||||
|
@ -162,6 +183,15 @@
|
|||
background: fade-out($background-color, 0.6);
|
||||
}
|
||||
|
||||
&--discouraged {
|
||||
@include light-theme {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@include dark-theme {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
@include hover-and-active-states($background-color, $color-black);
|
||||
}
|
||||
|
@ -191,6 +221,10 @@
|
|||
color: fade-out($color, 0.4);
|
||||
background: fade-out($background-color, 0.6);
|
||||
}
|
||||
&--discouraged {
|
||||
color: fade-out($color, 0.5);
|
||||
}
|
||||
|
||||
@include hover-and-active-states($background-color, $color-black);
|
||||
}
|
||||
|
||||
|
@ -205,6 +239,10 @@
|
|||
color: fade-out($color, 0.4);
|
||||
background: fade-out($background-color, 0.6);
|
||||
}
|
||||
&--discouraged {
|
||||
color: fade-out($color, 0.5);
|
||||
}
|
||||
|
||||
@include hover-and-active-states($background-color, $color-white);
|
||||
}
|
||||
}
|
||||
|
@ -221,6 +259,15 @@
|
|||
min-width: 68px;
|
||||
padding: 8px;
|
||||
|
||||
&--discouraged {
|
||||
@include light-theme {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@include dark-theme {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
background-color: $color-gray-05;
|
||||
color: $color-black;
|
||||
|
|
|
@ -406,7 +406,12 @@
|
|||
}
|
||||
|
||||
.CallsNewCall__ItemActionButton--join-call-disabled {
|
||||
opacity: 0.5;
|
||||
@include light-theme {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@include dark-theme {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.CallsNewCall__ItemActionButtonTooltip {
|
||||
|
|
|
@ -309,8 +309,4 @@
|
|||
|
||||
margin-block: 8px 5px;
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
@include tooltip;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,21 +171,38 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&--in-another-call {
|
||||
@include light-theme {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@include dark-theme {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
@include light-theme {
|
||||
&:hover,
|
||||
&:focus {
|
||||
&:hover {
|
||||
background: $color-gray-02;
|
||||
}
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
background: $color-gray-02;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
}
|
||||
@include dark-theme {
|
||||
&:hover,
|
||||
&:focus {
|
||||
&:hover {
|
||||
background: $color-gray-80;
|
||||
}
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
background: $color-gray-02;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background: $color-gray-75;
|
||||
}
|
||||
|
@ -281,13 +298,13 @@
|
|||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
// Override hover state coming from __button above.
|
||||
&:hover {
|
||||
// Override hover/focus/active state coming from __button above.
|
||||
&:hover,
|
||||
&:active {
|
||||
@include any-theme {
|
||||
background-color: darken($background, 16%);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
background-color: darken($background, 16%);
|
||||
|
|
6
stylesheets/components/InAnotherCallTooltip.scss
Normal file
6
stylesheets/components/InAnotherCallTooltip.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.InAnotherCallTooltip {
|
||||
@include tooltip;
|
||||
}
|
|
@ -100,6 +100,7 @@
|
|||
@import './components/GroupDialog.scss';
|
||||
@import './components/GroupInput.scss';
|
||||
@import './components/HueSlider.scss';
|
||||
@import './components/InAnotherCallTooltip.scss';
|
||||
@import './components/Inbox.scss';
|
||||
@import './components/IncomingCallBar.scss';
|
||||
@import './components/Input.scss';
|
||||
|
|
|
@ -37,6 +37,16 @@ export function KitchenSink(): JSX.Element {
|
|||
{variant}
|
||||
</Button>
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
discouraged
|
||||
onClick={action('onClick')}
|
||||
size={size}
|
||||
variant={variant}
|
||||
>
|
||||
{variant}
|
||||
</Button>
|
||||
</p>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -43,6 +43,7 @@ export enum ButtonIconType {
|
|||
export type PropsType = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
discouraged?: boolean;
|
||||
icon?: ButtonIconType;
|
||||
size?: ButtonSize;
|
||||
style?: CSSProperties;
|
||||
|
@ -105,6 +106,7 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
|||
children,
|
||||
className,
|
||||
disabled = false,
|
||||
discouraged = false,
|
||||
icon,
|
||||
style,
|
||||
tabIndex,
|
||||
|
@ -143,8 +145,10 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
|||
'module-Button',
|
||||
sizeClassName,
|
||||
variantClassName,
|
||||
discouraged ? `${variantClassName}--discouraged` : undefined,
|
||||
icon && `module-Button--icon--${icon}`,
|
||||
className
|
||||
className,
|
||||
className && discouraged ? `${className}--discouraged` : undefined
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
|
|
|
@ -23,6 +23,7 @@ export default {
|
|||
i18n,
|
||||
callHistoryGroup: getFakeCallLinkHistoryGroup(),
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
hasActiveCall: false,
|
||||
onDeleteCallLink: action('onDeleteCallLink'),
|
||||
onOpenCallLinkAddNameModal: action('onOpenCallLinkAddNameModal'),
|
||||
onStartCallLinkLobby: action('onStartCallLinkLobby'),
|
||||
|
@ -38,3 +39,7 @@ export function Admin(args: CallLinkDetailsProps): JSX.Element {
|
|||
export function NonAdmin(args: CallLinkDetailsProps): JSX.Element {
|
||||
return <CallLinkDetails {...args} callLink={FAKE_CALL_LINK} />;
|
||||
}
|
||||
|
||||
export function InAnotherCall(args: CallLinkDetailsProps): JSX.Element {
|
||||
return <CallLinkDetails {...args} callLink={FAKE_CALL_LINK} hasActiveCall />;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import { getColorForCallLink } from '../util/getColorForCallLink';
|
|||
import { isCallLinkAdmin } from '../types/CallLink';
|
||||
import { CallLinkRestrictionsSelect } from './CallLinkRestrictionsSelect';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { InAnotherCallTooltip } from './conversation/InAnotherCallTooltip';
|
||||
|
||||
function toUrlWithoutProtocol(url: URL): string {
|
||||
return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
|
||||
|
@ -28,6 +29,7 @@ function toUrlWithoutProtocol(url: URL): string {
|
|||
export type CallLinkDetailsProps = Readonly<{
|
||||
callHistoryGroup: CallHistoryGroup;
|
||||
callLink: CallLinkType;
|
||||
hasActiveCall: boolean;
|
||||
i18n: LocalizerType;
|
||||
onDeleteCallLink: () => void;
|
||||
onOpenCallLinkAddNameModal: () => void;
|
||||
|
@ -40,6 +42,7 @@ export function CallLinkDetails({
|
|||
callHistoryGroup,
|
||||
callLink,
|
||||
i18n,
|
||||
hasActiveCall,
|
||||
onDeleteCallLink,
|
||||
onOpenCallLinkAddNameModal,
|
||||
onStartCallLinkLobby,
|
||||
|
@ -51,6 +54,18 @@ export function CallLinkDetails({
|
|||
const webUrl = linkCallRoute.toWebUrl({
|
||||
key: callLink.rootKey,
|
||||
});
|
||||
const joinButton = (
|
||||
<Button
|
||||
className="CallLinkDetails__HeaderButton"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
discouraged={hasActiveCall}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onStartCallLinkLobby}
|
||||
>
|
||||
{i18n('icu:CallLinkDetails__Join')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="CallLinkDetails__Container">
|
||||
<header className="CallLinkDetails__Header">
|
||||
|
@ -77,14 +92,13 @@ export function CallLinkDetails({
|
|||
</p>
|
||||
</div>
|
||||
<div className="CallLinkDetails__HeaderActions">
|
||||
<Button
|
||||
className="CallLinkDetails__HeaderButton"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onStartCallLinkLobby}
|
||||
>
|
||||
{i18n('icu:CallLinkDetails__Join')}
|
||||
</Button>
|
||||
{hasActiveCall ? (
|
||||
<InAnotherCallTooltip i18n={i18n}>
|
||||
{joinButton}
|
||||
</InAnotherCallTooltip>
|
||||
) : (
|
||||
joinButton
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<CallHistoryGroupPanelSection
|
||||
|
|
|
@ -18,6 +18,7 @@ export default {
|
|||
args: {
|
||||
i18n,
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
hasActiveCall: false,
|
||||
onClose: action('onClose'),
|
||||
onCopyCallLink: action('onCopyCallLink'),
|
||||
onOpenCallLinkAddNameModal: action('onOpenCallLinkAddNameModal'),
|
||||
|
@ -30,3 +31,7 @@ export default {
|
|||
export function Basic(args: CallLinkEditModalProps): JSX.Element {
|
||||
return <CallLinkEditModal {...args} />;
|
||||
}
|
||||
|
||||
export function InAnotherCall(args: CallLinkEditModalProps): JSX.Element {
|
||||
return <CallLinkEditModal {...args} hasActiveCall />;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Button, ButtonSize, ButtonVariant } from './Button';
|
|||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { getColorForCallLink } from '../util/getColorForCallLink';
|
||||
import { CallLinkRestrictionsSelect } from './CallLinkRestrictionsSelect';
|
||||
import { InAnotherCallTooltip } from './conversation/InAnotherCallTooltip';
|
||||
|
||||
const CallLinkEditModalRowIconClasses = {
|
||||
Edit: 'CallLinkEditModal__RowIcon--Edit',
|
||||
|
@ -67,6 +68,7 @@ function Hr() {
|
|||
export type CallLinkEditModalProps = {
|
||||
i18n: LocalizerType;
|
||||
callLink: CallLinkType;
|
||||
hasActiveCall: boolean;
|
||||
onClose: () => void;
|
||||
onCopyCallLink: () => void;
|
||||
onOpenCallLinkAddNameModal: () => void;
|
||||
|
@ -78,6 +80,7 @@ export type CallLinkEditModalProps = {
|
|||
export function CallLinkEditModal({
|
||||
i18n,
|
||||
callLink,
|
||||
hasActiveCall,
|
||||
onClose,
|
||||
onCopyCallLink,
|
||||
onOpenCallLinkAddNameModal,
|
||||
|
@ -91,6 +94,18 @@ export function CallLinkEditModal({
|
|||
return linkCallRoute.toWebUrl({ key: callLink.rootKey }).toString();
|
||||
}, [callLink.rootKey]);
|
||||
|
||||
const joinButton = (
|
||||
<Button
|
||||
onClick={onStartCallLinkLobby}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
discouraged={hasActiveCall}
|
||||
className="CallLinkEditModal__JoinButton"
|
||||
>
|
||||
{i18n('icu:CallLinkEditModal__JoinButtonLabel')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
|
@ -141,14 +156,13 @@ export function CallLinkEditModal({
|
|||
</button>
|
||||
</div>
|
||||
<div className="CallLinkEditModal__Header__Actions">
|
||||
<Button
|
||||
onClick={onStartCallLinkLobby}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
className="CallLinkEditModal__JoinButton"
|
||||
>
|
||||
{i18n('icu:CallLinkEditModal__JoinButtonLabel')}
|
||||
</Button>
|
||||
{hasActiveCall ? (
|
||||
<InAnotherCallTooltip i18n={i18n}>
|
||||
{joinButton}
|
||||
</InAnotherCallTooltip>
|
||||
) : (
|
||||
joinButton
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ import {
|
|||
formatCallHistoryGroup,
|
||||
getCallIdFromEra,
|
||||
} from '../util/callDisposition';
|
||||
import { CallsNewCallButton } from './CallsNewCall';
|
||||
import { CallsNewCallButton } from './CallsNewCallButton';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { Theme } from '../util/theme';
|
||||
import type { CallingConversationType } from '../types/Calling';
|
||||
|
|
|
@ -20,8 +20,10 @@ import { I18n } from './I18n';
|
|||
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||
import { CallType } from '../types/CallDisposition';
|
||||
import type { CallsTabSelectedView } from './CallsTab';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { offsetDistanceModifier } from '../util/popperUtil';
|
||||
import {
|
||||
InAnotherCallTooltip,
|
||||
getTooltipContent,
|
||||
} from './conversation/InAnotherCallTooltip';
|
||||
|
||||
type CallsNewCallProps = Readonly<{
|
||||
hasActiveCall: boolean;
|
||||
|
@ -53,9 +55,9 @@ export function CallsNewCallButton({
|
|||
onClick: () => void;
|
||||
}): JSX.Element {
|
||||
let innerContent: React.ReactNode | string;
|
||||
let tooltipContent = '';
|
||||
let inAnotherCallTooltipContent = '';
|
||||
if (!isEnabled) {
|
||||
tooltipContent = i18n('icu:ContactModal--already-in-call');
|
||||
inAnotherCallTooltipContent = getTooltipContent(i18n);
|
||||
}
|
||||
// Note: isActive is only set for groups and adhoc calls
|
||||
if (isActive) {
|
||||
|
@ -82,7 +84,7 @@ export function CallsNewCallButton({
|
|||
? undefined
|
||||
: 'CallsNewCall__ItemActionButton--join-call-disabled'
|
||||
)}
|
||||
aria-label={tooltipContent}
|
||||
aria-label={inAnotherCallTooltipContent}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
onClick();
|
||||
|
@ -92,17 +94,10 @@ export function CallsNewCallButton({
|
|||
</button>
|
||||
);
|
||||
|
||||
return tooltipContent === '' ? (
|
||||
return inAnotherCallTooltipContent === '' ? (
|
||||
buttonContent
|
||||
) : (
|
||||
<Tooltip
|
||||
className="CallsNewCall__ItemActionButtonTooltip"
|
||||
content={tooltipContent}
|
||||
direction={TooltipPlacement.Top}
|
||||
popperModifiers={[offsetDistanceModifier(15)]}
|
||||
>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
<InAnotherCallTooltip i18n={i18n}>{buttonContent}</InAnotherCallTooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
CallHistoryGroup,
|
||||
CallHistoryPagination,
|
||||
} from '../types/CallDisposition';
|
||||
import { CallsNewCall } from './CallsNewCall';
|
||||
import { CallsNewCall } from './CallsNewCallButton';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import type {
|
||||
ActiveCallStateType,
|
||||
|
|
|
@ -29,6 +29,7 @@ export default {
|
|||
} satisfies Meta<PropsType>;
|
||||
|
||||
const getCommonProps = (options: {
|
||||
activeConversationId?: string;
|
||||
mode: CallMode;
|
||||
type?: CallType;
|
||||
direction?: CallDirection;
|
||||
|
@ -81,7 +82,7 @@ const getCommonProps = (options: {
|
|||
status,
|
||||
},
|
||||
callCreator,
|
||||
activeConversationId: null,
|
||||
activeConversationId: options.activeConversationId ?? null,
|
||||
groupCallEnded,
|
||||
maxDevices,
|
||||
deviceCount,
|
||||
|
@ -118,6 +119,42 @@ export function AcceptedIncomingAudioCall(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function AcceptedIncomingAudioCallWithActiveCall(): JSX.Element {
|
||||
return (
|
||||
<CallingNotification
|
||||
{...getCommonProps({
|
||||
mode: CallMode.Direct,
|
||||
type: CallType.Audio,
|
||||
direction: CallDirection.Incoming,
|
||||
status: DirectCallStatus.Accepted,
|
||||
groupCallEnded: null,
|
||||
deviceCount: 0,
|
||||
maxDevices: Infinity,
|
||||
activeConversationId: 'someOtherConversation',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AcceptedIncomingAudioCallInCurrentCall(): JSX.Element {
|
||||
const props = getCommonProps({
|
||||
mode: CallMode.Direct,
|
||||
type: CallType.Audio,
|
||||
direction: CallDirection.Incoming,
|
||||
status: DirectCallStatus.Accepted,
|
||||
groupCallEnded: null,
|
||||
deviceCount: 0,
|
||||
maxDevices: Infinity,
|
||||
});
|
||||
|
||||
return (
|
||||
<CallingNotification
|
||||
{...props}
|
||||
activeConversationId={props.conversationId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AcceptedIncomingVideoCall(): JSX.Element {
|
||||
return (
|
||||
<CallingNotification
|
||||
|
@ -374,6 +411,42 @@ export function GroupCallActiveCallFull(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function GroupCallActiveInAnotherCall(): JSX.Element {
|
||||
return (
|
||||
<CallingNotification
|
||||
{...getCommonProps({
|
||||
mode: CallMode.Group,
|
||||
type: CallType.Group,
|
||||
direction: CallDirection.Incoming,
|
||||
status: GroupCallStatus.GenericGroupCall,
|
||||
groupCallEnded: false,
|
||||
deviceCount: 8,
|
||||
maxDevices: 10,
|
||||
activeConversationId: 'someOtherId',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallActiveInCurrentCall(): JSX.Element {
|
||||
const props = getCommonProps({
|
||||
mode: CallMode.Group,
|
||||
type: CallType.Group,
|
||||
direction: CallDirection.Incoming,
|
||||
status: GroupCallStatus.GenericGroupCall,
|
||||
groupCallEnded: false,
|
||||
deviceCount: 8,
|
||||
maxDevices: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<CallingNotification
|
||||
{...props}
|
||||
activeConversationId={props.conversationId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallEnded(): JSX.Element {
|
||||
return (
|
||||
<CallingNotification
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from '../../hooks/useKeyboardShortcuts';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
import { isMoreRecentThan } from '../../util/timestamp';
|
||||
import { InAnotherCallTooltip } from './InAnotherCallTooltip';
|
||||
|
||||
export type PropsActionsType = {
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
|
@ -162,6 +163,11 @@ function renderCallingNotificationButton(
|
|||
let disabledTooltipText: undefined | string;
|
||||
let onClick: () => void;
|
||||
|
||||
const inThisCall = Boolean(
|
||||
props.activeConversationId &&
|
||||
props.activeConversationId === props.conversationId
|
||||
);
|
||||
|
||||
if (props.callHistory == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -169,25 +175,20 @@ function renderCallingNotificationButton(
|
|||
switch (props.callHistory.mode) {
|
||||
case CallMode.Direct: {
|
||||
const { direction, type } = props.callHistory;
|
||||
if (props.callHistory.status === DirectCallStatus.Pending) {
|
||||
if (props.callHistory.status === DirectCallStatus.Pending || inThisCall) {
|
||||
return null;
|
||||
}
|
||||
buttonText =
|
||||
direction === CallDirection.Incoming
|
||||
? i18n('icu:calling__call-back')
|
||||
: i18n('icu:calling__call-again');
|
||||
if (props.activeConversationId != null) {
|
||||
disabledTooltipText = i18n('icu:calling__in-another-call-tooltip');
|
||||
onClick = noop;
|
||||
} else {
|
||||
onClick = () => {
|
||||
if (type === CallType.Video) {
|
||||
onOutgoingVideoCallInConversation(conversationId);
|
||||
} else {
|
||||
onOutgoingAudioCallInConversation(conversationId);
|
||||
}
|
||||
};
|
||||
}
|
||||
onClick = () => {
|
||||
if (type === CallType.Video) {
|
||||
onOutgoingVideoCallInConversation(conversationId);
|
||||
} else {
|
||||
onOutgoingAudioCallInConversation(conversationId);
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
case CallMode.Group: {
|
||||
|
@ -207,15 +208,16 @@ function renderCallingNotificationButton(
|
|||
return null;
|
||||
}
|
||||
} else if (props.activeConversationId != null) {
|
||||
if (props.activeConversationId === conversationId) {
|
||||
if (inThisCall) {
|
||||
buttonText = i18n('icu:calling__return');
|
||||
onClick = returnToActiveCall;
|
||||
} else {
|
||||
buttonText = i18n('icu:calling__join');
|
||||
disabledTooltipText = i18n('icu:calling__in-another-call-tooltip');
|
||||
onClick = noop;
|
||||
onClick = () => {
|
||||
onOutgoingVideoCallInConversation(conversationId);
|
||||
};
|
||||
}
|
||||
} else if (props.deviceCount > props.maxDevices) {
|
||||
} else if (props.deviceCount >= props.maxDevices) {
|
||||
buttonText = i18n('icu:calling__call-is-full');
|
||||
disabledTooltipText = i18n(
|
||||
'icu:calling__call-notification__button__call-full-tooltip',
|
||||
|
@ -240,9 +242,16 @@ function renderCallingNotificationButton(
|
|||
return null;
|
||||
}
|
||||
|
||||
const disabled = Boolean(disabledTooltipText);
|
||||
const inAnotherCall = Boolean(
|
||||
!disabled &&
|
||||
props.activeConversationId &&
|
||||
props.activeConversationId !== props.conversationId
|
||||
);
|
||||
const button = (
|
||||
<Button
|
||||
disabled={Boolean(disabledTooltipText)}
|
||||
disabled={disabled}
|
||||
discouraged={inAnotherCall}
|
||||
onClick={onClick}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SystemMessage}
|
||||
|
@ -258,5 +267,9 @@ function renderCallingNotificationButton(
|
|||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if (inAnotherCall) {
|
||||
return <InAnotherCallTooltip i18n={i18n}>{button}</InAnotherCallTooltip>;
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
|
|
@ -127,3 +127,8 @@ InSystemContacts.args = {
|
|||
systemGivenName: defaultContact.title,
|
||||
},
|
||||
};
|
||||
|
||||
export const InAnotherCall = Template.bind({});
|
||||
InAnotherCall.args = {
|
||||
hasActiveCall: true,
|
||||
};
|
||||
|
|
|
@ -26,9 +26,11 @@ import { Button, ButtonIconType, ButtonVariant } from '../Button';
|
|||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
import { InContactsIcon } from '../InContactsIcon';
|
||||
import { canHaveNicknameAndNote } from '../../util/nicknames';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||
import { offsetDistanceModifier } from '../../util/popperUtil';
|
||||
import { getThemeByThemeType } from '../../util/theme';
|
||||
import {
|
||||
InAnotherCallTooltip,
|
||||
getTooltipContent,
|
||||
} from './InAnotherCallTooltip';
|
||||
|
||||
export type PropsDataType = {
|
||||
areWeASubscriber: boolean;
|
||||
|
@ -124,11 +126,17 @@ export function ContactModal({
|
|||
|
||||
const renderQuickActions = React.useCallback(
|
||||
(conversationId: string) => {
|
||||
const inAnotherCallTooltipContent = hasActiveCall
|
||||
? getTooltipContent(i18n)
|
||||
: undefined;
|
||||
const discouraged = hasActiveCall;
|
||||
|
||||
const videoCallButton = (
|
||||
<Button
|
||||
icon={ButtonIconType.video}
|
||||
variant={ButtonVariant.Details}
|
||||
disabled={hasActiveCall}
|
||||
discouraged={discouraged}
|
||||
aria-label={inAnotherCallTooltipContent}
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
onOutgoingVideoCallInConversation(conversationId);
|
||||
|
@ -141,7 +149,8 @@ export function ContactModal({
|
|||
<Button
|
||||
icon={ButtonIconType.audio}
|
||||
variant={ButtonVariant.Details}
|
||||
disabled={hasActiveCall}
|
||||
discouraged={discouraged}
|
||||
aria-label={inAnotherCallTooltipContent}
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
onOutgoingAudioCallInConversation(conversationId);
|
||||
|
@ -170,28 +179,16 @@ export function ContactModal({
|
|||
{i18n('icu:ConversationDetails__HeaderButton--Message')}
|
||||
</Button>
|
||||
{hasActiveCall ? (
|
||||
<Tooltip
|
||||
className="ContactModal__tooltip"
|
||||
wrapperClassName="ContactModal__tooltip-wrapper"
|
||||
content={i18n('icu:ContactModal--already-in-call')}
|
||||
direction={TooltipPlacement.Top}
|
||||
popperModifiers={[offsetDistanceModifier(5)]}
|
||||
>
|
||||
<InAnotherCallTooltip i18n={i18n}>
|
||||
{videoCallButton}
|
||||
</Tooltip>
|
||||
</InAnotherCallTooltip>
|
||||
) : (
|
||||
videoCallButton
|
||||
)}
|
||||
{hasActiveCall ? (
|
||||
<Tooltip
|
||||
className="ContactModal__tooltip"
|
||||
wrapperClassName="ContactModal__tooltip-wrapper"
|
||||
content={i18n('icu:ContactModal--already-in-call')}
|
||||
direction={TooltipPlacement.Top}
|
||||
popperModifiers={[offsetDistanceModifier(5)]}
|
||||
>
|
||||
<InAnotherCallTooltip i18n={i18n}>
|
||||
{audioCallButton}
|
||||
</Tooltip>
|
||||
</InAnotherCallTooltip>
|
||||
) : (
|
||||
audioCallButton
|
||||
)}
|
||||
|
|
|
@ -5,7 +5,10 @@ import type { ComponentProps } from 'react';
|
|||
import React, { useContext } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
} from '../../test-both/helpers/getDefaultConversation';
|
||||
import { getRandomColor } from '../../test-both/helpers/getRandomColor';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { DurationInSeconds } from '../../util/durations';
|
||||
|
@ -17,6 +20,7 @@ import {
|
|||
OutgoingCallButtonStyle,
|
||||
} from './ConversationHeader';
|
||||
import { gifUrl } from '../../storybook/Fixtures';
|
||||
import { ThemeType } from '../../types/Util';
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/ConversationHeader',
|
||||
|
@ -30,17 +34,14 @@ type ItemsType = Array<{
|
|||
}>;
|
||||
|
||||
const commonConversation = getDefaultConversation();
|
||||
const commonProps = {
|
||||
const commonProps: PropsType = {
|
||||
...commonConversation,
|
||||
conversationId: commonConversation.id,
|
||||
conversationType: commonConversation.type,
|
||||
conversation: getDefaultConversation(),
|
||||
conversationName: commonConversation,
|
||||
addedByName: null,
|
||||
isBlocked: commonConversation.isBlocked ?? false,
|
||||
isReported: commonConversation.isReported ?? false,
|
||||
theme: ThemeType.light,
|
||||
|
||||
cannotLeaveBecauseYouAreLastAdmin: false,
|
||||
showBackButton: false,
|
||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.Both,
|
||||
isSelectMode: false,
|
||||
|
||||
|
@ -159,21 +160,6 @@ export function PrivateConvo(): JSX.Element {
|
|||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'With back button',
|
||||
props: {
|
||||
...commonProps,
|
||||
showBackButton: true,
|
||||
conversation: getDefaultConversation({
|
||||
color: getRandomColor(),
|
||||
phoneNumber: '(202) 555-0004',
|
||||
title: '(202) 555-0004',
|
||||
type: 'direct',
|
||||
id: '6',
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Disappearing messages set',
|
||||
props: {
|
||||
|
@ -422,7 +408,6 @@ export function NeedsDeleteConfirmation(): JSX.Element {
|
|||
React.useState(false);
|
||||
const props = {
|
||||
...commonProps,
|
||||
conversation: getDefaultConversation(),
|
||||
localDeleteWarningShown,
|
||||
setLocalDeleteWarningShown: () => setLocalDeleteWarningShown(true),
|
||||
};
|
||||
|
@ -436,7 +421,6 @@ export function NeedsDeleteConfirmationButNotEnabled(): JSX.Element {
|
|||
React.useState(false);
|
||||
const props = {
|
||||
...commonProps,
|
||||
conversation: getDefaultConversation(),
|
||||
localDeleteWarningShown,
|
||||
isDeleteSyncSendEnabled: false,
|
||||
setLocalDeleteWarningShown: () => setLocalDeleteWarningShown(true),
|
||||
|
@ -445,3 +429,48 @@ export function NeedsDeleteConfirmationButNotEnabled(): JSX.Element {
|
|||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function DirectConversationInAnotherCall(): JSX.Element {
|
||||
const props = {
|
||||
...commonProps,
|
||||
hasActiveCall: true,
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function DirectConversationInCurrentCall(): JSX.Element {
|
||||
const props = {
|
||||
...commonProps,
|
||||
hasActiveCall: true,
|
||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.None,
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function GroupConversationInAnotherCall(): JSX.Element {
|
||||
const props = {
|
||||
...commonProps,
|
||||
conversation: getDefaultGroup(),
|
||||
hasActiveCall: true,
|
||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.Join,
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function GroupConversationInCurrentCall(): JSX.Element {
|
||||
const props = {
|
||||
...commonProps,
|
||||
conversation: getDefaultGroup(),
|
||||
hasActiveCall: true,
|
||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.None,
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
} from './MessageRequestActionsConfirmation';
|
||||
import type { MinimalConversation } from '../../hooks/useMinimalConversation';
|
||||
import { LocalDeleteWarningModal } from '../LocalDeleteWarningModal';
|
||||
import { InAnotherCallTooltip } from './InAnotherCallTooltip';
|
||||
|
||||
function HeaderInfoTitle({
|
||||
name,
|
||||
|
@ -93,6 +94,7 @@ export type PropsDataType = {
|
|||
conversationName: ContactNameData;
|
||||
hasPanelShowing?: boolean;
|
||||
hasStories?: HasStories;
|
||||
hasActiveCall?: boolean;
|
||||
localDeleteWarningShown: boolean;
|
||||
isDeleteSyncSendEnabled: boolean;
|
||||
isMissingMandatoryProfileSharing?: boolean;
|
||||
|
@ -149,6 +151,7 @@ export const ConversationHeader = memo(function ConversationHeader({
|
|||
cannotLeaveBecauseYouAreLastAdmin,
|
||||
conversation,
|
||||
conversationName,
|
||||
hasActiveCall,
|
||||
hasPanelShowing,
|
||||
hasStories,
|
||||
i18n,
|
||||
|
@ -295,6 +298,7 @@ export const ConversationHeader = memo(function ConversationHeader({
|
|||
{!isSMSOnly && !isSignalConversation && (
|
||||
<OutgoingCallButtons
|
||||
conversation={conversation}
|
||||
hasActiveCall={hasActiveCall}
|
||||
i18n={i18n}
|
||||
isNarrow={isNarrow}
|
||||
onOutgoingAudioCall={onOutgoingAudioCall}
|
||||
|
@ -806,6 +810,7 @@ function HeaderMenu({
|
|||
|
||||
function OutgoingCallButtons({
|
||||
conversation,
|
||||
hasActiveCall,
|
||||
i18n,
|
||||
isNarrow,
|
||||
onOutgoingAudioCall,
|
||||
|
@ -815,24 +820,39 @@ function OutgoingCallButtons({
|
|||
PropsType,
|
||||
| 'i18n'
|
||||
| 'conversation'
|
||||
| 'hasActiveCall'
|
||||
| 'onOutgoingAudioCall'
|
||||
| 'onOutgoingVideoCall'
|
||||
| 'outgoingCallButtonStyle'
|
||||
>): JSX.Element | null {
|
||||
const disabled =
|
||||
conversation.type === 'group' &&
|
||||
conversation.announcementsOnly &&
|
||||
!conversation.areWeAdmin;
|
||||
const inAnotherCall = !disabled && hasActiveCall;
|
||||
|
||||
const videoButton = (
|
||||
<button
|
||||
aria-label={i18n('icu:makeOutgoingVideoCall')}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--video',
|
||||
conversation.announcementsOnly && !conversation.areWeAdmin
|
||||
disabled
|
||||
? 'module-ConversationHeader__button--show-disabled'
|
||||
: undefined,
|
||||
inAnotherCall
|
||||
? 'module-ConversationHeader__button--in-another-call'
|
||||
: undefined
|
||||
)}
|
||||
onClick={onOutgoingVideoCall}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
const videoElement = inAnotherCall ? (
|
||||
<InAnotherCallTooltip i18n={i18n}>{videoButton}</InAnotherCallTooltip>
|
||||
) : (
|
||||
videoButton
|
||||
);
|
||||
|
||||
const startCallShortcuts = useStartCallShortcuts(
|
||||
onOutgoingAudioCall,
|
||||
|
@ -844,31 +864,49 @@ function OutgoingCallButtons({
|
|||
case OutgoingCallButtonStyle.None:
|
||||
return null;
|
||||
case OutgoingCallButtonStyle.JustVideo:
|
||||
return videoButton;
|
||||
return videoElement;
|
||||
case OutgoingCallButtonStyle.Both:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const audioButton = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOutgoingAudioCall}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--audio',
|
||||
inAnotherCall
|
||||
? 'module-ConversationHeader__button--in-another-call'
|
||||
: undefined
|
||||
)}
|
||||
aria-label={i18n('icu:makeOutgoingCall')}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{videoButton}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOutgoingAudioCall}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--audio'
|
||||
)}
|
||||
aria-label={i18n('icu:makeOutgoingCall')}
|
||||
/>
|
||||
{videoElement}
|
||||
{inAnotherCall ? (
|
||||
<InAnotherCallTooltip i18n={i18n}>
|
||||
{audioButton}
|
||||
</InAnotherCallTooltip>
|
||||
) : (
|
||||
audioButton
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case OutgoingCallButtonStyle.Join:
|
||||
return (
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const joinButton = (
|
||||
<button
|
||||
aria-label={i18n('icu:joinOngoingCall')}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--join-call',
|
||||
conversation.announcementsOnly && !conversation.areWeAdmin
|
||||
disabled
|
||||
? 'module-ConversationHeader__button--show-disabled'
|
||||
: undefined,
|
||||
inAnotherCall
|
||||
? 'module-ConversationHeader__button--in-another-call'
|
||||
: undefined
|
||||
)}
|
||||
onClick={onOutgoingVideoCall}
|
||||
|
@ -877,6 +915,11 @@ function OutgoingCallButtons({
|
|||
{isNarrow ? null : i18n('icu:joinOngoingCall')}
|
||||
</button>
|
||||
);
|
||||
return inAnotherCall ? (
|
||||
<InAnotherCallTooltip i18n={i18n}>{joinButton}</InAnotherCallTooltip>
|
||||
) : (
|
||||
joinButton
|
||||
);
|
||||
default:
|
||||
throw missingCaseError(outgoingCallButtonStyle);
|
||||
}
|
||||
|
|
31
ts/components/conversation/InAnotherCallTooltip.tsx
Normal file
31
ts/components/conversation/InAnotherCallTooltip.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { offsetDistanceModifier } from '../../util/popperUtil';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
|
||||
type Props = {
|
||||
i18n: LocalizerType;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function getTooltipContent(i18n: LocalizerType): string {
|
||||
return i18n('icu:calling__in-another-call-tooltip');
|
||||
}
|
||||
|
||||
export function InAnotherCallTooltip({ i18n, children }: Props): JSX.Element {
|
||||
return (
|
||||
<Tooltip
|
||||
className="InAnotherCallTooltip"
|
||||
content={getTooltipContent(i18n)}
|
||||
direction={TooltipPlacement.Top}
|
||||
popperModifiers={[offsetDistanceModifier(5)]}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
|
@ -97,10 +97,9 @@ import { PanelType } from '../../types/Panels';
|
|||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
import { RenderLocation } from './MessageTextRenderer';
|
||||
import { UserText } from '../UserText';
|
||||
import {
|
||||
getColorForCallLink,
|
||||
getKeyFromCallLink,
|
||||
} from '../../util/getColorForCallLink';
|
||||
import { getColorForCallLink } from '../../util/getColorForCallLink';
|
||||
import { getKeyFromCallLink } from '../../util/callLinks';
|
||||
import { InAnotherCallTooltip } from './InAnotherCallTooltip';
|
||||
|
||||
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 16;
|
||||
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
|
||||
|
@ -215,6 +214,7 @@ export type PropsData = {
|
|||
customColor?: CustomColorType;
|
||||
conversationId: string;
|
||||
displayLimit?: number;
|
||||
activeCallConversationId?: string;
|
||||
text?: string;
|
||||
textDirection: TextDirection;
|
||||
textAttachment?: AttachmentType;
|
||||
|
@ -1980,23 +1980,38 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
private renderAction(): JSX.Element | null {
|
||||
const { direction, i18n, previews } = this.props;
|
||||
const { direction, activeCallConversationId, i18n, previews } = this.props;
|
||||
|
||||
if (this.shouldShowJoinButton()) {
|
||||
const firstPreview = previews[0];
|
||||
const inAnotherCall = Boolean(
|
||||
activeCallConversationId &&
|
||||
(!firstPreview.callLinkRoomId ||
|
||||
activeCallConversationId !== firstPreview.callLinkRoomId)
|
||||
);
|
||||
|
||||
return (
|
||||
const joinButton = (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames('module-message__action', {
|
||||
'module-message__action--incoming': direction === 'incoming',
|
||||
'module-message__action--outgoing': direction === 'outgoing',
|
||||
'module-message__action--incoming--in-another-call':
|
||||
direction === 'incoming' && inAnotherCall,
|
||||
'module-message__action--outgoing--in-another-call':
|
||||
direction === 'outgoing' && inAnotherCall,
|
||||
})}
|
||||
onClick={() => openLinkInWebBrowser(firstPreview?.url)}
|
||||
>
|
||||
{i18n('icu:calling__join')}
|
||||
</button>
|
||||
);
|
||||
|
||||
return inAnotherCall ? (
|
||||
<InAnotherCallTooltip i18n={i18n}>{joinButton}</InAnotherCallTooltip>
|
||||
) : (
|
||||
joinButton
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -1122,8 +1122,26 @@ LinkPreviewWithCallLink.args = {
|
|||
text: 'Use this link to join a Signal call: https://signal.link/call/#key=hzcn-pcff-ctsc-bdbf-stcr-tzpc-bhqx-kghh',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithCallLinkInGroup = Template.bind({});
|
||||
LinkPreviewWithCallLinkInGroup.args = {
|
||||
export const LinkPreviewWithCallLinkInAnotherCall = Template.bind({});
|
||||
LinkPreviewWithCallLinkInAnotherCall.args = {
|
||||
previews: [
|
||||
{
|
||||
url: 'https://signal.link/call/#key=hzcn-pcff-ctsc-bdbf-stcr-tzpc-bhqx-kghh',
|
||||
title: 'Camping Prep',
|
||||
description: 'Use this link to join a Signal call',
|
||||
image: undefined,
|
||||
date: undefined,
|
||||
isCallLink: true,
|
||||
isStickerPack: false,
|
||||
},
|
||||
],
|
||||
status: 'sent',
|
||||
activeCallConversationId: 'some-other-conversation',
|
||||
text: 'Use this link to join a Signal call: https://signal.link/call/#key=hzcn-pcff-ctsc-bdbf-stcr-tzpc-bhqx-kghh',
|
||||
};
|
||||
|
||||
export const LinkPreviewWithCallLinkInCurrentCall = Template.bind({});
|
||||
LinkPreviewWithCallLinkInCurrentCall.args = {
|
||||
previews: [
|
||||
{
|
||||
url: 'https://signal.link/call/#key=hzcn-pcff-ctsc-bdbf-stcr-tzpc-bhqx-kghh',
|
||||
|
@ -1133,11 +1151,13 @@ LinkPreviewWithCallLinkInGroup.args = {
|
|||
image: undefined,
|
||||
date: undefined,
|
||||
isCallLink: true,
|
||||
callLinkRoomId: 'room-id',
|
||||
isStickerPack: false,
|
||||
},
|
||||
],
|
||||
conversationType: 'group',
|
||||
status: 'sent',
|
||||
activeCallConversationId: 'room-id',
|
||||
text: 'Use this link to join a Signal call: https://signal.link/call/#key=hzcn-pcff-ctsc-bdbf-stcr-tzpc-bhqx-kghh',
|
||||
};
|
||||
|
||||
|
|
|
@ -232,3 +232,15 @@ export function WithCallHistoryGroup(): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InAnotherCallGroup(): JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return <ConversationDetails {...props} hasActiveCall />;
|
||||
}
|
||||
|
||||
export function InAnotherCallIndividual(): JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return <ConversationDetails {...props} hasActiveCall isGroup={false} />;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { ReactNode } from 'react';
|
|||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { Button, ButtonIconType, ButtonVariant } from '../../Button';
|
||||
import { Tooltip } from '../../Tooltip';
|
||||
import type {
|
||||
ConversationType,
|
||||
PushPanelForConversationActionType,
|
||||
|
@ -57,6 +56,10 @@ import { NavTab } from '../../../state/ducks/nav';
|
|||
import { ContextMenu } from '../../ContextMenu';
|
||||
import { canHaveNicknameAndNote } from '../../../util/nicknames';
|
||||
import { CallHistoryGroupPanelSection } from './CallHistoryGroupPanelSection';
|
||||
import {
|
||||
InAnotherCallTooltip,
|
||||
getTooltipContent,
|
||||
} from '../InAnotherCallTooltip';
|
||||
|
||||
enum ModalState {
|
||||
AddingGroupMembers,
|
||||
|
@ -743,22 +746,21 @@ function ConversationDetailsCallButton({
|
|||
onClick: () => unknown;
|
||||
type: 'audio' | 'video';
|
||||
}>) {
|
||||
const tooltipContent = hasActiveCall ? getTooltipContent(i18n) : undefined;
|
||||
const button = (
|
||||
<Button
|
||||
icon={ButtonIconType[type]}
|
||||
onClick={onClick}
|
||||
variant={ButtonVariant.Details}
|
||||
discouraged={hasActiveCall}
|
||||
aria-label={tooltipContent}
|
||||
>
|
||||
{type === 'audio' ? i18n('icu:audio') : i18n('icu:video')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (hasActiveCall) {
|
||||
return (
|
||||
<Tooltip content={i18n('icu:calling__in-another-call-tooltip')}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
return <InAnotherCallTooltip i18n={i18n}>{button}</InAnotherCallTooltip>;
|
||||
}
|
||||
|
||||
return button;
|
||||
|
|
|
@ -156,6 +156,7 @@ import {
|
|||
copyFromQuotedMessage,
|
||||
copyQuoteContentFromOriginal,
|
||||
} from '../messages/copyQuote';
|
||||
import { getRoomIdFromCallLink } from '../util/callLinksRingrtc';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
|
@ -1823,19 +1824,34 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
const urls = LinkPreview.findLinks(dataMessage.body || '');
|
||||
const incomingPreview = dataMessage.preview || [];
|
||||
const preview = incomingPreview.filter((item: LinkPreviewType) => {
|
||||
if (!item.image && !item.title) {
|
||||
return false;
|
||||
}
|
||||
// Story link previews don't have to correspond to links in the
|
||||
// message body.
|
||||
if (isStory(message.attributes)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
urls.includes(item.url) && LinkPreview.shouldPreviewHref(item.url)
|
||||
);
|
||||
});
|
||||
const preview = incomingPreview
|
||||
.map((item: LinkPreviewType) => {
|
||||
if (!item.image && !item.title) {
|
||||
return null;
|
||||
}
|
||||
// Story link previews don't have to correspond to links in the
|
||||
// message body.
|
||||
if (isStory(message.attributes)) {
|
||||
return item;
|
||||
}
|
||||
if (
|
||||
!urls.includes(item.url) ||
|
||||
!LinkPreview.shouldPreviewHref(item.url)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (LinkPreview.isCallLink(item.url)) {
|
||||
return {
|
||||
...item,
|
||||
isCallLink: true,
|
||||
callLinkRoomId: getRoomIdFromCallLink(item.url),
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter(isNotNil);
|
||||
if (preview.length < incomingPreview.length) {
|
||||
log.info(
|
||||
`${message.idForLogging()}: Eliminated ${
|
||||
|
|
|
@ -30,8 +30,9 @@ import { imageToBlurHash } from '../util/imageToBlurHash';
|
|||
import { maybeParseUrl } from '../util/url';
|
||||
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||
import { drop } from '../util/drop';
|
||||
import { linkCallRoute } from '../util/signalRoutes';
|
||||
import { calling } from './calling';
|
||||
import { getKeyFromCallLink } from '../util/callLinks';
|
||||
import { getRoomIdFromCallLink } from '../util/callLinksRingrtc';
|
||||
|
||||
const LINK_PREVIEW_TIMEOUT = 60 * SECOND;
|
||||
|
||||
|
@ -266,29 +267,27 @@ export function getLinkPreviewForSend(
|
|||
export function sanitizeLinkPreview(
|
||||
item: LinkPreviewResult | LinkPreviewWithHydratedData
|
||||
): LinkPreviewWithHydratedData {
|
||||
if (item.image) {
|
||||
// We eliminate the ObjectURL here, unneeded for send or save
|
||||
return {
|
||||
...item,
|
||||
image: omit(item.image, 'url'),
|
||||
title: dropNull(item.title),
|
||||
description: dropNull(item.description),
|
||||
date: dropNull(item.date),
|
||||
domain: LinkPreview.getDomain(item.url),
|
||||
isStickerPack: LinkPreview.isStickerPack(item.url),
|
||||
isCallLink: LinkPreview.isCallLink(item.url),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
const isCallLink = LinkPreview.isCallLink(item.url);
|
||||
const base: LinkPreviewWithHydratedData = {
|
||||
...item,
|
||||
title: dropNull(item.title),
|
||||
description: dropNull(item.description),
|
||||
date: dropNull(item.date),
|
||||
domain: LinkPreview.getDomain(item.url),
|
||||
isStickerPack: LinkPreview.isStickerPack(item.url),
|
||||
isCallLink: LinkPreview.isCallLink(item.url),
|
||||
isCallLink,
|
||||
callLinkRoomId: isCallLink ? getRoomIdFromCallLink(item.url) : undefined,
|
||||
};
|
||||
|
||||
if (item.image) {
|
||||
// We eliminate the ObjectURL here, unneeded for send or save
|
||||
return {
|
||||
...base,
|
||||
image: omit(item.image, 'url'),
|
||||
};
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
async function getPreview(
|
||||
|
@ -577,12 +576,8 @@ async function getCallLinkPreview(
|
|||
url: string,
|
||||
_abortSignal: Readonly<AbortSignal>
|
||||
): Promise<null | LinkPreviewResult> {
|
||||
const parsedUrl = linkCallRoute.fromUrl(url);
|
||||
if (parsedUrl == null) {
|
||||
throw new Error('Failed to parse call link URL');
|
||||
}
|
||||
|
||||
const callLinkRootKey = CallLinkRootKey.parse(parsedUrl.args.key);
|
||||
const keyString = getKeyFromCallLink(url);
|
||||
const callLinkRootKey = CallLinkRootKey.parse(keyString);
|
||||
const callLinkState = await calling.readCallLink(callLinkRootKey);
|
||||
if (callLinkState == null || callLinkState.revoked) {
|
||||
return null;
|
||||
|
|
|
@ -2104,9 +2104,7 @@ const _startCallLinkLobby = async ({
|
|||
|
||||
const { activeCallState } = state.calling;
|
||||
if (activeCallState && activeCallState.conversationId === roomId) {
|
||||
dispatch({
|
||||
type: TOGGLE_PIP,
|
||||
});
|
||||
dispatch(togglePip());
|
||||
return;
|
||||
}
|
||||
if (activeCallState) {
|
||||
|
@ -2263,7 +2261,13 @@ function startCallingLobby({
|
|||
"startCallingLobby: can't start lobby without a conversation"
|
||||
);
|
||||
|
||||
if (state.calling.activeCallState) {
|
||||
const { activeCallState } = state.calling;
|
||||
|
||||
if (activeCallState && activeCallState.conversationId === conversationId) {
|
||||
dispatch(togglePip());
|
||||
return;
|
||||
}
|
||||
if (activeCallState) {
|
||||
dispatch(
|
||||
toggleConfirmLeaveCallModal({
|
||||
type: 'conversation',
|
||||
|
|
|
@ -577,6 +577,7 @@ export const getPropsForQuote = (
|
|||
|
||||
export type GetPropsForMessageOptions = Pick<
|
||||
GetPropsForBubbleOptions,
|
||||
| 'activeCall'
|
||||
| 'conversationSelector'
|
||||
| 'ourConversationId'
|
||||
| 'ourAci'
|
||||
|
@ -676,6 +677,7 @@ export const getPropsForMessage = (
|
|||
const payment = getPayment(message);
|
||||
|
||||
const {
|
||||
activeCall,
|
||||
accountSelector,
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
|
@ -699,6 +701,7 @@ export const getPropsForMessage = (
|
|||
const { sticker } = message;
|
||||
|
||||
const isMessageTapToView = isTapToView(message);
|
||||
const activeCallConversationId = activeCall?.conversationId;
|
||||
|
||||
const isTargeted = message.id === targetedMessageId;
|
||||
const isSelected = selectedMessageIds?.includes(message.id) ?? false;
|
||||
|
@ -726,6 +729,7 @@ export const getPropsForMessage = (
|
|||
attachmentDroppedDueToSize,
|
||||
author,
|
||||
bodyRanges,
|
||||
activeCallConversationId,
|
||||
previews,
|
||||
quote,
|
||||
reactions,
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
|
|||
import type { CallHistoryGroup } from '../../types/CallDisposition';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { CallLinkDetails } from '../../components/CallLinkDetails';
|
||||
import { getCallLinkSelector } from '../selectors/calling';
|
||||
import { getActiveCallState, getCallLinkSelector } from '../selectors/calling';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useCallingActions } from '../ducks/calling';
|
||||
import * as log from '../../logging/log';
|
||||
|
@ -60,6 +60,11 @@ export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
|
|||
[roomId, updateCallLinkRestrictions]
|
||||
);
|
||||
|
||||
const activeCall = useSelector(getActiveCallState);
|
||||
const hasActiveCall = Boolean(
|
||||
activeCall && callLink && activeCall?.conversationId !== callLink?.roomId
|
||||
);
|
||||
|
||||
if (callLink == null) {
|
||||
log.error(`SmartCallLinkDetails: callLink not found for room ${roomId}`);
|
||||
return null;
|
||||
|
@ -69,6 +74,7 @@ export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
|
|||
<CallLinkDetails
|
||||
callHistoryGroup={callHistoryGroup}
|
||||
callLink={callLink}
|
||||
hasActiveCall={hasActiveCall}
|
||||
i18n={i18n}
|
||||
onDeleteCallLink={handleDeleteCallLink}
|
||||
onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
|
||||
|
|
|
@ -4,7 +4,7 @@ import React, { memo, useCallback, useMemo } from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
import { CallLinkEditModal } from '../../components/CallLinkEditModal';
|
||||
import { useCallingActions } from '../ducks/calling';
|
||||
import { getCallLinkSelector } from '../selectors/calling';
|
||||
import { getActiveCallState, getCallLinkSelector } from '../selectors/calling';
|
||||
import * as log from '../../logging/log';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
|
@ -74,6 +74,11 @@ export const SmartCallLinkEditModal = memo(
|
|||
toggleCallLinkEditModal(null);
|
||||
}, [callLink, startCallLinkLobby, toggleCallLinkEditModal]);
|
||||
|
||||
const activeCall = useSelector(getActiveCallState);
|
||||
const hasActiveCall = Boolean(
|
||||
activeCall && callLink && activeCall?.conversationId !== callLink?.roomId
|
||||
);
|
||||
|
||||
if (!callLink) {
|
||||
log.error(
|
||||
'SmartCallLinkEditModal: No call link found for roomId',
|
||||
|
@ -86,6 +91,7 @@ export const SmartCallLinkEditModal = memo(
|
|||
<CallLinkEditModal
|
||||
i18n={i18n}
|
||||
callLink={callLink}
|
||||
hasActiveCall={hasActiveCall}
|
||||
onClose={handleClose}
|
||||
onCopyCallLink={handleCopyCallLink}
|
||||
onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
|
||||
|
|
|
@ -154,7 +154,8 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
|
|||
conversation,
|
||||
allComposableConversations
|
||||
);
|
||||
const hasActiveCall = activeCall != null;
|
||||
const hasActiveCall =
|
||||
activeCall != null && activeCall.conversationId !== conversationId;
|
||||
const hasGroupLink =
|
||||
conversation.groupLink != null &&
|
||||
conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE;
|
||||
|
|
|
@ -56,7 +56,7 @@ const useOutgoingCallButtonStyle = (
|
|||
const callSelector = useSelector(getCallSelector);
|
||||
strictAssert(ourAci, 'useOutgoingCallButtonStyle missing our uuid');
|
||||
|
||||
if (activeCall != null) {
|
||||
if (activeCall?.conversationId === conversation.id) {
|
||||
return OutgoingCallButtonStyle.None;
|
||||
}
|
||||
|
||||
|
@ -100,6 +100,8 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
|
|||
const hasPanelShowing = useSelector(getHasPanelOpen);
|
||||
const outgoingCallButtonStyle = useOutgoingCallButtonStyle(conversation);
|
||||
const theme = useSelector(getTheme);
|
||||
const activeCall = useSelector(getActiveCallState);
|
||||
const hasActiveCall = Boolean(activeCall);
|
||||
|
||||
const {
|
||||
destroyMessages,
|
||||
|
@ -264,6 +266,7 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
|
|||
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
|
||||
conversation={minimalConversation}
|
||||
conversationName={conversationName}
|
||||
hasActiveCall={hasActiveCall}
|
||||
hasPanelShowing={hasPanelShowing}
|
||||
hasStories={hasStories}
|
||||
i18n={i18n}
|
||||
|
|
|
@ -10,6 +10,7 @@ type GenericLinkPreviewType<Image> = {
|
|||
url: string;
|
||||
isStickerPack?: boolean;
|
||||
isCallLink?: boolean;
|
||||
callLinkRoomId?: string;
|
||||
image?: Readonly<Image>;
|
||||
date?: number;
|
||||
};
|
||||
|
|
|
@ -23,6 +23,18 @@ export const CALL_LINK_DEFAULT_STATE = {
|
|||
expiration: null,
|
||||
};
|
||||
|
||||
export function getKeyFromCallLink(callLink: string): string {
|
||||
const url = new URL(callLink);
|
||||
if (url == null) {
|
||||
throw new Error('Failed to parse call link URL');
|
||||
}
|
||||
|
||||
const hash = url.hash.slice(1);
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
|
||||
return hashParams.get('key') || '';
|
||||
}
|
||||
|
||||
export function isCallLinksCreateEnabled(): boolean {
|
||||
if (isTestOrMockEnvironment()) {
|
||||
return true;
|
||||
|
|
|
@ -28,7 +28,11 @@ import {
|
|||
} from './zkgroup';
|
||||
import { getCheckedCallLinkAuthCredentialsForToday } from '../services/groupCredentialFetcher';
|
||||
import * as durations from './durations';
|
||||
import { fromAdminKeyBytes, toAdminKeyBytes } from './callLinks';
|
||||
import {
|
||||
fromAdminKeyBytes,
|
||||
getKeyFromCallLink,
|
||||
toAdminKeyBytes,
|
||||
} from './callLinks';
|
||||
|
||||
/**
|
||||
* RingRTC conversions
|
||||
|
@ -64,6 +68,12 @@ export function getCallLinkRootKeyFromUrlKey(key: string): Uint8Array {
|
|||
return CallLinkRootKey.parse(key).bytes;
|
||||
}
|
||||
|
||||
export function getRoomIdFromCallLink(url: string): string {
|
||||
const keyString = getKeyFromCallLink(url);
|
||||
const key = CallLinkRootKey.parse(keyString);
|
||||
return getRoomIdFromRootKey(key);
|
||||
}
|
||||
|
||||
export async function getCallLinkAuthCredentialPresentation(
|
||||
callLinkRootKey: CallLinkRootKey
|
||||
): Promise<CallLinkAuthCredentialPresentation> {
|
||||
|
|
|
@ -18,11 +18,3 @@ export function getColorForCallLink(rootKey: string): string {
|
|||
|
||||
return AvatarColors[index];
|
||||
}
|
||||
|
||||
export function getKeyFromCallLink(callLink: string): string {
|
||||
const url = new URL(callLink);
|
||||
const hash = url.hash.slice(1);
|
||||
const hashParams = new URLSearchParams(hash);
|
||||
|
||||
return hashParams.get('key') || '';
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue