Properly style call buttons across app, when already in a call

This commit is contained in:
Scott Nonnenberg 2024-08-27 06:48:41 +10:00 committed by GitHub
parent 3c25092f50
commit c251867699
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 610 additions and 189 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -309,8 +309,4 @@
margin-block: 8px 5px;
}
&__tooltip {
@include tooltip;
}
}

View file

@ -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%);

View file

@ -0,0 +1,6 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.InAnotherCallTooltip {
@include tooltip;
}

View file

@ -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';

View file

@ -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>

View file

@ -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}

View file

@ -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 />;
}

View file

@ -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

View file

@ -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 />;
}

View file

@ -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>

View file

@ -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';

View file

@ -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>
);
}

View file

@ -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,

View file

@ -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

View file

@ -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;
}

View file

@ -127,3 +127,8 @@ InSystemContacts.args = {
systemGivenName: defaultContact.title,
},
};
export const InAnotherCall = Template.bind({});
InAnotherCall.args = {
hasActiveCall: true,
};

View file

@ -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
)}

View file

@ -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} />;
}

View file

@ -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);
}

View 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>
);
}

View file

@ -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;

View file

@ -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',
};

View file

@ -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} />;
}

View file

@ -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;

View file

@ -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 ${

View file

@ -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;

View file

@ -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',

View file

@ -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,

View file

@ -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}

View file

@ -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}

View file

@ -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;

View file

@ -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}

View file

@ -10,6 +10,7 @@ type GenericLinkPreviewType<Image> = {
url: string;
isStickerPack?: boolean;
isCallLink?: boolean;
callLinkRoomId?: string;
image?: Readonly<Image>;
date?: number;
};

View file

@ -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;

View file

@ -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> {

View file

@ -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') || '';
}