Merge pull request #5223 from matrix-org/travis/ft-sep1620/04-jitsi-hangup

Make the hangup button do things for conference calls
This commit is contained in:
Travis Ralston 2020-09-29 11:06:21 -06:00 committed by GitHub
commit 5534206e77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 144 additions and 42 deletions

View file

@ -217,7 +217,7 @@ limitations under the License.
} }
} }
&.mx_MessageComposer_hangup::before { &.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before {
background-color: $warning-color; background-color: $warning-color;
} }
} }

View file

@ -74,6 +74,8 @@ import {base32} from "rfc4648";
import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import WidgetStore from "./stores/WidgetStore";
import ActiveWidgetStore from "./stores/ActiveWidgetStore";
// until we ts-ify the js-sdk voip code // until we ts-ify the js-sdk voip code
type Call = any; type Call = any;
@ -351,6 +353,14 @@ export default class CallHandler {
console.info("Place conference call in %s", payload.room_id); console.info("Place conference call in %s", payload.room_id);
this.startCallApp(payload.room_id, payload.type); this.startCallApp(payload.room_id, payload.type);
break; break;
case 'end_conference':
console.info("Terminating conference call in %s", payload.room_id);
this.terminateCallApp(payload.room_id);
break;
case 'hangup_conference':
console.info("Leaving conference call in %s", payload.room_id);
this.hangupCallApp(payload.room_id);
break;
case 'incoming_call': case 'incoming_call':
{ {
if (this.getAnyActiveCall()) { if (this.getAnyActiveCall()) {
@ -398,10 +408,12 @@ export default class CallHandler {
show: true, show: true,
}); });
// prevent double clicking the call button
const room = MatrixClientPeg.get().getRoom(roomId); const room = MatrixClientPeg.get().getRoom(roomId);
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
const hasJitsi = currentJitsiWidgets.length > 0
if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) { || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
if (hasJitsi) {
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
title: _t('Call in Progress'), title: _t('Call in Progress'),
description: _t('A call is currently being placed!'), description: _t('A call is currently being placed!'),
@ -409,33 +421,6 @@ export default class CallHandler {
return; return;
} }
if (currentJitsiWidgets.length > 0) {
console.warn(
"Refusing to start conference call widget in " + roomId +
" a conference call widget is already present",
);
if (WidgetUtils.canUserModifyWidgets(roomId)) {
Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
title: _t('End Call'),
description: _t('Remove the group call from the room?'),
button: _t('End Call'),
cancelButton: _t('Cancel'),
onFinished: (endCall) => {
if (endCall) {
WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
}
},
});
} else {
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
title: _t('Call in Progress'),
description: _t("You don't have permission to remove the call from the room"),
});
}
return;
}
const jitsiDomain = Jitsi.getInstance().preferredDomain; const jitsiDomain = Jitsi.getInstance().preferredDomain;
const jitsiAuth = await Jitsi.getInstance().getJitsiAuth(); const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
let confId; let confId;
@ -484,4 +469,38 @@ export default class CallHandler {
console.error(e); console.error(e);
}); });
} }
private terminateCallApp(roomId: string) {
Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
hasCancelButton: true,
title: _t("End conference"),
description: _t("This will end the conference for everyone. Continue?"),
button: _t("End conference"),
onFinished: (proceed) => {
if (!proceed) return;
// We'll just obliterate them all. There should only ever be one, but might as well
// be safe.
const roomInfo = WidgetStore.instance.getRoom(roomId);
const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
jitsiWidgets.forEach(w => {
// setting invalid content removes it
WidgetUtils.setRoomWidget(roomId, w.id);
});
},
});
}
private hangupCallApp(roomId: string) {
const roomInfo = WidgetStore.instance.getRoom(roomId);
if (!roomInfo) return; // "should never happen" clauses go here
const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
jitsiWidgets.forEach(w => {
const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
if (!messaging) return; // more "should never happen" words
messaging.hangup();
});
}
} }

