diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 97f9e5a22d..672ad5c2b8 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -6200,6 +6200,18 @@ "messageformat": "Voice", "description": "Text for button to start a new voice call in the Contact Details modal" }, + "icu:ContactModal--mute-audio": { + "messageformat": "Mute audio", + "description": "Button text to mute another call participant's audio from the Contact Details modal when in a group or call link call" + }, + "icu:ContactModal--confirm-mute-body": { + "messageformat": "Are you sure you want to mute {contact}?", + "description": "Confirm dialog body when muting another call participant's audio from the Contact Details modal in a group or call link call" + }, + "icu:ContactModal--confirm-mute-primary-button": { + "messageformat": "Mute", + "description": "Confirm dialog primary button text when muting another call participant's audio from the Contact Details modal in a group or call link call" + }, "icu:GroupMemberLabelInfoModal--title": { "messageformat": "Member labels", "description": "Title of explainer dialog you see when clicking member label in Contact Modal" diff --git a/stylesheets/components/ContactModal.scss b/stylesheets/components/ContactModal.scss index c5dfac3c0a..f12c4024cc 100644 --- a/stylesheets/components/ContactModal.scss +++ b/stylesheets/components/ContactModal.scss @@ -126,6 +126,10 @@ background-color: variables.$color-gray-80; } } + + &[disabled] { + opacity: 0.5; + } } &__bubble-icon { diff --git a/ts/components/CallLinkPendingParticipantModal.dom.tsx b/ts/components/CallLinkPendingParticipantModal.dom.tsx index 5715fb3142..0306cc2b7d 100644 --- a/ts/components/CallLinkPendingParticipantModal.dom.tsx +++ b/ts/components/CallLinkPendingParticipantModal.dom.tsx @@ -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; diff --git a/ts/components/CallManager.dom.tsx b/ts/components/CallManager.dom.tsx index 9e1f19c548..d4af8cfd42 100644 --- a/ts/components/CallManager.dom.tsx +++ b/ts/components/CallManager.dom.tsx @@ -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 ) => React.JSX.Element; - showContactModal: (contactId: string, conversationId?: string) => void; + showContactModal: (payload: ContactModalStateType) => void; startCall: (payload: StartCallType) => void; toggleParticipants: () => void; acceptCall: (_: AcceptCallType) => void; diff --git a/ts/components/CallingAdhocCallInfo.dom.tsx b/ts/components/CallingAdhocCallInfo.dom.tsx index bc97915f60..983a2d68a1 100644 --- a/ts/components/CallingAdhocCallInfo.dom.tsx +++ b/ts/components/CallingAdhocCallInfo.dom.tsx @@ -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" > diff --git a/ts/components/CallingParticipantsList.dom.tsx b/ts/components/CallingParticipantsList.dom.tsx index e7073cda48..07040cd23f 100644 --- a/ts/components/CallingParticipantsList.dom.tsx +++ b/ts/components/CallingParticipantsList.dom.tsx @@ -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; - 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" > diff --git a/ts/components/GlobalModalContainer.dom.tsx b/ts/components/GlobalModalContainer.dom.tsx index 108de4927a..bceb4bbaff 100644 --- a/ts/components/GlobalModalContainer.dom.tsx +++ b/ts/components/GlobalModalContainer.dom.tsx @@ -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'; diff --git a/ts/components/StoryViewer.dom.tsx b/ts/components/StoryViewer.dom.tsx index e50e6c7afe..1f3ebfaa60 100644 --- a/ts/components/StoryViewer.dom.tsx +++ b/ts/components/StoryViewer.dom.tsx @@ -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; diff --git a/ts/components/StoryViewsNRepliesModal.dom.tsx b/ts/components/StoryViewsNRepliesModal.dom.tsx index 3d926c3136..26ba0b823c 100644 --- a/ts/components/StoryViewsNRepliesModal.dom.tsx +++ b/ts/components/StoryViewsNRepliesModal.dom.tsx @@ -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; replies: ReadonlyArray; - showContactModal: (contactId: string, conversationId?: string) => void; + showContactModal: (payload: ContactModalStateType) => void; emojiSkinToneDefault: EmojiSkinTone | null; sortedGroupMembers?: ReadonlyArray; views: ReadonlyArray; @@ -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) => void; }; diff --git a/ts/components/conversation/ContactModal.dom.stories.tsx b/ts/components/conversation/ContactModal.dom.stories.tsx index e90a13a21b..ad763c59a3 100644 --- a/ts/components/conversation/ContactModal.dom.stories.tsx +++ b/ts/components/conversation/ContactModal.dom.stories.tsx @@ -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, +}; diff --git a/ts/components/conversation/ContactModal.dom.tsx b/ts/components/conversation/ContactModal.dom.tsx index ee2e320068..4821373542 100644 --- a/ts/components/conversation/ContactModal.dom.tsx +++ b/ts/components/conversation/ContactModal.dom.tsx @@ -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; @@ -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({ ); break; + case SubModalState.ConfirmingMute: + modalNode = ( + { + 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, + })} + + ); + break; default: { const state: never = subModalState; log.warn(`unexpected ${state}!`); @@ -530,6 +567,19 @@ export function ContactModal({ )} + {isRemoteMuteVisible && ( + + )} {modalNode} diff --git a/ts/components/conversation/ContactSpoofingReviewDialog.dom.tsx b/ts/components/conversation/ContactSpoofingReviewDialog.dom.tsx index f93e693f59..851ce21bba 100644 --- a/ts/components/conversation/ContactSpoofingReviewDialog.dom.tsx +++ b/ts/components/conversation/ContactSpoofingReviewDialog.dom.tsx @@ -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} > diff --git a/ts/components/conversation/ConversationHero.dom.tsx b/ts/components/conversation/ConversationHero.dom.tsx index 888bc11624..ac0452b471 100644 --- a/ts/components/conversation/ConversationHero.dom.tsx +++ b/ts/components/conversation/ConversationHero.dom.tsx @@ -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; diff --git a/ts/components/conversation/Message.dom.tsx b/ts/components/conversation/Message.dom.tsx index f4681c8499..2e5fd507b8 100644 --- a/ts/components/conversation/Message.dom.tsx +++ b/ts/components/conversation/Message.dom.tsx @@ -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; }) => void; endPoll: (messageId: string) => void; - showContactModal: (contactId: string, conversationId?: string) => void; + showContactModal: (payload: ContactModalStateType) => void; showSpoiler: (messageId: string, data: Record) => void; cancelAttachmentDownload: (options: { messageId: string }) => void; @@ -2330,7 +2331,7 @@ export class Message extends React.PureComponent { event.stopPropagation(); event.preventDefault(); - showContactModal(author.id, conversationId); + showContactModal({ contactId: author.id, conversationId }); }} phoneNumber={author.phoneNumber} profileName={author.profileName} diff --git a/ts/components/conversation/TypingBubble.dom.tsx b/ts/components/conversation/TypingBubble.dom.tsx index b97d374034..cb752e6e24 100644 --- a/ts/components/conversation/TypingBubble.dom.tsx +++ b/ts/components/conversation/TypingBubble.dom.tsx @@ -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} diff --git a/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx b/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx index b4c71768ee..cc8fbe3260 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx @@ -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; diff --git a/ts/components/conversation/conversation-details/ConversationDetailsHeader.dom.tsx b/ts/components/conversation/conversation-details/ConversationDetailsHeader.dom.tsx index 2d7be87067..f9bab8b3e5 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsHeader.dom.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsHeader.dom.tsx @@ -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; diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx index c008a35da4..6b1789a6dd 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx @@ -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; memberColors: Map; - showContactModal: (contactId: string, conversationId?: string) => void; + showContactModal: (payload: ContactModalStateType) => void; showLabelEditor: () => void; startAddingNewMembers?: () => void; theme: ThemeType; @@ -129,7 +130,9 @@ export function ConversationDetailsMembershipList({ return ( showContactModal(member.id, conversationId)} + onClick={() => + showContactModal({ contactId: member.id, conversationId }) + } icon={ { + return (dispatch, getState) => { + const state = getState(); + const activeCall = getActiveCall(state.calling); + if (!isGroupOrAdhocCallState(activeCall)) { + log.warn( + 'sendRemoteMute: Trying to remote mute without active group or adhoc call' + ); + return; + } + + calling.sendRemoteMute(activeCall.conversationId, demuxId); + dispatch({ + type: 'NOOP', + payload: null, + }); + }; +} + function callStateChange( payload: CallStateChangeType ): ThunkAction< @@ -3071,6 +3093,7 @@ export const actions = { returnToActiveCall, sendGroupCallRaiseHand, sendGroupCallReaction, + sendRemoteMute, selectPresentingSource, setGroupCallVideoRequest, setIsCallActive, diff --git a/ts/state/ducks/globalModals.preload.ts b/ts/state/ducks/globalModals.preload.ts index b97cc6a0f4..895bec34f4 100644 --- a/ts/state/ducks/globalModals.preload.ts +++ b/ts/state/ducks/globalModals.preload.ts @@ -22,6 +22,7 @@ import type { StateType as RootStateType } from '../reducer.preload.js'; import * as SingleServePromise from '../../services/singleServePromise.std.js'; import { isKeyTransparencyAvailable } from '../../services/keyTransparency.preload.js'; import * as Stickers from '../../types/Stickers.preload.js'; +import type { ContactModalStateType } from '../../types/globalModals.std.js'; import { UsernameOnboardingState } from '../../types/globalModals.std.js'; import { createLogger } from '../../logging/log.std.js'; import { @@ -257,11 +258,6 @@ const HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL = 'globalModals/HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL'; const TOGGLE_PIN_MESSAGE_DIALOG = 'globalModals/TOGGLE_PIN_MESSAGE_DIALOG'; -export type ContactModalStateType = ReadonlyDeep<{ - contactId: string; - conversationId?: string; -}>; - export type UserNotFoundModalStateType = ReadonlyDeep< | { type: 'phoneNumber'; @@ -738,13 +734,19 @@ function hideContactModal(): HideContactModalActionType { }; } -function showContactModal( - contactId: string, - conversationId?: string -): ShowContactModalActionType { +function showContactModal({ + activeCallDemuxId, + contactId, + conversationId, +}: { + contactId: string; + conversationId?: string; + activeCallDemuxId?: number; +}): ShowContactModalActionType { return { type: SHOW_CONTACT_MODAL, payload: { + activeCallDemuxId, contactId, conversationId, }, diff --git a/ts/state/selectors/calling.std.ts b/ts/state/selectors/calling.std.ts index 631e0e6d21..c210cd5a7a 100644 --- a/ts/state/selectors/calling.std.ts +++ b/ts/state/selectors/calling.std.ts @@ -12,6 +12,7 @@ import type { DirectCallStateType, GroupCallStateType, ActiveCallStateType, + GroupCallParticipantInfoType, } from '../ducks/calling.preload.js'; import { getRingingCall as getRingingCallHelper } from '../ducks/callingHelpers.std.js'; import type { PresentedSource } from '../../types/Calling.std.js'; @@ -23,6 +24,7 @@ import { import { getUserACI } from './user.std.js'; import { getOwn } from '../../util/getOwn.std.js'; import type { AciString } from '../../types/ServiceId.std.js'; +import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall.std.js'; export type CallStateType = DirectCallStateType | GroupCallStateType; @@ -183,3 +185,21 @@ export const getPresentingSource = createSelector( (activeCallState): PresentedSource | undefined => activeCallState?.presentingSource ); + +type ParticipantByDemuxIdInCallSelectorType = ( + demuxId: number | undefined +) => GroupCallParticipantInfoType | undefined; + +export const getParticipantInActiveCall = createSelector( + getActiveCall, + (call: CallStateType | undefined): ParticipantByDemuxIdInCallSelectorType => + (demuxId: number | undefined): GroupCallParticipantInfoType | undefined => { + if (demuxId == null || !isGroupOrAdhocCallState(call)) { + return undefined; + } + + return call.remoteParticipants.find( + participant => participant.demuxId === demuxId + ); + } +); diff --git a/ts/state/smart/ContactModal.preload.tsx b/ts/state/smart/ContactModal.preload.tsx index 44e084631c..2de67e40b4 100644 --- a/ts/state/smart/ContactModal.preload.tsx +++ b/ts/state/smart/ContactModal.preload.tsx @@ -15,6 +15,7 @@ import { getHasStoriesSelector } from '../selectors/stories2.dom.js'; import { getActiveCallState, isInFullScreenCall as getIsInFullScreenCall, + getParticipantInActiveCall, } from '../selectors/calling.std.js'; import { useStoriesActions } from '../ducks/stories.preload.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; @@ -26,11 +27,18 @@ import { strictAssert } from '../../util/assert.std.js'; export const SmartContactModal = memo(function SmartContactModal() { const i18n = useSelector(getIntl); const theme = useSelector(getTheme); - const { conversationId, contactId } = useSelector(getContactModalState) ?? {}; + const { conversationId, contactId, activeCallDemuxId } = + useSelector(getContactModalState) ?? {}; const conversationSelector = useSelector(getConversationSelector); const hasStoriesSelector = useSelector(getHasStoriesSelector); + const activeCallState = useSelector(getActiveCallState); const isInFullScreenCall = useSelector(getIsInFullScreenCall); + const getCallParticipant = useSelector(getParticipantInActiveCall); + const callParticipant = getCallParticipant(activeCallDemuxId); + const isRemoteMuteVisible = Boolean(callParticipant); + const isMuted = !callParticipant?.hasRemoteAudio; + const badgesSelector = useSelector(getBadgesSelector); const areWeASubscriber = useSelector(getAreWeASubscriber); @@ -78,6 +86,7 @@ export const SmartContactModal = memo(function SmartContactModal() { onOutgoingVideoCallInConversation, onOutgoingAudioCallInConversation, togglePip, + sendRemoteMute, } = useCallingActions(); const handleOpenEditNicknameAndNoteModal = useCallback(() => { @@ -103,10 +112,14 @@ export const SmartContactModal = memo(function SmartContactModal() { isAdmin={isAdmin} isInFullScreenCall={isInFullScreenCall} isMember={isMember} + isMuted={isMuted} + isRemoteMuteVisible={isRemoteMuteVisible} + activeCallDemuxId={activeCallDemuxId} onOpenEditNicknameAndNoteModal={handleOpenEditNicknameAndNoteModal} onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation} onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation} removeMemberFromGroup={removeMemberFromGroup} + sendRemoteMute={sendRemoteMute} showConversation={showConversation} startAvatarDownload={() => startAvatarDownload(contact.id)} theme={theme} diff --git a/ts/state/smart/ContactName.preload.tsx b/ts/state/smart/ContactName.preload.tsx index dae869156e..7afcee98a0 100644 --- a/ts/state/smart/ContactName.preload.tsx +++ b/ts/state/smart/ContactName.preload.tsx @@ -17,7 +17,7 @@ export const SmartContactName = memo(function SmartContactName({ }: ExternalProps) { const i18n = useSelector(getIntl); const getConversation = useSelector(getConversationSelector); - const currentConversationId = useSelector(getSelectedConversationId); + const conversationId = useSelector(getSelectedConversationId); const { showContactModal } = useGlobalModalActions(); @@ -26,8 +26,8 @@ export const SmartContactName = memo(function SmartContactName({ }, [getConversation, contactId]); const handleClick = useCallback(() => { - showContactModal(contactId, currentConversationId); - }, [showContactModal, contactId, currentConversationId]); + showContactModal({ contactId, conversationId }); + }, [showContactModal, contactId, conversationId]); return ( ;