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