Skip to content

Commit

Permalink
[MM-57953] Host controls: Add more host notices (#731)
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

* host notifications -> host notices; add for hostChanged, hostRemoved

* i18n; package-lock? hmm

* package-lock

* continue sending channel_id in payload

* adjust border radius

* icon sizing tweaks

* add 'You are the host'; fix no host message if host rejoins
  • Loading branch information
cpoile authored May 14, 2024
1 parent 79d5418 commit 716fd1d
Show file tree
Hide file tree
Showing 19 changed files with 322 additions and 178 deletions.
6 changes: 4 additions & 2 deletions server/host_controls.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ func (p *Plugin) changeHost(requesterID, channelID, newHostID string) error {
}

p.publishWebSocketEvent(wsEventCallHostChanged, map[string]interface{}{
"hostID": newHostID,
"hostID": newHostID,
"call_id": state.Call.ID,
}, &model.WebsocketBroadcast{ChannelId: channelID, ReliableClusterSend: true})

return nil
Expand Down Expand Up @@ -228,7 +229,8 @@ func (p *Plugin) hostRemoveSession(requesterID, channelID, sessionID string) err
"call_id": state.Call.ID,
"channel_id": channelID,
"session_id": sessionID,
}, &model.WebsocketBroadcast{UserId: ust.UserID, ReliableClusterSend: true})
"user_id": ust.UserID,
}, &model.WebsocketBroadcast{ChannelId: channelID, ReliableClusterSend: true})

