Send remote mute requests in group calls and call links

This commit is contained in:
ayumi-signal
2026-02-27 10:36:15 -08:00
committed by GitHub
parent b155aa1cfb
commit 54e5b64ab0
25 changed files with 226 additions and 48 deletions

View File

@@ -13,7 +13,7 @@ import { ThemeType } from '../types/Util.std.js';
import { Theme } from '../util/theme.std.js';
import { UserText } from './UserText.dom.js';
import { SharedGroupNames } from './SharedGroupNames.dom.js';
import type { ContactModalStateType } from '../state/ducks/globalModals.preload.js';
import type { ContactModalStateType } from '../types/globalModals.std.js';
export type CallLinkPendingParticipantModalProps = {
readonly i18n: LocalizerType;

View File

@@ -60,6 +60,7 @@ import {
import type { NotificationProfileType } from '../types/NotificationProfile.std.js';
import { strictAssert } from '../util/assert.std.js';
import type { SetLocalPreviewContainerType } from '../services/calling.preload.js';
import type { ContactModalStateType } from '../types/globalModals.std.js';
const { noop } = lodash;
@@ -107,7 +108,7 @@ export type PropsType = {
renderReactionPicker: (
props: React.ComponentProps<typeof SmartReactionPicker>
) => React.JSX.Element;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
startCall: (payload: StartCallType) => void;
toggleParticipants: () => void;
acceptCall: (_: AcceptCallType) => void;

View File

@@ -21,6 +21,7 @@ import { Button } from './Button.dom.js';
import { Modal } from './Modal.dom.js';
import { Theme } from '../util/theme.std.js';
import { ConfirmationDialog } from './ConfirmationDialog.dom.js';
import type { ContactModalStateType } from '../types/globalModals.std.js';
const { partition } = lodash;
@@ -46,10 +47,7 @@ export type PropsType = {
readonly onShareCallLinkViaSignal: () => void;
readonly removeClient: (payload: RemoveClientType) => void;
readonly blockClient: (payload: RemoveClientType) => void;
readonly showContactModal: (
contactId: string,
conversationId?: string
) => void;
readonly showContactModal: (payload: ContactModalStateType) => void;
};
type UnknownContactsPropsType = {
@@ -204,7 +202,10 @@ export function CallingAdhocCallInfo({
}
onClose();
showContactModal(participant.id);
showContactModal({
activeCallDemuxId: participant.demuxId,
contactId: participant.id,
});
}}
type="button"
>

View File

@@ -16,12 +16,14 @@ import { sortByTitle } from '../util/sortByTitle.std.js';
import type { ConversationType } from '../state/ducks/conversations.preload.js';
import { isInSystemContacts } from '../util/isInSystemContacts.std.js';
import { ModalContainerContext } from './ModalHost.dom.js';
import type { ContactModalStateType } from '../types/globalModals.std.js';
type ParticipantType = ConversationType & {
hasRemoteAudio?: boolean;
hasRemoteVideo?: boolean;
isHandRaised?: boolean;
presenting?: boolean;
demuxId?: number;
};
export type PropsType = {
@@ -30,10 +32,7 @@ export type PropsType = {
readonly onClose: () => void;
readonly ourServiceId: ServiceIdString | undefined;
readonly participants: Array<ParticipantType>;
readonly showContactModal: (
contactId: string,
conversationId?: string
) => void;
readonly showContactModal: (payload: ContactModalStateType) => void;
};
export const CallingParticipantsList = React.memo(
@@ -119,7 +118,11 @@ export const CallingParticipantsList = React.memo(
}
onClose();
showContactModal(participant.id, conversationId);
showContactModal({
activeCallDemuxId: participant.demuxId,
contactId: participant.id,
conversationId,
});
}}
type="button"
>

View File

@@ -4,7 +4,6 @@
import React from 'react';
import type {
CallQualitySurveyPropsType,
ContactModalStateType,
DeleteMessagesPropsType,
EditHistoryMessagesType,
EditNicknameAndNoteModalPropsType,
@@ -15,7 +14,10 @@ import type {
UserNotFoundModalStateType,
} from '../state/ducks/globalModals.preload.js';
import type { LocalizerType, ThemeType } from '../types/Util.std.js';
import { UsernameOnboardingState } from '../types/globalModals.std.js';
import {
type ContactModalStateType,
UsernameOnboardingState,
} from '../types/globalModals.std.js';
import { missingCaseError } from '../util/missingCaseError.std.js';
import { ButtonVariant } from './Button.dom.js';

View File

@@ -53,6 +53,7 @@ import { arrow } from '../util/keyboard.dom.js';
import { StoryProgressSegment } from './StoryProgressSegment.dom.js';
import type { EmojiSkinTone } from './fun/data/emojis.std.js';
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.js';
import type { ContactModalStateType } from '../types/globalModals.std.js';
const log = createLogger('StoryViewer');
@@ -110,7 +111,7 @@ export type PropsType = {
retryMessageSend: (messageId: string) => unknown;
saveAttachment: SaveAttachmentActionCreatorType;
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
showToast: ShowToastAction;
emojiSkinToneDefault: EmojiSkinTone | null;
story: StoryViewType;

View File

@@ -45,6 +45,7 @@ import { useConfirmDiscard } from '../hooks/useConfirmDiscard.dom.js';
import { AxoContextMenu } from '../axo/AxoContextMenu.dom.js';
import type { AxoMenuBuilder } from '../axo/AxoMenuBuilder.dom.js';
import { drop } from '../util/drop.std.js';
import type { ContactModalStateType } from '../types/globalModals.std.js';
const { noop, orderBy } = lodash;
@@ -124,7 +125,7 @@ export type PropsType = {
ourConversationId: string | undefined;
preferredReactionEmoji: ReadonlyArray<string>;
replies: ReadonlyArray<ReplyType>;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
emojiSkinToneDefault: EmojiSkinTone | null;
sortedGroupMembers?: ReadonlyArray<ConversationType>;
views: ReadonlyArray<StorySendStateType>;
@@ -545,7 +546,7 @@ type ReplyOrReactionMessageProps = {
reply: ReplyType;
shouldCollapseAbove: boolean;
shouldCollapseBelow: boolean;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
messageExpanded: (messageId: string, displayLimit: number) => void;
showSpoiler: (messageId: string, data: Record<number, boolean>) => void;
};

View File

@@ -37,6 +37,7 @@ export default {
},
args: {
i18n,
activeCallDemuxId: undefined,
areWeASubscriber: false,
areWeAdmin: false,
badges: [],
@@ -51,6 +52,8 @@ export default {
hideContactModal: action('hideContactModal'),
isAdmin: false,
isMember: true,
isMuted: false,
isRemoteMuteVisible: false,
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
),
@@ -58,6 +61,7 @@ export default {
'onOutgoingVideoCallInConversation'
),
removeMemberFromGroup: action('removeMemberFromGroup'),
sendRemoteMute: action('sendRemoteMute'),
showConversation: action('showConversation'),
startAvatarDownload: action('startAvatarDownload'),
theme: ThemeType.light,
@@ -188,3 +192,19 @@ export const InAnotherCall = Template.bind({});
InAnotherCall.args = {
hasActiveCall: true,
};
export const InCallTogether = Template.bind({});
InCallTogether.args = {
activeCallDemuxId: 123,
hasActiveCall: true,
isMuted: false,
isRemoteMuteVisible: true,
};
export const InCallTogetherMuted = Template.bind({});
InCallTogetherMuted.args = {
activeCallDemuxId: 123,
hasActiveCall: true,
isMuted: true,
isRemoteMuteVisible: true,
};

View File

@@ -31,18 +31,20 @@ import {
InAnotherCallTooltip,
getTooltipContent,
} from './InAnotherCallTooltip.dom.js';
import type {
ContactModalStateType,
ToggleGroupMemberLabelInfoModalType,
} from '../../state/ducks/globalModals.preload.js';
import type { ToggleGroupMemberLabelInfoModalType } from '../../state/ducks/globalModals.preload.js';
import type { ContactModalStateType } from '../../types/globalModals.std.js';
import { GroupMemberLabel } from './ContactName.dom.js';
import { SignalService as Proto } from '../../protobuf/index.std.js';
import { AxoSymbol } from '../../axo/AxoSymbol.dom.js';
import { tw } from '../../axo/tw.dom.js';
import { strictAssert } from '../../util/assert.std.js';
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
const log = createLogger('ContactModal');
export type PropsDataType = {
activeCallDemuxId?: number;
areWeASubscriber: boolean;
areWeAdmin: boolean;
badges: ReadonlyArray<BadgeType>;
@@ -55,6 +57,8 @@ export type PropsDataType = {
readonly i18n: LocalizerType;
isAdmin: boolean;
isMember: boolean;
isMuted: boolean;
isRemoteMuteVisible: boolean;
theme: ThemeType;
hasActiveCall: boolean;
isInFullScreenCall: boolean;
@@ -67,6 +71,7 @@ type PropsActionType = {
onOutgoingAudioCallInConversation: (conversationId: string) => unknown;
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
removeMemberFromGroup: (conversationId: string, contactId: string) => void;
sendRemoteMute: (demuxId: number) => void;
showConversation: ShowConversationType;
startAvatarDownload: () => void;
toggleAboutContactModal: (options: ContactModalStateType) => unknown;
@@ -91,9 +96,11 @@ enum SubModalState {
ToggleAdmin = 'ToggleAdmin',
MemberRemove = 'MemberRemove',
ConfirmingBlock = 'ConfirmingBlock',
ConfirmingMute = 'ConfirmingMute',
}
export function ContactModal({
activeCallDemuxId,
areWeAdmin,
areWeASubscriber,
badges,
@@ -110,10 +117,13 @@ export function ContactModal({
i18n,
isAdmin,
isMember,
isMuted,
isRemoteMuteVisible,
onOpenEditNicknameAndNoteModal,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
removeMemberFromGroup,
sendRemoteMute,
showConversation,
startAvatarDownload,
theme,
@@ -326,6 +336,33 @@ export function ContactModal({
</ConfirmationDialog>
);
break;
case SubModalState.ConfirmingMute:
modalNode = (
<ConfirmationDialog
dialogName="ContactModal.confirmMute"
actions={[
{
text: i18n('icu:ContactModal--confirm-mute-primary-button'),
action: () => {
strictAssert(
activeCallDemuxId != null,
'activeCallDemuxId must exist'
);
hideContactModal();
sendRemoteMute(activeCallDemuxId);
},
style: 'affirmative',
},
]}
i18n={i18n}
onClose={() => setSubModalState(SubModalState.None)}
>
{i18n('icu:ContactModal--confirm-mute-body', {
contact: contact.title,
})}
</ConfirmationDialog>
);
break;
default: {
const state: never = subModalState;
log.warn(`unexpected ${state}!`);
@@ -530,6 +567,19 @@ export function ContactModal({
</button>
</>
)}
{isRemoteMuteVisible && (
<button
type="button"
className="ContactModal__button"
onClick={() => setSubModalState(SubModalState.ConfirmingMute)}
disabled={isMuted}
>
<AxoSymbol.Icon symbol="mic-slash" size={20} label={null} />
<span className={tw('ms-[12px]')}>
{i18n('icu:ContactModal--mute-audio')}
</span>
</button>
)}
</div>
{modalNode}
</div>

View File

@@ -20,6 +20,7 @@ import { Button, ButtonVariant } from '../Button.dom.js';
import { assertDev } from '../../util/assert.std.js';
import { missingCaseError } from '../../util/missingCaseError.std.js';
import { isInSystemContacts } from '../../util/isInSystemContacts.std.js';
import type { ContactModalStateType } from '../../types/globalModals.std.js';
export type ReviewPropsType = Readonly<
| {
@@ -61,7 +62,7 @@ export type PropsType = {
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
onClose: () => void;
showContactModal: (contactId: string, conversationId?: string) => unknown;
showContactModal: (payload: ContactModalStateType) => unknown;
removeMember: (
conversationId: string,
memberConversationId: string
@@ -253,7 +254,7 @@ export function ContactSpoofingReviewDialog(
sharedGroupNames={safe.sharedGroupNames}
i18n={i18n}
onClick={() => {
showContactModal(safe.conversation.id);
showContactModal({ contactId: safe.conversation.id });
}}
theme={theme}
isSignalConnection={safe.isSignalConnection}
@@ -350,7 +351,9 @@ export function ContactSpoofingReviewDialog(
theme={theme}
oldName={oldName}
onClick={() => {
showContactModal(conversationInfo.conversation.id);
showContactModal({
contactId: conversationInfo.conversation.id,
});
}}
isSignalConnection={isSignalConnection}
>

View File

@@ -18,7 +18,7 @@ import { StoryViewModeType } from '../../types/Stories.std.js';
import { Button, ButtonVariant } from '../Button.dom.js';
import { SafetyTipsModal } from '../SafetyTipsModal.dom.js';
import { I18n } from '../I18n.dom.js';
import type { ContactModalStateType } from '../../state/ducks/globalModals.preload.js';
import type { ContactModalStateType } from '../../types/globalModals.std.js';
export type Props = {
about?: string;

View File

@@ -127,6 +127,7 @@ import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.js';
import type { MemberLabelType } from '../../types/GroupMemberLabels.std.js';
import type { ContactModalStateType } from '../../types/globalModals.std.js';
const { drop, take, unescape } = lodash;
@@ -391,7 +392,7 @@ export type PropsActions = {
optionIndexes: ReadonlyArray<number>;
}) => void;
endPoll: (messageId: string) => void;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
showSpoiler: (messageId: string, data: Record<number, boolean>) => void;
cancelAttachmentDownload: (options: { messageId: string }) => void;
@@ -2330,7 +2331,7 @@ export class Message extends React.PureComponent<Props, State> {
event.stopPropagation();
event.preventDefault();
showContactModal(author.id, conversationId);
showContactModal({ contactId: author.id, conversationId });
}}
phoneNumber={author.phoneNumber}
profileName={author.profileName}

View File

@@ -14,6 +14,7 @@ import type { ConversationType } from '../../state/ducks/conversations.preload.j
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges.preload.js';
import { drop } from '../../util/drop.std.js';
import { useReducedMotion } from '../../hooks/useReducedMotion.dom.js';
import type { ContactModalStateType } from '../../types/globalModals.std.js';
const MAX_AVATARS_COUNT = 3;
@@ -38,7 +39,7 @@ export type TypingBubblePropsType = {
lastItemTimestamp: number | undefined;
getConversation: (id: string) => ConversationType;
getPreferredBadge: PreferredBadgeSelectorType;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
i18n: LocalizerType;
theme: ThemeType;
};
@@ -83,7 +84,7 @@ function TypingBubbleAvatar({
shouldAnimate: boolean;
getPreferredBadge: PreferredBadgeSelectorType;
onContactExit: (id: string | undefined) => void;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
i18n: LocalizerType;
theme: ThemeType;
}): ReactElement | null {
@@ -130,7 +131,7 @@ function TypingBubbleAvatar({
onClick={event => {
event.stopPropagation();
event.preventDefault();
showContactModal(contact.id, conversationId);
showContactModal({ contactId: contact.id, conversationId });
}}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}

View File

@@ -65,7 +65,7 @@ import {
getTooltipContent,
} from '../InAnotherCallTooltip.dom.js';
import { BadgeSustainerInstructionsDialog } from '../../BadgeSustainerInstructionsDialog.dom.js';
import type { ContactModalStateType } from '../../../state/ducks/globalModals.preload.js';
import type { ContactModalStateType } from '../../../types/globalModals.std.js';
import type { ShowToastAction } from '../../../state/ducks/toast.preload.js';
import { ToastType } from '../../../types/Toast.dom.js';
@@ -142,7 +142,7 @@ type ActionProps = {
searchInConversation: (id: string) => unknown;
setDisappearingMessages: (id: string, seconds: DurationInSeconds) => void;
setMuteExpiration: (id: string, muteExpiresAt: undefined | number) => unknown;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
showConversation: ShowConversationType;
toggleAboutContactModal: (options: ContactModalStateType) => void;
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;

View File

@@ -16,7 +16,7 @@ import type { BadgeType } from '../../../badges/types.std.js';
import { UserText } from '../../UserText.dom.js';
import { isInSystemContacts } from '../../../util/isInSystemContacts.std.js';
import { InContactsIcon } from '../../InContactsIcon.dom.js';
import type { ContactModalStateType } from '../../../state/ducks/globalModals.preload.js';
import type { ContactModalStateType } from '../../../types/globalModals.std.js';
export type Props = {
areWeASubscriber: boolean;

View File

@@ -18,6 +18,7 @@ import { PanelRow } from './PanelRow.dom.js';
import { PanelSection } from './PanelSection.dom.js';
import { GroupMemberLabel } from '../ContactName.dom.js';
import { AriaClickable } from '../../../axo/AriaClickable.dom.js';
import type { ContactModalStateType } from '../../../types/globalModals.std.js';
export type GroupV2Membership = {
isAdmin: boolean;
@@ -36,7 +37,7 @@ export type Props = {
maxShownMemberCount?: number;
memberships: ReadonlyArray<GroupV2Membership>;
memberColors: Map<string, string>;
showContactModal: (contactId: string, conversationId?: string) => void;
showContactModal: (payload: ContactModalStateType) => void;
showLabelEditor: () => void;
startAddingNewMembers?: () => void;
theme: ThemeType;
@@ -129,7 +130,9 @@ export function ConversationDetailsMembershipList({
return (
<PanelRow
key={member.id}
onClick={() => showContactModal(member.id, conversationId)}
onClick={() =>
showContactModal({ contactId: member.id, conversationId })
}
icon={
<Avatar
conversationType="direct"