Receive side of remote mute
This commit is contained in:
parent
0d89e7b01a
commit
a444790bf9
13 changed files with 540 additions and 16 deletions
|
@ -13529,7 +13529,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
|||
|
||||
```
|
||||
|
||||
## libsignal-account-keys 0.1.0, libsignal-core 0.1.0, mrp 2.51.0, protobuf 2.51.0, ringrtc 2.51.0, regex-aot 0.1.0, partial-default-derive 0.1.0
|
||||
## libsignal-account-keys 0.1.0, libsignal-core 0.1.0, mrp 2.52.0, protobuf 2.52.0, ringrtc 2.52.0, regex-aot 0.1.0, partial-default-derive 0.1.0
|
||||
|
||||
```
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
|
|
|
@ -4164,6 +4164,22 @@
|
|||
"messageformat": "Mic on",
|
||||
"description": "Shown in a call when the user is muted and then unmutes their audio input using the Mute toggle button."
|
||||
},
|
||||
"icu:CallControls__MutedBySomeoneToast": {
|
||||
"messageformat": "{otherName} muted you.",
|
||||
"description": "Shown in a call when another client mutes this user"
|
||||
},
|
||||
"icu:CallControls__SomeoneMutedSomeoneToast": {
|
||||
"messageformat": "{name} muted {otherName}.",
|
||||
"description": "Shown in a call when one client mutes another client"
|
||||
},
|
||||
"icu:CallControls__YouMutedSomeoneToast": {
|
||||
"messageformat": "You muted {otherName}.",
|
||||
"description": "Shown in a call when you mute another client"
|
||||
},
|
||||
"icu:CallControls__YouMutedYourselfToast": {
|
||||
"messageformat": "You muted yourself from another device.",
|
||||
"description": "Shown in a call when you joined a call on two devices and mute one from the other."
|
||||
},
|
||||
"icu:CallControls__LowerHandSuggestionToast": {
|
||||
"messageformat": "Lower your hand?",
|
||||
"description": "Shown in a call when the user has their hand raised but has been talking, next to a button to lower their hand."
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
"@react-types/shared": "3.27.0",
|
||||
"@signalapp/libsignal-client": "0.70.0",
|
||||
"@signalapp/quill-cjs": "2.1.2",
|
||||
"@signalapp/ringrtc": "2.51.0",
|
||||
"@signalapp/ringrtc": "2.52.0",
|
||||
"@signalapp/sqlcipher": "2.0.1",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@types/dom-mediacapture-transform": "0.1.11",
|
||||
|
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
@ -132,8 +132,8 @@ importers:
|
|||
specifier: 2.1.2
|
||||
version: 2.1.2
|
||||
'@signalapp/ringrtc':
|
||||
specifier: 2.51.0
|
||||
version: 2.51.0
|
||||
specifier: 2.52.0
|
||||
version: 2.52.0
|
||||
'@signalapp/sqlcipher':
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
|
@ -2550,8 +2550,8 @@ packages:
|
|||
resolution: {integrity: sha512-y2sgqdivlrG41J4Zvt/82xtH/PZjDlgItqlD2g/Cv3ZbjlR6cGhTNXbfNygCJB8nXj+C7I28pjt1Zm3k0pv2mg==}
|
||||
engines: {npm: '>=8.2.3'}
|
||||
|
||||
'@signalapp/ringrtc@2.51.0':
|
||||
resolution: {integrity: sha512-p0S7JLReO9NjfxB3Er3V6eydNB4IUfrIIimtlD7E9CerUWtejxvPNqEPxfKTCL/vVde/pqsIn0Qw9LjysA84xA==}
|
||||
'@signalapp/ringrtc@2.52.0':
|
||||
resolution: {integrity: sha512-s4mwnL4f6LBpDonIJ2TZKv1mD+zbAq6aPnPH3tlJZUhrdJj0GZtjTVY2i7Y6dmLG3zsho10a8hGtGjPAMyDmlw==}
|
||||
|
||||
'@signalapp/sqlcipher@2.0.1':
|
||||
resolution: {integrity: sha512-7dSgNnf/hrGZfVSGlhVH39f7BDNNOO61tg6Xu/Fa38TCeZj6/U5YILKQavBArCtkahUvzGBV9QIyRr0zereU7A==}
|
||||
|
@ -12072,7 +12072,7 @@ snapshots:
|
|||
lodash: 4.17.21
|
||||
quill-delta: 5.1.0
|
||||
|
||||
'@signalapp/ringrtc@2.51.0':
|
||||
'@signalapp/ringrtc@2.52.0':
|
||||
dependencies:
|
||||
https-proxy-agent: 7.0.6
|
||||
tar: 6.2.1
|
||||
|
|
|
@ -128,6 +128,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||
setIsCallActive: action('set-is-call-active'),
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalAudioRemoteMuted: action('set-local-audio-remote-muted'),
|
||||
setLocalPreviewContainer: action('set-local-preview-container'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
|
|
|
@ -36,6 +36,7 @@ import type {
|
|||
SendGroupCallReactionType,
|
||||
SetGroupCallVideoRequestType,
|
||||
SetLocalAudioType,
|
||||
SetMutedByType,
|
||||
SetLocalVideoType,
|
||||
SetRendererCanvasType,
|
||||
StartCallType,
|
||||
|
@ -124,6 +125,7 @@ export type PropsType = {
|
|||
setIsCallActive: (_: boolean) => void;
|
||||
setLocalAudio: SetLocalAudioType;
|
||||
setLocalVideo: SetLocalVideoType;
|
||||
setLocalAudioRemoteMuted: SetMutedByType;
|
||||
setLocalPreviewContainer: (container: HTMLDivElement | null) => void;
|
||||
setOutgoingRing: (_: boolean) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
|
@ -188,6 +190,7 @@ function ActiveCallManager({
|
|||
sendGroupCallReaction,
|
||||
setGroupCallVideoRequest,
|
||||
setLocalAudio,
|
||||
setLocalAudioRemoteMuted,
|
||||
setLocalPreviewContainer,
|
||||
setLocalVideo,
|
||||
setRendererCanvas,
|
||||
|
@ -475,6 +478,7 @@ function ActiveCallManager({
|
|||
setLocalPreviewContainer={setLocalPreviewContainer}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalAudioRemoteMuted={setLocalAudioRemoteMuted}
|
||||
setLocalVideo={setLocalVideo}
|
||||
stickyControls={showParticipantsList}
|
||||
switchToPresentationView={switchToPresentationView}
|
||||
|
@ -567,6 +571,7 @@ export function CallManager({
|
|||
setGroupCallVideoRequest,
|
||||
setIsCallActive,
|
||||
setLocalAudio,
|
||||
setLocalAudioRemoteMuted,
|
||||
setLocalPreviewContainer,
|
||||
setLocalVideo,
|
||||
setOutgoingRing,
|
||||
|
@ -659,6 +664,7 @@ export function CallManager({
|
|||
sendGroupCallReaction={sendGroupCallReaction}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalAudioRemoteMuted={setLocalAudioRemoteMuted}
|
||||
setLocalPreviewContainer={setLocalPreviewContainer}
|
||||
setLocalVideo={setLocalVideo}
|
||||
setOutgoingRing={setOutgoingRing}
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
ActiveCallReactionsType,
|
||||
ActiveGroupCallType,
|
||||
GroupCallRemoteParticipantType,
|
||||
ObservedRemoteMuteType,
|
||||
} from '../types/Calling';
|
||||
import {
|
||||
CallViewMode,
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
} from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { generateAci } from '../types/ServiceId';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import type { PropsType } from './CallScreen';
|
||||
|
@ -47,6 +49,7 @@ const conversation = getDefaultConversation({
|
|||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
isMe: true,
|
||||
});
|
||||
|
||||
type OverridePropsBase = {
|
||||
|
@ -56,6 +59,7 @@ type OverridePropsBase = {
|
|||
viewMode?: CallViewMode;
|
||||
outgoingRing?: boolean;
|
||||
reactions?: ActiveCallReactionsType;
|
||||
myAci?: AciString;
|
||||
};
|
||||
|
||||
type DirectCallOverrideProps = OverridePropsBase & {
|
||||
|
@ -80,6 +84,9 @@ type GroupCallOverrideProps = OverridePropsBase & {
|
|||
selfViewExpanded?: boolean;
|
||||
suggestLowerHand?: boolean;
|
||||
outgoingRing?: boolean;
|
||||
localMutedBy?: number;
|
||||
observedRemoteMute?: ObservedRemoteMuteType;
|
||||
forceIndex0IsMe?: boolean;
|
||||
};
|
||||
|
||||
const createActiveDirectCallProp = (
|
||||
|
@ -112,10 +119,16 @@ const getConversationsByDemuxId = (overrideProps: GroupCallOverrideProps) => {
|
|||
const conversationsByDemuxId = new Map<number, ConversationType>(
|
||||
overrideProps.remoteParticipants?.map((participant, index) => [
|
||||
participant.demuxId,
|
||||
getDefaultConversationWithServiceId({
|
||||
getDefaultConversationWithServiceId(
|
||||
{
|
||||
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
|
||||
title: `Participant ${index + 1}`,
|
||||
}),
|
||||
isMe: index === 0 && (overrideProps.forceIndex0IsMe ?? false),
|
||||
},
|
||||
index === 0 && (overrideProps.forceIndex0IsMe ?? false)
|
||||
? conversation.serviceId
|
||||
: undefined
|
||||
),
|
||||
])
|
||||
);
|
||||
conversationsByDemuxId.set(LOCAL_DEMUX_ID, conversation);
|
||||
|
@ -164,6 +177,8 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
|||
),
|
||||
reactions: overrideProps.reactions || [],
|
||||
suggestLowerHand: overrideProps.suggestLowerHand ?? false,
|
||||
mutedBy: overrideProps.localMutedBy ?? undefined,
|
||||
observedRemoteMute: overrideProps.observedRemoteMute ?? undefined,
|
||||
});
|
||||
|
||||
const createActiveCallProp = (
|
||||
|
@ -221,7 +236,7 @@ const createProps = (
|
|||
name: 'Morty Smith',
|
||||
profileName: 'Morty Smith',
|
||||
title: 'Morty Smith',
|
||||
serviceId: generateAci(),
|
||||
serviceId: overrideProps.myAci ?? generateAci(),
|
||||
}),
|
||||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
|
@ -231,6 +246,7 @@ const createProps = (
|
|||
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalAudioRemoteMuted: action('set-local-audio-remote-muted'),
|
||||
setLocalPreviewContainer: action('set-local-preview-container'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
|
@ -997,3 +1013,131 @@ export function CallLinkUnknownContactMissingMediaKeys(): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RemoteMuteYouRemoteMutedByOther(): JSX.Element {
|
||||
// Should show you're muted by another
|
||||
const [props, setProps] = React.useState(() =>
|
||||
createProps({ callMode: CallMode.Group })
|
||||
);
|
||||
React.useEffect(() => {
|
||||
setProps(
|
||||
createProps({
|
||||
callMode: CallMode.Group,
|
||||
hasLocalAudio: false,
|
||||
remoteParticipants: allRemoteParticipants,
|
||||
localMutedBy: 3,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
return <CallScreen {...props} />;
|
||||
}
|
||||
|
||||
export function RemoteMuteYouRemoteMutedBySelf(): JSX.Element {
|
||||
// Should show you're muted by yourself
|
||||
const [props, setProps] = React.useState(() =>
|
||||
createProps({ callMode: CallMode.Group })
|
||||
);
|
||||
const myAci = conversation.serviceId as AciString;
|
||||
React.useEffect(() => {
|
||||
setProps(
|
||||
createProps({
|
||||
callMode: CallMode.Group,
|
||||
remoteParticipants: [
|
||||
{
|
||||
aci: myAci,
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isHandRaised: false,
|
||||
mediaKeysReceived: true,
|
||||
presenting: false,
|
||||
sharingScreen: false,
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversation({
|
||||
isBlocked: false,
|
||||
title: 'Note To Self',
|
||||
serviceId: myAci,
|
||||
isMe: true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
localMutedBy: 0,
|
||||
myAci,
|
||||
forceIndex0IsMe: true,
|
||||
})
|
||||
);
|
||||
}, [myAci]);
|
||||
return <CallScreen {...props} />;
|
||||
}
|
||||
|
||||
export function RemoteMuteObserveMuteYouSent(): JSX.Element {
|
||||
// Should show you muted someone else
|
||||
const [props, setProps] = React.useState(() =>
|
||||
createProps({ callMode: CallMode.Group })
|
||||
);
|
||||
React.useEffect(() => {
|
||||
setProps(
|
||||
createProps({
|
||||
callMode: CallMode.Group,
|
||||
remoteParticipants: allRemoteParticipants,
|
||||
observedRemoteMute: { source: LOCAL_DEMUX_ID, target: 0 },
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
return <CallScreen {...props} />;
|
||||
}
|
||||
|
||||
export function RemoteMuteObserveMuteOtherSent(): JSX.Element {
|
||||
// Should show someone else muted a third person
|
||||
const [props, setProps] = React.useState(() =>
|
||||
createProps({ callMode: CallMode.Group })
|
||||
);
|
||||
React.useEffect(() => {
|
||||
setProps(
|
||||
createProps({
|
||||
callMode: CallMode.Group,
|
||||
remoteParticipants: allRemoteParticipants,
|
||||
observedRemoteMute: { source: 3, target: 4 },
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
return <CallScreen {...props} />;
|
||||
}
|
||||
|
||||
export function RemoteMuteObserveIgnoreSelfMute(): JSX.Element {
|
||||
// Should show nothing because the ACIs match
|
||||
const [props, setProps] = React.useState(() =>
|
||||
createProps({ callMode: CallMode.Group })
|
||||
);
|
||||
const myAci = conversation.serviceId as AciString;
|
||||
React.useEffect(() => {
|
||||
setProps(
|
||||
createProps({
|
||||
callMode: CallMode.Group,
|
||||
remoteParticipants: [
|
||||
{
|
||||
aci: myAci,
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isHandRaised: false,
|
||||
mediaKeysReceived: true,
|
||||
presenting: false,
|
||||
sharingScreen: false,
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversation({
|
||||
isBlocked: false,
|
||||
title: 'Note To Self',
|
||||
serviceId: myAci,
|
||||
isMe: true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
observedRemoteMute: { source: LOCAL_DEMUX_ID, target: 0 },
|
||||
myAci,
|
||||
forceIndex0IsMe: true,
|
||||
})
|
||||
);
|
||||
}, [myAci]);
|
||||
return <CallScreen {...props} />;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
SetLocalAudioType,
|
||||
SetLocalVideoType,
|
||||
SetRendererCanvasType,
|
||||
SetMutedByType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { CallingHeader, getCallViewIconClassname } from './CallingHeader';
|
||||
|
@ -135,6 +136,7 @@ export type PropsType = {
|
|||
toggleSelfViewExpanded: () => void;
|
||||
toggleSettings: () => void;
|
||||
changeCallView: (mode: CallViewMode) => void;
|
||||
setLocalAudioRemoteMuted: SetMutedByType;
|
||||
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
|
||||
|
||||
export const isInSpeakerView = (
|
||||
|
@ -224,6 +226,7 @@ export function CallScreen({
|
|||
toggleScreenRecordingPermissionsDialog,
|
||||
toggleSelfViewExpanded,
|
||||
toggleSettings,
|
||||
setLocalAudioRemoteMuted,
|
||||
}: PropsType): JSX.Element {
|
||||
const {
|
||||
conversation,
|
||||
|
@ -980,7 +983,17 @@ export function CallScreen({
|
|||
: false
|
||||
}
|
||||
isHandRaised={localHandRaised}
|
||||
mutedBy={
|
||||
isGroupOrAdhocActiveCall(activeCall) ? activeCall.mutedBy : undefined
|
||||
}
|
||||
observedRemoteMute={
|
||||
isGroupOrAdhocActiveCall(activeCall)
|
||||
? activeCall.observedRemoteMute
|
||||
: undefined
|
||||
}
|
||||
conversationsByDemuxId={conversationsByDemuxId}
|
||||
i18n={i18n}
|
||||
setLocalAudioRemoteMuted={setLocalAudioRemoteMuted}
|
||||
/>
|
||||
{isCallLinkAdmin ? (
|
||||
<CallingPendingParticipants
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import type { ActiveCallType } from '../types/Calling';
|
||||
import type { ActiveCallType, ObservedRemoteMuteType } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
@ -12,6 +12,7 @@ import { difference as setDifference } from '../util/setUtil';
|
|||
import { isMoreRecentThan } from '../util/timestamp';
|
||||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||
import { SECOND } from '../util/durations';
|
||||
import type { SetMutedByType } from '../state/ducks/calling';
|
||||
|
||||
type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -83,9 +84,11 @@ export function useScreenSharingStoppedToast({
|
|||
|
||||
function useMutedToast({
|
||||
hasLocalAudio,
|
||||
mutedBy,
|
||||
i18n,
|
||||
}: {
|
||||
hasLocalAudio: boolean;
|
||||
mutedBy: number | undefined;
|
||||
i18n: LocalizerType;
|
||||
}): void {
|
||||
const previousHasLocalAudio = usePrevious(hasLocalAudio, hasLocalAudio);
|
||||
|
@ -95,7 +98,8 @@ function useMutedToast({
|
|||
useEffect(() => {
|
||||
if (
|
||||
previousHasLocalAudio !== undefined &&
|
||||
hasLocalAudio !== previousHasLocalAudio
|
||||
hasLocalAudio !== previousHasLocalAudio &&
|
||||
mutedBy === undefined // skip this if we were muted by someone
|
||||
) {
|
||||
hideToast(MUTED_TOAST_KEY);
|
||||
showToast({
|
||||
|
@ -107,7 +111,14 @@ function useMutedToast({
|
|||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}, [hasLocalAudio, previousHasLocalAudio, hideToast, showToast, i18n]);
|
||||
}, [
|
||||
hasLocalAudio,
|
||||
previousHasLocalAudio,
|
||||
hideToast,
|
||||
showToast,
|
||||
mutedBy,
|
||||
i18n,
|
||||
]);
|
||||
}
|
||||
|
||||
function useOutgoingRingToast({
|
||||
|
@ -321,6 +332,143 @@ function useLowerHandSuggestionToast({
|
|||
}, [isHandRaised, hideToast]);
|
||||
}
|
||||
|
||||
function useMutedByToast({
|
||||
mutedBy,
|
||||
setLocalAudioRemoteMuted,
|
||||
conversationsByDemuxId,
|
||||
i18n,
|
||||
}: {
|
||||
mutedBy: number | undefined;
|
||||
setLocalAudioRemoteMuted?: SetMutedByType;
|
||||
conversationsByDemuxId?: Map<number, ConversationType>;
|
||||
i18n: LocalizerType;
|
||||
}): void {
|
||||
const previousMutedBy = usePrevious(mutedBy, mutedBy);
|
||||
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const MUTED_BY_TOAST_KEY = 'MUTED_BY_TOAST_KEY';
|
||||
|
||||
useEffect(() => {
|
||||
if (setLocalAudioRemoteMuted === undefined) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
mutedBy === undefined ||
|
||||
// if it's undefined, likely we just received a remote mute request
|
||||
// and hadn't had one before.
|
||||
(previousMutedBy !== undefined && previousMutedBy === mutedBy) ||
|
||||
conversationsByDemuxId === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const otherConversation = conversationsByDemuxId.get(mutedBy);
|
||||
const title = otherConversation?.title;
|
||||
if (title === undefined) {
|
||||
return;
|
||||
}
|
||||
setLocalAudioRemoteMuted({ mutedBy });
|
||||
let content;
|
||||
if (otherConversation?.isMe) {
|
||||
content = i18n('icu:CallControls__YouMutedYourselfToast');
|
||||
} else {
|
||||
content = i18n('icu:CallControls__MutedBySomeoneToast', {
|
||||
otherName: title,
|
||||
});
|
||||
}
|
||||
hideToast(MUTED_BY_TOAST_KEY);
|
||||
showToast({
|
||||
key: MUTED_BY_TOAST_KEY,
|
||||
content,
|
||||
dismissable: true,
|
||||
autoClose: true,
|
||||
lifetime: 10 * SECOND,
|
||||
});
|
||||
}, [
|
||||
mutedBy,
|
||||
previousMutedBy,
|
||||
conversationsByDemuxId,
|
||||
i18n,
|
||||
showToast,
|
||||
hideToast,
|
||||
MUTED_BY_TOAST_KEY,
|
||||
setLocalAudioRemoteMuted,
|
||||
]);
|
||||
}
|
||||
|
||||
function useObservedRemoteMuteToast({
|
||||
observedRemoteMute,
|
||||
conversationsByDemuxId,
|
||||
i18n,
|
||||
}: {
|
||||
observedRemoteMute: ObservedRemoteMuteType | undefined;
|
||||
conversationsByDemuxId?: Map<number, ConversationType>;
|
||||
i18n: LocalizerType;
|
||||
}): void {
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const OBSERVED_REMOTE_MUTE_TOAST_KEY = 'OBSERVED_REMOTE_MUTE_TOAST_KEY';
|
||||
const previousObservedRemoteMute = usePrevious(
|
||||
observedRemoteMute,
|
||||
observedRemoteMute
|
||||
);
|
||||
useEffect(() => {
|
||||
if (
|
||||
observedRemoteMute === undefined ||
|
||||
(previousObservedRemoteMute !== undefined &&
|
||||
previousObservedRemoteMute === observedRemoteMute) ||
|
||||
conversationsByDemuxId === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceConversation = conversationsByDemuxId.get(
|
||||
observedRemoteMute.source
|
||||
);
|
||||
const targetConversation = conversationsByDemuxId.get(
|
||||
observedRemoteMute.target
|
||||
);
|
||||
if (sourceConversation?.serviceId === targetConversation?.serviceId) {
|
||||
// Ignore self-mutes.
|
||||
return;
|
||||
}
|
||||
const targetTitle = targetConversation?.title;
|
||||
if (targetTitle === undefined) {
|
||||
return;
|
||||
}
|
||||
let content;
|
||||
if (sourceConversation?.isMe) {
|
||||
content = i18n('icu:CallControls__YouMutedSomeoneToast', {
|
||||
otherName: targetTitle,
|
||||
});
|
||||
} else {
|
||||
const sourceTitle = sourceConversation?.title;
|
||||
if (sourceTitle === undefined) {
|
||||
return;
|
||||
}
|
||||
content = i18n('icu:CallControls__SomeoneMutedSomeoneToast', {
|
||||
name: sourceTitle,
|
||||
otherName: targetTitle,
|
||||
});
|
||||
}
|
||||
|
||||
hideToast(OBSERVED_REMOTE_MUTE_TOAST_KEY);
|
||||
showToast({
|
||||
key: OBSERVED_REMOTE_MUTE_TOAST_KEY,
|
||||
content,
|
||||
dismissable: true,
|
||||
autoClose: true,
|
||||
lifetime: 10 * SECOND,
|
||||
});
|
||||
}, [
|
||||
observedRemoteMute,
|
||||
previousObservedRemoteMute,
|
||||
conversationsByDemuxId,
|
||||
i18n,
|
||||
showToast,
|
||||
hideToast,
|
||||
OBSERVED_REMOTE_MUTE_TOAST_KEY,
|
||||
]);
|
||||
}
|
||||
|
||||
type CallingButtonToastsType = {
|
||||
hasLocalAudio: boolean;
|
||||
outgoingRing: boolean | undefined;
|
||||
|
@ -331,7 +479,11 @@ type CallingButtonToastsType = {
|
|||
suggestLowerHand?: boolean;
|
||||
isHandRaised?: boolean;
|
||||
handleLowerHand?: () => void;
|
||||
mutedBy?: number;
|
||||
observedRemoteMute?: ObservedRemoteMuteType;
|
||||
conversationsByDemuxId?: Map<number, ConversationType>;
|
||||
i18n: LocalizerType;
|
||||
setLocalAudioRemoteMuted?: SetMutedByType;
|
||||
};
|
||||
|
||||
export function CallingButtonToastsContainer(
|
||||
|
@ -361,8 +513,12 @@ function CallingButtonToasts({
|
|||
handleLowerHand,
|
||||
isHandRaised,
|
||||
i18n,
|
||||
mutedBy,
|
||||
observedRemoteMute,
|
||||
conversationsByDemuxId,
|
||||
setLocalAudioRemoteMuted,
|
||||
}: CallingButtonToastsType) {
|
||||
useMutedToast({ hasLocalAudio, i18n });
|
||||
useMutedToast({ hasLocalAudio, mutedBy, i18n });
|
||||
useOutgoingRingToast({ outgoingRing, i18n });
|
||||
useRaisedHandsToast({ raisedHands, renderRaisedHandsToast });
|
||||
useLowerHandSuggestionToast({
|
||||
|
@ -371,6 +527,17 @@ function CallingButtonToasts({
|
|||
handleLowerHand,
|
||||
isHandRaised,
|
||||
});
|
||||
useMutedByToast({
|
||||
mutedBy,
|
||||
setLocalAudioRemoteMuted,
|
||||
conversationsByDemuxId,
|
||||
i18n,
|
||||
});
|
||||
useObservedRemoteMuteToast({
|
||||
observedRemoteMute,
|
||||
conversationsByDemuxId,
|
||||
i18n,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -221,6 +221,9 @@ type CallingReduxInterface = Pick<
|
|||
| 'startCallLinkLobbyByRoomId'
|
||||
| 'peekNotConnectedGroupCall'
|
||||
| 'setSuggestLowerHand'
|
||||
| 'setLocalAudio'
|
||||
| 'setMutedBy'
|
||||
| 'onObservedRemoteMute'
|
||||
> & {
|
||||
areAnyCallsActiveOrRinging(): boolean;
|
||||
};
|
||||
|
@ -1637,6 +1640,21 @@ export class CallingClass {
|
|||
);
|
||||
}
|
||||
},
|
||||
onRemoteMute: (_groupCall: GroupCall, demuxId: number) => {
|
||||
log.info('GroupCall#onRemoteMute');
|
||||
this.#reduxInterface?.setMutedBy({ mutedBy: demuxId });
|
||||
},
|
||||
onObservedRemoteMute: (
|
||||
_groupCall: GroupCall,
|
||||
sourceDemuxId: number,
|
||||
targetDemuxId: number
|
||||
) => {
|
||||
log.info('GroupCall#onObservedRemoteMute');
|
||||
this.#reduxInterface?.onObservedRemoteMute({
|
||||
source: sourceDemuxId,
|
||||
target: targetDemuxId,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2251,6 +2269,20 @@ export class CallingClass {
|
|||
}
|
||||
}
|
||||
|
||||
setOutgoingAudioRemoteMuted(conversationId: string, source: number): void {
|
||||
const call = getOwn(this.#callsLookup, conversationId);
|
||||
if (!call) {
|
||||
log.warn('Trying to remote mute outgoing audio for a non-existent call');
|
||||
return;
|
||||
}
|
||||
|
||||
if (call instanceof GroupCall) {
|
||||
call.setOutgoingAudioMutedRemotely(source);
|
||||
} else {
|
||||
log.warn('Trying to remote mute outgoing audio on a 1:1 call');
|
||||
}
|
||||
}
|
||||
|
||||
async setOutgoingVideo(
|
||||
conversationId: string,
|
||||
enabled: boolean
|
||||
|
|
|
@ -29,6 +29,7 @@ import type {
|
|||
ChangeIODevicePayloadType,
|
||||
GroupCallVideoRequest,
|
||||
MediaDeviceSettings,
|
||||
ObservedRemoteMuteType,
|
||||
PresentedSource,
|
||||
PresentableSource,
|
||||
} from '../../types/Calling';
|
||||
|
@ -192,6 +193,8 @@ export type ActiveCallStateType = {
|
|||
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
||||
showParticipantsList: boolean;
|
||||
suggestLowerHand?: boolean;
|
||||
mutedBy?: number;
|
||||
observedRemoteMute?: ObservedRemoteMuteType;
|
||||
reactions?: ActiveCallReactionsType;
|
||||
};
|
||||
export type WaitingCallStateType = ReadonlyDeep<{
|
||||
|
@ -370,6 +373,18 @@ export type SetLocalVideoType = (
|
|||
}>
|
||||
) => void;
|
||||
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
export type SetMutedByType = (
|
||||
payload: ReadonlyDeep<{
|
||||
mutedBy: number;
|
||||
}>
|
||||
) => void;
|
||||
|
||||
export type ObservedRemoteMuteDucksType = ReadonlyDeep<{
|
||||
source: number;
|
||||
target: number;
|
||||
}>;
|
||||
|
||||
export type SetGroupCallVideoRequestType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
resolutions: Array<GroupCallVideoRequest>;
|
||||
|
@ -658,6 +673,8 @@ const SELECT_PRESENTING_SOURCE = 'calling/SELECT_PRESENTING_SOURCE';
|
|||
const SEND_GROUP_CALL_REACTION = 'calling/SEND_GROUP_CALL_REACTION';
|
||||
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||
const SET_MUTED_BY = 'calling/SET_MUTED_BY';
|
||||
const OBSERVED_REMOTE_MUTE = 'calling/OBSERVED_REMOTE_MUTE';
|
||||
const SET_OUTGOING_RING = 'calling/SET_OUTGOING_RING';
|
||||
const SET_PRESENTING = 'calling/SET_PRESENTING';
|
||||
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
|
||||
|
@ -915,6 +932,16 @@ type SetLocalVideoFulfilledActionType = ReadonlyDeep<{
|
|||
payload: Parameters<SetLocalVideoType>[0];
|
||||
}>;
|
||||
|
||||
type SetMutedByActionType = ReadonlyDeep<{
|
||||
type: 'calling/SET_MUTED_BY';
|
||||
payload: Parameters<SetMutedByType>[0];
|
||||
}>;
|
||||
|
||||
type ObservedRemoteMuteActionType = ReadonlyDeep<{
|
||||
type: 'calling/OBSERVED_REMOTE_MUTE';
|
||||
payload: ObservedRemoteMuteDucksType;
|
||||
}>;
|
||||
|
||||
type SetPresentingFulfilledActionType = ReadonlyDeep<{
|
||||
type: 'calling/SET_PRESENTING';
|
||||
payload?: PresentedSource;
|
||||
|
@ -1019,6 +1046,8 @@ export type CallingActionType =
|
|||
| SetCapturerBatonActionType
|
||||
| SetLocalAudioActionType
|
||||
| SetLocalVideoFulfilledActionType
|
||||
| SetMutedByActionType
|
||||
| ObservedRemoteMuteActionType
|
||||
| SetPresentingSourcesActionType
|
||||
| SetOutgoingRingActionType
|
||||
| StartDirectCallActionType
|
||||
|
@ -1929,6 +1958,28 @@ function setLocalAudio(
|
|||
};
|
||||
}
|
||||
|
||||
function setLocalAudioRemoteMuted(
|
||||
payload: Parameters<SetMutedByType>[0]
|
||||
): ThunkAction<void, RootStateType, unknown, SetLocalAudioActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const activeCall = getActiveCall(getState().calling);
|
||||
if (!activeCall) {
|
||||
log.warn('Trying to set local audio when no call is active');
|
||||
return;
|
||||
}
|
||||
|
||||
calling.setOutgoingAudioRemoteMuted(
|
||||
activeCall.conversationId,
|
||||
payload?.mutedBy
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SET_LOCAL_AUDIO_FULFILLED,
|
||||
payload: { enabled: false },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setLocalVideo(
|
||||
payload: Parameters<SetLocalVideoType>[0]
|
||||
): ThunkAction<void, RootStateType, unknown, SetLocalVideoFulfilledActionType> {
|
||||
|
@ -1967,6 +2018,40 @@ function setLocalVideo(
|
|||
};
|
||||
}
|
||||
|
||||
function setMutedBy(
|
||||
payload: Parameters<SetMutedByType>[0]
|
||||
): ThunkAction<void, RootStateType, unknown, SetMutedByActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const activeCall = getActiveCall(getState().calling);
|
||||
if (!activeCall) {
|
||||
log.warn('Trying to set muted by when no call is active');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_MUTED_BY,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function onObservedRemoteMute(
|
||||
payload: ObservedRemoteMuteDucksType
|
||||
): ThunkAction<void, RootStateType, unknown, ObservedRemoteMuteActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const activeCall = getActiveCall(getState().calling);
|
||||
if (!activeCall) {
|
||||
log.warn('Trying to record remote mute when no call is active');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: OBSERVED_REMOTE_MUTE,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setGroupCallVideoRequest(
|
||||
payload: SetGroupCallVideoRequestType
|
||||
): ThunkAction<void, RootStateType, unknown, never> {
|
||||
|
@ -2765,6 +2850,7 @@ export const actions = {
|
|||
handleCallLinkDelete,
|
||||
joinedAdhocCall,
|
||||
leaveCurrentCallAndStartCallingLobby,
|
||||
onObservedRemoteMute,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
openSystemPreferencesAction,
|
||||
|
@ -2788,6 +2874,8 @@ export const actions = {
|
|||
setIsCallActive,
|
||||
setLocalAudio,
|
||||
setLocalVideo,
|
||||
setLocalAudioRemoteMuted,
|
||||
setMutedBy,
|
||||
setOutgoingRing,
|
||||
setRendererCanvas,
|
||||
setSuggestLowerHand,
|
||||
|
@ -3995,11 +4083,16 @@ export function reducer(
|
|||
return state;
|
||||
}
|
||||
|
||||
const newMutedBy = action.payload?.enabled
|
||||
? undefined
|
||||
: state.activeCallState.mutedBy;
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeCallState: {
|
||||
...state.activeCallState,
|
||||
hasLocalAudio: Boolean(action.payload?.enabled),
|
||||
mutedBy: newMutedBy,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -4019,6 +4112,47 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_MUTED_BY) {
|
||||
const { mutedBy } = action.payload;
|
||||
const { activeCallState } = state;
|
||||
|
||||
if (activeCallState?.state !== 'Active') {
|
||||
log.warn('Cannot set muted by with no active call');
|
||||
return state;
|
||||
}
|
||||
|
||||
const newMutedBy = activeCallState.hasLocalAudio ? mutedBy : undefined;
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeCallState: {
|
||||
...activeCallState,
|
||||
mutedBy: newMutedBy,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === OBSERVED_REMOTE_MUTE) {
|
||||
const { source, target } = action.payload;
|
||||
const { activeCallState } = state;
|
||||
|
||||
if (activeCallState?.state !== 'Active') {
|
||||
log.warn('Cannot observe muted by with no active call');
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeCallState: {
|
||||
...activeCallState,
|
||||
observedRemoteMute: {
|
||||
source,
|
||||
target,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === CHANGE_IO_DEVICE_FULFILLED) {
|
||||
const { selectedDevice } = action.payload;
|
||||
const nextState = Object.create(null);
|
||||
|
|
|
@ -338,6 +338,8 @@ const mapStateToActiveCallProp = (
|
|||
remoteParticipants,
|
||||
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
|
||||
suggestLowerHand: Boolean(activeCallState.suggestLowerHand),
|
||||
mutedBy: activeCallState.mutedBy,
|
||||
observedRemoteMute: activeCallState.observedRemoteMute,
|
||||
} satisfies ActiveGroupCallType;
|
||||
}
|
||||
default:
|
||||
|
@ -460,6 +462,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
|||
setGroupCallVideoRequest,
|
||||
setIsCallActive,
|
||||
setLocalAudio,
|
||||
setLocalAudioRemoteMuted,
|
||||
setLocalVideo,
|
||||
setOutgoingRing,
|
||||
setRendererCanvas,
|
||||
|
@ -519,6 +522,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
|||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
setIsCallActive={setIsCallActive}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalAudioRemoteMuted={setLocalAudioRemoteMuted}
|
||||
setLocalPreviewContainer={setLocalPreviewContainer}
|
||||
setLocalVideo={setLocalVideo}
|
||||
setOutgoingRing={setOutgoingRing}
|
||||
|
|
|
@ -84,6 +84,11 @@ export type ActiveDirectCallType = ActiveCallBaseType & {
|
|||
remoteAudioLevel: number;
|
||||
};
|
||||
|
||||
export type ObservedRemoteMuteType = {
|
||||
source: number;
|
||||
target: number;
|
||||
};
|
||||
|
||||
export type ActiveGroupCallType = ActiveCallBaseType & {
|
||||
callMode: CallMode.Group | CallMode.Adhoc;
|
||||
connectionState: GroupCallConnectionState;
|
||||
|
@ -100,6 +105,8 @@ export type ActiveGroupCallType = ActiveCallBaseType & {
|
|||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
remoteAudioLevels: Map<number, number>;
|
||||
suggestLowerHand: boolean;
|
||||
mutedBy?: number;
|
||||
observedRemoteMute?: ObservedRemoteMuteType;
|
||||
};
|
||||
|
||||
export type ActiveCallType = ActiveDirectCallType | ActiveGroupCallType;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue