Skip to content

Commit

Permalink
[MM-57470] Host controls: Mute all (#726)
Browse files Browse the repository at this point in the history
* i18n

* host mute all

* add host check

* check for hostControlsAvailable

* api error changes

* mute-all -> mute-others

* rhs header tweaks

* add todo for MM-53455
  • Loading branch information
cpoile authored May 10, 2024
1 parent 8f36a15 commit 9f7b047
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 6 deletions.
1 change: 1 addition & 0 deletions server/api_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func (p *Plugin) newAPIRouter() *mux.Router {
hostCtrlRouter.HandleFunc("/screen-off", p.handleScreenOff).Methods("POST")
hostCtrlRouter.HandleFunc("/lower-hand", p.handleLowerHand).Methods("POST")
hostCtrlRouter.HandleFunc("/remove", p.handleRemoveSession).Methods("POST")
hostCtrlRouter.HandleFunc("/mute-others", p.handleMuteOthers).Methods("POST")

// Bot
botRouter := router.PathPrefix("/bot").Subrouter()
Expand Down
31 changes: 31 additions & 0 deletions server/host_controls.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,37 @@ func (p *Plugin) muteSession(requesterID, channelID, sessionID string) error {
return nil
}

func (p *Plugin) muteOthers(requesterID, channelID string) error {
state, err := p.getCallState(channelID, false)
if err != nil {
return err
}

if state == nil {
return ErrNoCallOngoing
}

if requesterID != state.Call.GetHostID() {
if isAdmin := p.API.HasPermissionTo(requesterID, model.PermissionManageSystem); !isAdmin {
return ErrNoPermissions
}
}

// TODO: MM-53455 - send as a list
// Unmute anyone muted (who is not the host/requester).
// If there are no unmuted sessions, return without doing anything.
for id, s := range state.sessions {
if s.Unmuted && s.UserID != requesterID {
p.publishWebSocketEvent(wsEventHostMute, map[string]interface{}{
"channel_id": channelID,
"session_id": id,
}, &model.WebsocketBroadcast{UserId: s.UserID, ReliableClusterSend: true})
}
}

return nil
}

func (p *Plugin) screenOff(requesterID, channelID, sessionID string) error {
state, err := p.getCallState(channelID, false)
if err != nil {
Expand Down
16 changes: 16 additions & 0 deletions server/host_controls_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ func (p *Plugin) handleMuteSession(w http.ResponseWriter, r *http.Request) {
res.Msg = "success"
}

func (p *Plugin) handleMuteOthers(w http.ResponseWriter, r *http.Request) {
var res httpResponse
defer p.httpAudit("handleMuteOthers", &res, w, r)

userID := r.Header.Get("Mattermost-User-Id")
callID := mux.Vars(r)["call_id"]

if err := p.muteOthers(userID, callID); err != nil {
p.handleHostControlsError(err, &res, "handleMuteOthers")
return
}

res.Code = http.StatusOK
res.Msg = "success"
}

func (p *Plugin) handleScreenOff(w http.ResponseWriter, r *http.Request) {
var res httpResponse
defer p.httpAudit("handleScreenOff", &res, w, r)
Expand Down
1 change: 1 addition & 0 deletions webapp/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
"lhEBhE": "Looks like something went wrong with calls. You can restart the app and try again.",
"lmIKQg": "(you)",
"lr1SOF": "<b>{callerName}</b> is inviting you to a call with <b>{others}</b>",
"mMHaeQ": "Mute others",
"mRqfP4": "Choose what to share",
"mqXYm4": "At the moment, {count} is the participant limit for cloud calls.",
"n1Ubod": "Enable call recordings",
Expand Down
11 changes: 11 additions & 0 deletions webapp/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,14 @@ export const hostRemove = async (callID?: string, sessionID?: string) => {
},
);
};

export const hostMuteOthers = async (callID?: string) => {
if (!callID) {
return {};
}

return RestClient.fetch(
`${getPluginPath()}/calls/${callID}/host/mute-others`,
{method: 'post'},
);
};
43 changes: 41 additions & 2 deletions webapp/src/components/call_widget/participants_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {UserProfile} from '@mattermost/types/users';
import {IDMappedObjects} from '@mattermost/types/utilities';
import React from 'react';
import {useIntl} from 'react-intl';
import {hostMuteOthers} from 'src/actions';
import {Participant} from 'src/components/call_widget/participant';
import {useHostControls} from 'src/components/expanded_view/hooks';
import CompassIcon from 'src/components/icons/compassIcon';
import styled from 'styled-components';

type Props = {
sessions: UserSessionState[];
Expand All @@ -27,6 +31,9 @@ export const ParticipantsList = ({
callID,
}: Props) => {
const {formatMessage} = useIntl();
const isHost = currentSession?.user_id === callHostID;
const {hostControlsAvailable} = useHostControls(false, false, isHost);
const showMuteOthers = hostControlsAvailable && sessions.some((s) => s.unmuted && s.user_id !== currentSession?.user_id);

const renderParticipants = () => {
return sessions.map((session) => (
Expand All @@ -36,7 +43,7 @@ export const ParticipantsList = ({
profile={profiles[session.user_id]}
isYou={session.session_id === currentSession?.session_id}
isHost={callHostID === session.user_id}
iAmHost={currentSession?.user_id === callHostID}
iAmHost={isHost}
isSharingScreen={screenSharingSession?.session_id === session.session_id}
callID={callID}
onRemove={() => onRemove(session.session_id, session.user_id)}
Expand All @@ -59,6 +66,12 @@ export const ParticipantsList = ({
style={styles.participantsListHeader}
>
{formatMessage({defaultMessage: 'Participants'})}
{showMuteOthers &&
<MuteOthersButton onClick={() => hostMuteOthers(callID)}>
<CompassIcon icon={'microphone-off'}/>
{formatMessage({defaultMessage: 'Mute others'})}
</MuteOthersButton>
}
</li>
{renderParticipants()}
</ul>
Expand Down Expand Up @@ -86,11 +99,37 @@ const styles: Record<string, React.CSSProperties> = ({
position: 'sticky',
top: '0',
transform: 'translateY(-8px)',
paddingTop: '16px',
padding: '8px 0 0 20px',
color: 'var(--center-channel-color)',
background: 'var(--center-channel-bg)',

/* @ts-ignore */
appRegion: 'drag',
},
});

const MuteOthersButton = styled.button`
display: flex;
padding: 4px 10px;
margin-right: 8px;
margin-left: auto;
gap: 2px;
font-family: 'Open Sans', sans-serif;
font-size: 11px;
font-weight: 600;
line-height: 16px;
color: var(--button-bg);
border: none;
background: none;
border-radius: 4px;
&:hover {
// thanks style sheets...
background: rgba(var(--button-bg-rgb), 0.08) !important;
}
i {
font-size: 14px;
}
`;
44 changes: 41 additions & 3 deletions webapp/src/components/expanded_view/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import {IntlShape} from 'react-intl';
import {RouteComponentProps} from 'react-router-dom';
import {compareSemVer} from 'semver-parser';
import {hostRemove, stopCallRecording} from 'src/actions';
import {hostMuteOthers, hostRemove, stopCallRecording} from 'src/actions';
import {Badge} from 'src/components/badge';
import CallDuration from 'src/components/call_widget/call_duration';
import CallParticipantRHS from 'src/components/expanded_view/call_participant_rhs';
Expand Down Expand Up @@ -118,6 +118,8 @@ interface Props extends RouteComponentProps {
recordingPromptDismissedAt: (callID: string, dismissedAt: number) => void,
transcriptionsEnabled: boolean,
liveCaptionsAvailable: boolean,
isAdmin: boolean,
hostControlsAllowed: boolean,
}

interface State {
Expand Down Expand Up @@ -1034,6 +1036,8 @@ export default class ExpandedView extends React.PureComponent<Props, State> {
const isChatUnread = Boolean(this.props.threadUnreadReplies);

const isHost = this.props.callHostID === this.props.currentUserID;
const hostControlsAvailable = this.props.hostControlsAllowed && (isHost || this.props.isAdmin);
const showMuteOthers = hostControlsAvailable && this.props.sessions.some((s) => s.unmuted && s.user_id !== this.props.currentUserID);

const isRecording = isHost && this.props.isRecording;
const showCCButton = this.props.liveCaptionsAvailable;
Expand Down Expand Up @@ -1268,6 +1272,13 @@ export default class ExpandedView extends React.PureComponent<Props, State> {
<div style={this.style.rhsHeaderContainer}>
<div style={this.style.rhsHeader}>
<span>{formatMessage({defaultMessage: 'Participants'})}</span>
<ToTheRight/>
{showMuteOthers &&
<MuteOthersButton onClick={() => hostMuteOthers(this.props.channel?.id)}>
<CompassIcon icon={'microphone-off'}/>
{formatMessage({defaultMessage: 'Mute others'})}
</MuteOthersButton>
}
<CloseButton
className='style--none'
onClick={() => this.onParticipantsListToggle()}
Expand Down Expand Up @@ -1371,17 +1382,44 @@ const ExpandedViewGlobalsStyle = createGlobalStyle<{ callThreadSelected: boolean
}
`;

const ToTheRight = styled.div`
margin-left: auto;
`;

const MuteOthersButton = styled.button`
display: flex;
padding: 8px 8px;
margin-right: 6px;
gap: 2px;
font-family: 'Open Sans', sans-serif;
font-size: 11px;
font-weight: 600;
line-height: 16px;
color: var(--button-bg);
border: none;
background: none;
border-radius: 4px;
&:hover {
background: rgba(var(--button-bg-rgb), 0.08);
}
i {
font-size: 14px;
}
`;

const CloseButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
color: rgba(var(--center-channel-color-rgb), 0.56);
width: 32px;
height: 32px;
border-radius: 4px;
:hover {
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.72);
fill: rgba(var(--center-channel-color-rgb), 0.72);
Expand Down
5 changes: 4 additions & 1 deletion webapp/src/components/expanded_view/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {GlobalState} from '@mattermost/types/store';
import {getCurrentTeamId, getTeam} from 'mattermost-redux/selectors/entities/teams';
import {getThread} from 'mattermost-redux/selectors/entities/threads';
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {getCurrentUserId, getUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {connect} from 'react-redux';
import {withRouter} from 'react-router-dom';
import {
Expand All @@ -14,6 +14,7 @@ import {
} from 'src/actions';
import {
allowScreenSharing,
areHostControlsAllowed,
areLiveCaptionsAvailableInCurrentCall,
callStartAtForCurrentCall,
channelForCurrentCall,
Expand Down Expand Up @@ -89,6 +90,8 @@ const mapStateToProps = (state: GlobalState) => {
recordingMaxDuration: recordingMaxDuration(state),
transcriptionsEnabled: transcriptionsEnabled(state),
liveCaptionsAvailable: areLiveCaptionsAvailableInCurrentCall(state),
isAdmin: isCurrentUserSystemAdmin(state),
hostControlsAllowed: areHostControlsAllowed(state),
};
};

Expand Down

0 comments on commit 9f7b047

Please sign in to comment.