Move call buttons to the room header

This is to make some room in the composer for voice messages. The hangup behaviour is intentionally lost by this change as the VOIP UX is intended to rely on dedicated hangup buttons instead.
This commit is contained in:
Travis Ralston 2021-02-26 13:46:39 -07:00
parent f1330b7359
commit 4e27b00cf3
6 changed files with 45 additions and 134 deletions

View file

@ -227,18 +227,6 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
} }
.mx_MessageComposer_hangup::before {
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
}
.mx_MessageComposer_voicecall::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
.mx_MessageComposer_videocall::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
.mx_MessageComposer_emoji::before { .mx_MessageComposer_emoji::before {
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg'); mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
} }

View file

@ -252,6 +252,19 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/search-inset.svg'); mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
} }
.mx_RoomHeader_voiceCallButton::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
// The call button SVG is padded slightly differently, so match it up to the size
// of the other icons
mask-size: 20px;
mask-position: center;
}
.mx_RoomHeader_videoCallButton::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
.mx_RoomHeader_showPanel { .mx_RoomHeader_showPanel {
height: 16px; height: 16px;
} }

View file

@ -34,7 +34,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages'; import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal'; import Modal from '../../Modal';
import * as sdk from '../../index'; import * as sdk from '../../index';
import CallHandler from '../../CallHandler'; import CallHandler, { PlaceCallType } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import Tinter from '../../Tinter'; import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc'; import rateLimitedFunc from '../../ratelimitedfunc';
@ -1352,6 +1352,14 @@ export default class RoomView extends React.Component<IProps, IState> {
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned); SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
}; };
private onCallPlaced = (type: PlaceCallType) => {
dis.dispatch({
action: 'place_call',
type: type,
room_id: this.state.room.roomId,
});
};
private onSettingsClick = () => { private onSettingsClick = () => {
dis.dispatch({ action: "open_room_settings" }); dis.dispatch({ action: "open_room_settings" });
}; };
@ -2031,6 +2039,7 @@ export default class RoomView extends React.Component<IProps, IState> {
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null} onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
appsShown={this.state.showApps} appsShown={this.state.showApps}
onCallPlaced={this.onCallPlaced}
/> />
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}> <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className="mx_RoomView_body"> <div className="mx_RoomView_body">

View file

@ -50,97 +50,6 @@ ComposerAvatar.propTypes = {
me: PropTypes.object.isRequired, me: PropTypes.object.isRequired,
}; };
function CallButton(props) {
const onVoiceCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
type: PlaceCallType.Voice,
room_id: props.roomId,
});
};
return (<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_voicecall"
onClick={onVoiceCallClick}
title={_t('Voice call')}
/>);
}
CallButton.propTypes = {
roomId: PropTypes.string.isRequired,
};
function VideoCallButton(props) {
const onCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video,
room_id: props.roomId,
});
};
return <AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_videocall"
onClick={onCallClick}
title={_t('Video call')}
/>;
}
VideoCallButton.propTypes = {
roomId: PropTypes.string.isRequired,
};
function HangupButton(props) {
const onHangupClick = () => {
if (props.isConference) {
dis.dispatch({
action: props.canEndConference ? 'end_conference' : 'hangup_conference',
room_id: props.roomId,
});
return;
}
const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
if (!call) {
return;
}
const action = call.state === CallState.Ringing ? 'reject' : 'hangup';
dis.dispatch({
action,
// hangup the call for this room. NB. We use the room in props as the room ID
// as call.roomId may be the 'virtual room', and the dispatch actions always
// use the user-facing room (there was a time when we deliberately used
// call.roomId and *not* props.roomId, but that was for the old
// style Freeswitch conference calls and those times are gone.)
room_id: props.roomId,
});
};
let tooltip = _t("Hangup");
if (props.isConference && props.canEndConference) {
tooltip = _t("End conference");
}
const canLeaveConference = !props.isConference ? true : props.isInConference;
return (
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_hangup"
onClick={onHangupClick}
title={tooltip}
disabled={!canLeaveConference}
/>
);
}
HangupButton.propTypes = {
roomId: PropTypes.string.isRequired,
isConference: PropTypes.bool.isRequired,
canEndConference: PropTypes.bool,
isInConference: PropTypes.bool,
};
const EmojiButton = ({addEmoji}) => { const EmojiButton = ({addEmoji}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -265,7 +174,6 @@ export default class MessageComposer extends React.Component {
this.state = { this.state = {
tombstone: this._getRoomTombstone(), tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(), canSendMessages: this.props.room.maySendMessage(),
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room), hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room), joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
}; };
@ -405,12 +313,7 @@ export default class MessageComposer extends React.Component {
]; ];
if (!this.state.tombstone && this.state.canSendMessages) { if (!this.state.tombstone && this.state.canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer"); const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer");
const callInProgress = this.props.callState && this.props.callState !== 'ended';
controls.push( controls.push(
<SendMessageComposer <SendMessageComposer
@ -430,30 +333,6 @@ export default class MessageComposer extends React.Component {
SettingsStore.getValue("MessageComposerInput.showStickersButton")) { SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />); controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
} }
if (this.state.showCallButtons) {
if (this.state.hasConference) {
const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
controls.push(
<HangupButton
key="controls_hangup"
roomId={this.props.room.roomId}
isConference={true}
canEndConference={canEndConf}
isInConference={this.state.joinedConference}
/>,
);
} else if (callInProgress) {
controls.push(
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} isConference={false} />,
);
} else {
controls.push(
<CallButton key="controls_call" roomId={this.props.room.roomId} />,
<VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
);
}
}
} else if (this.state.tombstone) { } else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];

View file

@ -31,6 +31,7 @@ import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic"; import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import {PlaceCallType} from "../../../CallHandler";
export default class RoomHeader extends React.Component { export default class RoomHeader extends React.Component {
static propTypes = { static propTypes = {
@ -45,6 +46,7 @@ export default class RoomHeader extends React.Component {
e2eStatus: PropTypes.string, e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func, onAppsClick: PropTypes.func,
appsShown: PropTypes.bool, appsShown: PropTypes.bool,
onCallPlaced: PropTypes.func, // (PlaceCallType) => void;
}; };
static defaultProps = { static defaultProps = {
@ -226,8 +228,26 @@ export default class RoomHeader extends React.Component {
title={_t("Search")} />; title={_t("Search")} />;
} }
let voiceCallButton;
let videoCallButton;
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
voiceCallButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
title={_t("Voice call")} />;
videoCallButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev) => this.props.onCallPlaced(
ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
title={_t("Video call")} />;
}
const rightRow = const rightRow =
<div className="mx_RoomHeader_buttons"> <div className="mx_RoomHeader_buttons">
{ videoCallButton }
{ voiceCallButton }
{ pinnedEventsButton } { pinnedEventsButton }
{ forgetButton } { forgetButton }
{ appsButton } { appsButton }

View file

@ -632,6 +632,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: 3000, default: 3000,
}, },
"showCallButtonsInComposer": { "showCallButtonsInComposer": {
// Dev note: This is no longer "in composer" but is instead "in room header".
// TODO: Rename with settings v3
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: true, default: true,
controller: new UIFeatureController(UIFeature.Voip), controller: new UIFeatureController(UIFeature.Voip),