Merge pull request #4444 from pv/jitsi-popout-immediate-join

Ensure active Jitsi conference is closed on widget pop-out
pull/21833/head
Travis Ralston 2020-06-07 20:22:39 -06:00 committed by GitHub
commit e4aeabe5a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 80 additions and 20 deletions

View File

@ -96,6 +96,17 @@ export default class WidgetMessaging {
}); });
} }
/**
* Tells the widget that it should terminate now.
* @returns {Promise<*>} Resolves when widget has acknowledged the message.
*/
terminate() {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.Terminate,
});
}
/** /**
* 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

@ -39,6 +39,8 @@ import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import PersistedElement from "./PersistedElement"; import PersistedElement from "./PersistedElement";
import {WidgetType} from "../../../widgets/WidgetType"; import {WidgetType} from "../../../widgets/WidgetType";
import {Capability} from "../../../widgets/WidgetApi";
import {sleep} from "../../../utils/promise";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false; const ENABLE_REACT_PERF = false;
@ -341,8 +343,21 @@ export default class AppTile extends React.Component {
/** /**
* Ends all widget interaction, such as cancelling calls and disabling webcams. * Ends all widget interaction, such as cancelling calls and disabling webcams.
* @private * @private
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/ */
_endWidgetActions() { _endWidgetActions() {
let terminationPromise;
if (this._hasCapability(Capability.ReceiveTerminate)) {
// Wait for widget to terminate within a timeout
const timeout = 2000;
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
} else {
terminationPromise = Promise.resolve();
}
return terminationPromise.finally(() => {
// HACK: This is a really dirty way to ensure that Jitsi cleans up // HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media // its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351 // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
@ -358,6 +373,7 @@ export default class AppTile extends React.Component {
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this._persistKey);
});
} }
/* If user has permission to modify widgets, delete the widget, /* If user has permission to modify widgets, delete the widget,
@ -381,12 +397,12 @@ export default class AppTile extends React.Component {
} }
this.setState({deleting: true}); this.setState({deleting: true});
this._endWidgetActions(); this._endWidgetActions().then(() => {
return WidgetUtils.setRoomWidget(
WidgetUtils.setRoomWidget(
this.props.room.roomId, this.props.room.roomId,
this.props.app.id, this.props.app.id,
).catch((e) => { );
}).catch((e) => {
console.error('Failed to delete widget', e); console.error('Failed to delete widget', e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -669,6 +685,17 @@ export default class AppTile extends React.Component {
} }
_onPopoutWidgetClick() { _onPopoutWidgetClick() {
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
this._endWidgetActions().then(() => {
if (this._appFrame.current) {
// Reload iframe
this._appFrame.current.src = this._getRenderedUrl();
this.setState({});
}
});
}
// Using Object.assign workaround as the following opens in a new window instead of a new tab. // Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'), Object.assign(document.createElement('a'),

View File

@ -421,6 +421,7 @@ export default class WidgetUtils {
if (WidgetType.JITSI.matches(appType)) { if (WidgetType.JITSI.matches(appType)) {
capWhitelist.push(Capability.AlwaysOnScreen); capWhitelist.push(Capability.AlwaysOnScreen);
} }
capWhitelist.push(Capability.ReceiveTerminate);
return capWhitelist; return capWhitelist;
} }

View File

@ -18,11 +18,13 @@ limitations under the License.
// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts // https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { EventEmitter } from "events";
export enum Capability { export enum Capability {
Screenshot = "m.capability.screenshot", Screenshot = "m.capability.screenshot",
Sticker = "m.sticker", Sticker = "m.sticker",
AlwaysOnScreen = "m.always_on_screen", AlwaysOnScreen = "m.always_on_screen",
ReceiveTerminate = "im.vector.receive_terminate",
} }
export enum KnownWidgetActions { export enum KnownWidgetActions {
@ -34,6 +36,7 @@ export enum KnownWidgetActions {
ReceiveOpenIDCredentials = "openid_credentials", ReceiveOpenIDCredentials = "openid_credentials",
SetAlwaysOnScreen = "set_always_on_screen", SetAlwaysOnScreen = "set_always_on_screen",
ClientReady = "im.vector.ready", ClientReady = "im.vector.ready",
Terminate = "im.vector.terminate",
} }
export type WidgetAction = KnownWidgetActions | string; export type WidgetAction = KnownWidgetActions | string;
@ -62,8 +65,13 @@ export interface FromWidgetRequest extends WidgetRequest {
/** /**
* Handles Riot <--> Widget interactions for embedded/standalone widgets. * Handles Riot <--> Widget interactions for embedded/standalone widgets.
*
* Emitted events:
* - terminate(wait): client requested the widget to terminate.
* Call the argument 'wait(promise)' to postpone the finalization until
* the given promise resolves.
*/ */
export class WidgetApi { export class WidgetApi extends EventEmitter {
private origin: string; private origin: string;
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
private readyPromise: Promise<any>; private readyPromise: Promise<any>;
@ -75,6 +83,8 @@ export class WidgetApi {
public expectingExplicitReady = false; public expectingExplicitReady = false;
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) { constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
super();
this.origin = new URL(currentUrl).origin; this.origin = new URL(currentUrl).origin;
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve); this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
@ -98,6 +108,17 @@ export class WidgetApi {
// 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) {
// Finalization needs to be async, so postpone with a promise
let finalizePromise = Promise.resolve();
const wait = (promise) => {
finalizePromise = finalizePromise.then(value => promise);
};
this.emit('terminate', wait);
Promise.resolve(finalizePromise).then(() => {
// Acknowledge that we're shut down now
this.replyToRequest(<ToWidgetRequest>payload, {});
});
} else { } else {
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
} }