diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 580c437d06..926fdc0f0c 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -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;
diff --git a/stylesheets/components/Button.scss b/stylesheets/components/Button.scss
index 54bf6845a7..27e38f7997 100644
--- a/stylesheets/components/Button.scss
+++ b/stylesheets/components/Button.scss
@@ -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;
diff --git a/stylesheets/components/CallsTab.scss b/stylesheets/components/CallsTab.scss
index dd933cb6b4..06fcee9ab4 100644
--- a/stylesheets/components/CallsTab.scss
+++ b/stylesheets/components/CallsTab.scss
@@ -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 {
diff --git a/stylesheets/components/ContactModal.scss b/stylesheets/components/ContactModal.scss
index cc3ac37704..d6a1fb0061 100644
--- a/stylesheets/components/ContactModal.scss
+++ b/stylesheets/components/ContactModal.scss
@@ -309,8 +309,4 @@
margin-block: 8px 5px;
}
-
- &__tooltip {
- @include tooltip;
- }
}
diff --git a/stylesheets/components/ConversationHeader.scss b/stylesheets/components/ConversationHeader.scss
index 0b655bbd20..f5b3f120c5 100644
--- a/stylesheets/components/ConversationHeader.scss
+++ b/stylesheets/components/ConversationHeader.scss
@@ -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%);
diff --git a/stylesheets/components/InAnotherCallTooltip.scss b/stylesheets/components/InAnotherCallTooltip.scss
new file mode 100644
index 0000000000..a666cd4e4c
--- /dev/null
+++ b/stylesheets/components/InAnotherCallTooltip.scss
@@ -0,0 +1,6 @@
+// Copyright 2024 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+.InAnotherCallTooltip {
+ @include tooltip;
+}
diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss
index d51eff8990..733af684d6 100644
--- a/stylesheets/manifest.scss
+++ b/stylesheets/manifest.scss
@@ -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';
diff --git a/ts/components/Button.stories.tsx b/ts/components/Button.stories.tsx
index c745533933..42296fee40 100644
--- a/ts/components/Button.stories.tsx
+++ b/ts/components/Button.stories.tsx
@@ -37,6 +37,16 @@ export function KitchenSink(): JSX.Element {
{variant}
+
+
+
))}
diff --git a/ts/components/Button.tsx b/ts/components/Button.tsx
index 36dee96f50..8c511c1188 100644
--- a/ts/components/Button.tsx
+++ b/ts/components/Button.tsx
@@ -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(
children,
className,
disabled = false,
+ discouraged = false,
icon,
style,
tabIndex,
@@ -143,8 +145,10 @@ export const Button = React.forwardRef(
'module-Button',
sizeClassName,
variantClassName,
+ discouraged ? `${variantClassName}--discouraged` : undefined,
icon && `module-Button--icon--${icon}`,
- className
+ className,
+ className && discouraged ? `${className}--discouraged` : undefined
)}
disabled={disabled}
onClick={onClick}
diff --git a/ts/components/CallLinkDetails.stories.tsx b/ts/components/CallLinkDetails.stories.tsx
index e91f827d85..0662457c2b 100644
--- a/ts/components/CallLinkDetails.stories.tsx
+++ b/ts/components/CallLinkDetails.stories.tsx
@@ -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 ;
}
+
+export function InAnotherCall(args: CallLinkDetailsProps): JSX.Element {
+ return ;
+}
diff --git a/ts/components/CallLinkDetails.tsx b/ts/components/CallLinkDetails.tsx
index 40a8b57fda..e42718b443 100644
--- a/ts/components/CallLinkDetails.tsx
+++ b/ts/components/CallLinkDetails.tsx
@@ -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 = (
+
+ );
+
return (
@@ -77,14 +92,13 @@ export function CallLinkDetails({
-
+ {hasActiveCall ? (
+
+ {joinButton}
+
+ ) : (
+ joinButton
+ )}
;
}
+
+export function InAnotherCall(args: CallLinkEditModalProps): JSX.Element {
+ return ;
+}
diff --git a/ts/components/CallLinkEditModal.tsx b/ts/components/CallLinkEditModal.tsx
index 5d6b6f186e..f4f78e8193 100644
--- a/ts/components/CallLinkEditModal.tsx
+++ b/ts/components/CallLinkEditModal.tsx
@@ -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 = (
+
+ );
+
return (
-
+ {hasActiveCall ? (
+
+ {joinButton}
+
+ ) : (
+ joinButton
+ )}
diff --git a/ts/components/CallsList.tsx b/ts/components/CallsList.tsx
index aa031f6c17..16005cbf99 100644
--- a/ts/components/CallsList.tsx
+++ b/ts/components/CallsList.tsx
@@ -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';
diff --git a/ts/components/CallsNewCall.tsx b/ts/components/CallsNewCallButton.tsx
similarity index 94%
rename from ts/components/CallsNewCall.tsx
rename to ts/components/CallsNewCallButton.tsx
index 050cd37be0..4b7cb85885 100644
--- a/ts/components/CallsNewCall.tsx
+++ b/ts/components/CallsNewCallButton.tsx
@@ -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({
);
- return tooltipContent === '' ? (
+ return inAnotherCallTooltipContent === '' ? (
buttonContent
) : (
-
- {buttonContent}
-
+ {buttonContent}
);
}
diff --git a/ts/components/CallsTab.tsx b/ts/components/CallsTab.tsx
index f04e150866..152637389e 100644
--- a/ts/components/CallsTab.tsx
+++ b/ts/components/CallsTab.tsx
@@ -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,
diff --git a/ts/components/conversation/CallingNotification.stories.tsx b/ts/components/conversation/CallingNotification.stories.tsx
index 2ea4358d52..964b2c9bad 100644
--- a/ts/components/conversation/CallingNotification.stories.tsx
+++ b/ts/components/conversation/CallingNotification.stories.tsx
@@ -29,6 +29,7 @@ export default {
} satisfies Meta;
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 (
+
+ );
+}
+
+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 (
+
+ );
+}
+
export function AcceptedIncomingVideoCall(): JSX.Element {
return (
+ );
+}
+
+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 (
+
+ );
+}
+
export function GroupCallEnded(): JSX.Element {
return (
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 = (