View file

@ -107,6 +107,17 @@ export default class WidgetMessaging {
}); });
} }
/**
* Tells the widget to hang up on its call.
* @returns {Promise<*>} Resolves when the widget has acknowledged the message.
*/
hangup() {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.Hangup,
});
}
/** /**
* Request a screenshot from a widget * Request a screenshot from a widget
* @return {Promise} To be resolved with screenshot data when it has been generated * @return {Promise} To be resolved with screenshot data when it has been generated

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd Copyright 2017, 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -32,6 +33,10 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview"; import ReplyPreview from "./ReplyPreview";
import {UIFeature} from "../../../settings/UIFeature"; import {UIFeature} from "../../../settings/UIFeature";
import WidgetStore from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
function ComposerAvatar(props) { function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -85,8 +90,15 @@ VideoCallButton.propTypes = {
}; };
function HangupButton(props) { function HangupButton(props) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const onHangupClick = () => { 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); const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
if (!call) { if (!call) {
return; return;
@ -98,14 +110,28 @@ function HangupButton(props) {
room_id: call.roomId, room_id: call.roomId,
}); });
}; };
return (<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_hangup"
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} onClick={onHangupClick}
title={_t('Hangup')} title={tooltip}
/>); disabled={!canLeaveConference}
/>
);
} }
HangupButton.propTypes = { HangupButton.propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
isConference: PropTypes.bool.isRequired,
canEndConference: PropTypes.bool,
isInConference: PropTypes.bool,
}; };
const EmojiButton = ({addEmoji}) => { const EmojiButton = ({addEmoji}) => {
@ -226,12 +252,17 @@ export default class MessageComposer extends React.Component {
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this); this._onTombstoneClick = this._onTombstoneClick.bind(this);
this.renderPlaceholderText = this.renderPlaceholderText.bind(this); this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetUpdate);
this._dispatcherRef = null; this._dispatcherRef = null;
this.state = { this.state = {
isQuoting: Boolean(RoomViewStore.getQuotingEvent()), isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
tombstone: this._getRoomTombstone(), tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(), canSendMessages: this.props.room.maySendMessage(),
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"), showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
}; };
} }
@ -247,6 +278,14 @@ export default class MessageComposer extends React.Component {
} }
}; };
_onWidgetUpdate = () => {
this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
};
_onActiveWidgetUpdate = () => {
this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)});
};
componentDidMount() { componentDidMount() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
@ -277,6 +316,8 @@ export default class MessageComposer extends React.Component {
if (this._roomStoreToken) { if (this._roomStoreToken) {
this._roomStoreToken.remove(); this._roomStoreToken.remove();
} }
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
@ -392,9 +433,19 @@ export default class MessageComposer extends React.Component {
} }
if (this.state.showCallButtons) { if (this.state.showCallButtons) {
if (callInProgress) { if (this.state.hasConference) {
const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
controls.push( controls.push(
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} />, <HangupButton
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 { } else {
controls.push( controls.push(

View file

@ -50,12 +50,10 @@
"You cannot place a call with yourself.": "You cannot place a call with yourself.", "You cannot place a call with yourself.": "You cannot place a call with yourself.",
"Call in Progress": "Call in Progress", "Call in Progress": "Call in Progress",
"A call is currently being placed!": "A call is currently being placed!", "A call is currently being placed!": "A call is currently being placed!",
"End Call": "End Call",
"Remove the group call from the room?": "Remove the group call from the room?",
"Cancel": "Cancel",
"You don't have permission to remove the call from the room": "You don't have permission to remove the call from the room",
"Permission Required": "Permission Required", "Permission Required": "Permission Required",
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
"End conference": "End conference",
"This will end the conference for everyone. Continue?": "This will end the conference for everyone. Continue?",
"Replying With Files": "Replying With Files", "Replying With Files": "Replying With Files",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?",
"Continue": "Continue", "Continue": "Continue",
@ -143,6 +141,7 @@
"Cancel entering passphrase?": "Cancel entering passphrase?", "Cancel entering passphrase?": "Cancel entering passphrase?",
"Are you sure you want to cancel entering passphrase?": "Are you sure you want to cancel entering passphrase?", "Are you sure you want to cancel entering passphrase?": "Are you sure you want to cancel entering passphrase?",
"Go Back": "Go Back", "Go Back": "Go Back",
"Cancel": "Cancel",
"Setting up keys": "Setting up keys", "Setting up keys": "Setting up keys",
"Messages": "Messages", "Messages": "Messages",
"Actions": "Actions", "Actions": "Actions",

View file

@ -22,6 +22,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher"; import defaultDispatcher from "../dispatcher/dispatcher";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import WidgetEchoStore from "../stores/WidgetEchoStore"; import WidgetEchoStore from "../stores/WidgetEchoStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import WidgetUtils from "../utils/WidgetUtils"; import WidgetUtils from "../utils/WidgetUtils";
import {SettingLevel} from "../settings/SettingLevel"; import {SettingLevel} from "../settings/SettingLevel";
import {WidgetType} from "../widgets/WidgetType"; import {WidgetType} from "../widgets/WidgetType";
@ -207,6 +208,24 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
} }
return roomInfo.widgets; return roomInfo.widgets;
} }
public doesRoomHaveConference(room: Room): boolean {
const roomInfo = this.getRoom(room.roomId);
if (!roomInfo) return false;
const currentWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
const hasPendingWidgets = WidgetEchoStore.roomHasPendingWidgetsOfType(room.roomId, [], WidgetType.JITSI);
return currentWidgets.length > 0 || hasPendingWidgets;
}
public isJoinedToConferenceIn(room: Room): boolean {
const roomInfo = this.getRoom(room.roomId);
if (!roomInfo) return false;
// A persistent conference widget indicates that we're participating
const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
return widgets.some(w => ActiveWidgetStore.getWidgetPersistence(w.id));
}
} }
window.mxWidgetStore = WidgetStore.instance; window.mxWidgetStore = WidgetStore.instance;

View file

@ -39,6 +39,7 @@ export enum KnownWidgetActions {
SetAlwaysOnScreen = "set_always_on_screen", SetAlwaysOnScreen = "set_always_on_screen",
ClientReady = "im.vector.ready", ClientReady = "im.vector.ready",
Terminate = "im.vector.terminate", Terminate = "im.vector.terminate",
Hangup = "im.vector.hangup",
} }
export type WidgetAction = KnownWidgetActions | string; export type WidgetAction = KnownWidgetActions | string;
@ -119,13 +120,15 @@ export class WidgetApi extends EventEmitter {
// Automatically acknowledge so we can move on // Automatically acknowledge so we can move on
this.replyToRequest(<ToWidgetRequest>payload, {}); this.replyToRequest(<ToWidgetRequest>payload, {});
} else if (payload.action === KnownWidgetActions.Terminate) { } else if (payload.action === KnownWidgetActions.Terminate
|| payload.action === KnownWidgetActions.Hangup) {
// Finalization needs to be async, so postpone with a promise // Finalization needs to be async, so postpone with a promise
let finalizePromise = Promise.resolve(); let finalizePromise = Promise.resolve();
const wait = (promise) => { const wait = (promise) => {
finalizePromise = finalizePromise.then(() => promise); finalizePromise = finalizePromise.then(() => promise);
}; };
this.emit('terminate', wait); const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup';
this.emit(emitName, wait);
Promise.resolve(finalizePromise).then(() => { Promise.resolve(finalizePromise).then(() => {
// Acknowledge that we're shut down now // Acknowledge that we're shut down now
this.replyToRequest(<ToWidgetRequest>payload, {}); this.replyToRequest(<ToWidgetRequest>payload, {});