go func() {
// Wait a few seconds for the client to end their session cleanly. If they don't (like for an
Expand Down
6 changes: 4 additions & 2 deletions server/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ func (p *Plugin) addUserSession(state *callState, callsEnabled *bool, userID, co
if newHostID := state.getHostID(p.getBotID()); newHostID != state.Call.GetHostID() {
state.Call.Props.Hosts = []string{newHostID}
p.publishWebSocketEvent(wsEventCallHostChanged, map[string]interface{}{
"hostID": newHostID,
"hostID": newHostID,
"call_id": state.Call.ID,
}, &model.WebsocketBroadcast{ChannelId: channelID, ReliableClusterSend: true})
}

Expand Down Expand Up @@ -362,7 +363,8 @@ func (p *Plugin) removeUserSession(state *callState, userID, originalConnID, con
state.Call.Props.Hosts = []string{newHostID}
}
p.publishWebSocketEvent(wsEventCallHostChanged, map[string]interface{}{
"hostID": newHostID,
"hostID": newHostID,
"call_id": state.Call.ID,
}, &model.WebsocketBroadcast{ChannelId: channelID, ReliableClusterSend: true})
}
}
Expand Down
3 changes: 2 additions & 1 deletion standalone/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ export default async function init(cfg: InitConfig) {
handleUserUnraisedHand(store, ev as WebSocketMessage<UserRaiseUnraiseHandData>);
break;
case `custom_${pluginId}_call_host_changed`:
handleCallHostChanged(store, ev as WebSocketMessage<CallHostChangedData>);
// TODO: MM-57919, refactor wsmsg data to calls-common
handleCallHostChanged(store, ev as WebSocketMessage<CallHostChangedData & { call_id: string }>);
break;
case `custom_${pluginId}_user_reacted`:
handleUserReaction(store, ev as WebSocketMessage<UserReactionData>);
Expand Down
5 changes: 4 additions & 1 deletion webapp/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"6XllQM": "Allow microphone access to Mattermost.",
"6aGJYU": "There's no ongoing call in the channel.",
"6cjovA": "You can find the recording in this call's chat thread once it has finished processing.",
"6cy/Zu": "{host} lowered your hand",
"6ytNw2": "Not applicable when the <link>RTCD service URL</link> field is in use.",
"7C5R1Z": "Real-time communication daemon is a service built to offload calls onto your own WebRTC services and efficiently support scalable and secure deployments. <featureLink>Learn more about this feature</featureLink>.",
"7YIAur": "Calls can be recorded for up to {count, plural, =1 {# minute} other {# minutes}}.",
Expand Down Expand Up @@ -71,13 +70,15 @@
"IxKRIN": "Turn off live captions",
"J9GxXI": "Screen recording access is not currently allowed or was cancelled.",
"JPAXWx": "{users} raised a hand",
"JkRNKw": "<b>{name}</b> is now the host",
"KZiF9C": "Try plugging in an audio input device.",
"KaiRbV": "Calls are a quick, audio-first, way to interact with your team. Get the full calls experience when you start a free, 30-day trial.",
"KpV+N+": "Yes, remove",
"M6lXfS": "Set up RTCD services",
"M6nX1N": "Show chat",
"MQr9sh": "Unable to end the call",
"MspN+p": "Unable to start recording",
"MtkpLO": "You are now the host",
"N2IrpM": "Confirm",
"O6EeNO": "Allow call hosts to record meeting video and audio in the cloud. Recording include the entire call window view along with participants' audio track and any shared screen video. <featureLink>Learn more about this feature</featureLink>.",
"OKhRC6": "Share",
Expand Down Expand Up @@ -184,6 +185,7 @@
"sCBCDq": "Stop recording",
"sQHg4y": "Are you sure you want to end a call with {count, plural, =1 {# participant} other {# participants}} in {channelName}?",
"sb3k8n": "Lasted {callDuration}",
"tBbQCQ": "<b>{name}</b> was removed from the call",
"tWDocx": "Remove participant",
"tcxpLX": "No one",
"tjqBPU": "You were removed from the channel",
Expand All @@ -192,6 +194,7 @@
"v5GPHS": "Hide participants list",
"veptjG": "You're already connected to a call in the current channel.",
"vev9kZ": "You're already in a call with {participants}.",
"vp+5Dc": "<b>{host}</b> lowered your hand",
"vxEY29": "Leave call",
"wEGS+o": "You have left the channel, and have been disconnected from the call.",
"x6EHjL": "You don't have permission to start a recording. Please ask the call host to start a recording.",
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/action_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const CALL_REC_PROMPT_DISMISSED = pluginId + '_call_rec_prompt_dismissed'
export const USER_JOINED_TIMEOUT = pluginId + '_user_joined_timeout';
export const LIVE_CAPTION = pluginId + '_live_caption';
export const LIVE_CAPTION_TIMEOUT_EVENT = pluginId + '_live_caption_timeout_event';
export const HOST_CONTROL_NOTIFICATION = pluginId + '_host_control_notification';
export const HOST_CONTROL_NOTIFICATION_TIMEOUT_EVENT = pluginId + '_host_control_notification_timeout_event';
export const HOST_CONTROL_NOTICE = pluginId + '_host_control_notice';
export const HOST_CONTROL_NOTICE_TIMEOUT_EVENT = pluginId + '_host_control_notice_timeout_event';

export const SHOW_EXPANDED_VIEW = pluginId + '_show_expanded_view';
export const HIDE_EXPANDED_VIEW = pluginId + '_hide_expanded_view';
Expand Down
8 changes: 4 additions & 4 deletions webapp/src/components/call_widget/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Avatar from 'src/components/avatar/avatar';
import {Badge} from 'src/components/badge';
import {ParticipantsList} from 'src/components/call_widget/participants_list';
import {RemoveConfirmation} from 'src/components/call_widget/remove_confirmation';
import {HostNotifications} from 'src/components/host_notifications';
import {HostNotices} from 'src/components/host_notices';
import CompassIcon from 'src/components/icons/compassIcon';
import ExpandIcon from 'src/components/icons/expand';
import HorizontalDotsIcon from 'src/components/icons/horizontal_dots';
Expand Down Expand Up @@ -55,7 +55,7 @@ import {
CallAlertStates,
CallAlertStatesDefault,
CallJobReduxState,
HostControlNotification,
HostControlNotice,
IncomingCallNotification,
RemoveConfirmationData,
} from 'src/types/types';
Expand Down Expand Up @@ -96,7 +96,7 @@ interface Props {
left: number,
},
recentlyJoinedUsers: string[],
hostNotifications: HostControlNotification[],
hostNotices: HostControlNotice[],
wider: boolean,
callsIncoming: IncomingCallNotification[],
transcriptionsEnabled: boolean,
Expand Down Expand Up @@ -1563,7 +1563,7 @@ export default class CallWidget extends React.PureComponent<Props, State> {
visible={!this.props.clientConnecting}
isMuted={this.isMuted()}
/>
{this.props.hostNotifications.length > 0 && <HostNotifications onWidget={true}/>}
{this.props.hostNotices.length > 0 && <HostNotices onWidget={true}/>}
{joinedUsers}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/call_widget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
expandedView,
getChannelUrlAndDisplayName,
hostChangeAtForCurrentCall,
hostControlNotificationsForCurrentCall,
hostControlNoticesForCurrentCall,
hostIDForCurrentCall,
profilesInCurrentCallMap,
recentlyJoinedUsersInCurrentCall,
Expand Down Expand Up @@ -64,7 +64,7 @@ const mapStateToProps = (state: GlobalState) => {
allowScreenSharing: allowScreenSharing(state),
show: !expandedView(state),
recentlyJoinedUsers: recentlyJoinedUsersInCurrentCall(state),
hostNotifications: hostControlNotificationsForCurrentCall(state),
hostNotices: hostControlNoticesForCurrentCall(state),
wider: getMyTeams(state)?.length > 1,
callsIncoming: sortedIncomingCalls(state),
transcriptionsEnabled: transcriptionsEnabled(state),
Expand Down
1 change: 1 addition & 0 deletions webapp/src/components/call_widget/participant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export const Participant = ({session, profile, isYou, isHost, iAmHost, isSharing
isMuted={isMuted}
isSharingScreen={isSharingScreen}
isHandRaised={isHandRaised}
isHost={isHost}
onRemove={onRemove}
/>
</StyledDotMenu>
Expand Down
1 change: 1 addition & 0 deletions webapp/src/components/expanded_view/call_participant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export default function CallParticipant({
isMuted={isMuted}
isSharingScreen={isSharingScreen}
isHandRaised={isHandRaised}
isHost={isHost}
onRemove={onRemove}
/>
</StyledDotMenu>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const CallParticipantRHS = ({session, profile, isYou, isHost, iAmHost, isSharing
isMuted={isMuted}
isSharingScreen={isSharingScreen}
isHandRaised={isHandRaised}
isHost={isHost}
onRemove={onRemove}
/>
</StyledDotMenu>
Expand Down
19 changes: 13 additions & 6 deletions webapp/src/components/host_controls_menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Props = {
isMuted: boolean;
isSharingScreen: boolean;
isHandRaised: boolean;
isHost: boolean;
onRemove: () => void;
}

Expand All @@ -23,6 +24,7 @@ export const HostControlsMenu = ({
isMuted,
isSharingScreen,
isHandRaised,
isHost,
onRemove,
}: Props) => {
const {formatMessage} = useIntl();
Expand All @@ -38,7 +40,8 @@ export const HostControlsMenu = ({
</DropdownMenuItem>
);

// TODO: don't show 'make host' for host; keeping for now bc we will show other menu items next
const showingAtLeastOne = !isMuted || isSharingScreen || isHandRaised || !isHost;

return (
<>
{muteUnmute}
Expand All @@ -54,11 +57,15 @@ export const HostControlsMenu = ({
{formatMessage({defaultMessage: 'Lower hand'})}
</DropdownMenuItem>
}
<DropdownMenuItem onClick={() => hostMake(callID, userID)}>
<StyledMonitorAccount/>
{formatMessage({defaultMessage: 'Make host'})}
</DropdownMenuItem>
<DropdownMenuSeparator/>
{!isHost &&
<DropdownMenuItem onClick={() => hostMake(callID, userID)}>
<StyledMonitorAccount/>
{formatMessage({defaultMessage: 'Make host'})}
</DropdownMenuItem>
}
{showingAtLeastOne &&
<DropdownMenuSeparator/>
}
<DropdownMenuItem onClick={onRemove}>
<RedCompassIcon icon={'minus-circle-outline'}/>
<RedText>{formatMessage({defaultMessage: 'Remove from call'})}</RedText>
Expand Down
160 changes: 160 additions & 0 deletions webapp/src/components/host_notices.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import CompassIcon from 'src/components/icons/compassIcon';
import MonitorAccount from 'src/components/icons/monitor_account';
import {HOST_CONTROL_NOTICE_TIMEOUT} from 'src/constants';
import {hostControlNoticesForCurrentCall} from 'src/selectors';
import {HostControlNoticeType} from 'src/types/types';
import styled, {css, keyframes} from 'styled-components';

type Props = {
onWidget?: boolean;
}

export const HostNotices = ({onWidget = false}: Props) => {
const currentUserId = useSelector(getCurrentUserId);
const notices = useSelector(hostControlNoticesForCurrentCall);

const youAreHostMsg = <FormattedMessage defaultMessage={'You are now the host'}/>;

return (
<>
{notices.map((n) => {
switch (n.type) {
case HostControlNoticeType.LowerHand:
return (
<Notice
key={n.noticeID}
$onWidget={onWidget}
>
<StyledCompassIcon
icon={'hand-right-outline-off'}
$onWidget={onWidget}
/>
<Text $onWidget={onWidget}>
<FormattedMessage
defaultMessage={'<b>{host}</b> lowered your hand'}
values={{
b: (text: string) => <b>{text}</b>,
host: n.displayName,
}}
/>
</Text>
</Notice>
);
case HostControlNoticeType.HostChanged:
return (
<Notice
key={n.noticeID}
$onWidget={onWidget}
>
<StyledMonitorAccount $onWidget={onWidget}/>
<Text $onWidget={onWidget}>
{n.userID === currentUserId ? youAreHostMsg : (
<FormattedMessage
defaultMessage={'<b>{name}</b> is now the host'}
values={{
b: (text: string) => <b>{text}</b>,
name: n.displayName,
}}
/>)
}
</Text>
</Notice>
);
case HostControlNoticeType.HostRemoved:
return (
<Notice
key={n.noticeID}
$onWidget={onWidget}
>
<RedStyledCompassIcon
icon={'minus-circle-outline'}
$onWidget={onWidget}
/>
<Text $onWidget={onWidget}>
<FormattedMessage
defaultMessage={'<b>{name}</b> was removed from the call'}
values={{
b: (text: string) => <b>{text}</b>,
name: n.displayName,
}}
/>
</Text>
</Notice>
);
default:
return null;
}
})}
</>
);
};

const slideInAnimation = keyframes`
0%, 100% {
transform: translateY(100%);
opacity: 0;
}
10% {
transform: translateY(0);
opacity: 1;
}
90% {
transform: translateY(0);
opacity: 1;
}
`;

const Notice = styled.div<{ $onWidget?: boolean }>`
animation: ${slideInAnimation} ${HOST_CONTROL_NOTICE_TIMEOUT}ms ease-in-out 0.2s both;
display: flex;
align-items: center;
padding: 6px 16px 6px 8px;
gap: 8px;
border-radius: 16px;
width: fit-content;
font-weight: 600;
font-size: 14px;
line-height: 20px;
background: #FFFFFF;
${({$onWidget}) => $onWidget && css`
width: 100%;
border-radius: 8px;
padding: 4px 6px;
font-size: 11px;
font-weight: 400;
white-space: pre;
`}
`;

const StyledCompassIcon = styled(CompassIcon)<{ $onWidget?: boolean }>`
color: var(--away-indicator);
font-size: ${({$onWidget}) => ($onWidget ? 16 : 18)}px;
margin-right: ${({$onWidget}) => ($onWidget ? -4 : -5)}px;
margin-left: -3px;
`;

const RedStyledCompassIcon = styled(StyledCompassIcon)`
color: var(--dnd-indicator);
`;

export const StyledMonitorAccount = styled(MonitorAccount)<{ $onWidget?: boolean }>`
flex: none;
margin-left: ${({$onWidget}) => ($onWidget ? 1 : 0)}px;
margin-top: 1px;
fill: var(--center-channel-color-64);
width: ${({$onWidget}) => ($onWidget ? 14 : 18)}px;
`;

const Text = styled.span<{ $onWidget?: boolean }>`
color: var(--center-channel-color);
${({$onWidget}) => $onWidget && css`
overflow: hidden;
text-overflow: ellipsis;
`}
`;
Loading

0 comments on commit 716fd1d

Please sign in to comment.