mirror of https://github.com/vector-im/riot-web
Merge pull request #4444 from pv/jitsi-popout-immediate-join
Ensure active Jitsi conference is closed on widget pop-outpull/21833/head
commit
e4aeabe5a6
|
@ -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
|
||||||
|
|
|
@ -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,23 +343,37 @@ 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() {
|
||||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
let terminationPromise;
|
||||||
// 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
|
if (this._hasCapability(Capability.ReceiveTerminate)) {
|
||||||
if (this._appFrame.current) {
|
// Wait for widget to terminate within a timeout
|
||||||
// In practice we could just do `+= ''` to trick the browser
|
const timeout = 2000;
|
||||||
// into thinking the URL changed, however I can foresee this
|
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
|
||||||
// being optimized out by a browser. Instead, we'll just point
|
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
|
||||||
// the iframe at a page that is reasonably safe to use in the
|
} else {
|
||||||
// event the iframe doesn't wink away.
|
terminationPromise = Promise.resolve();
|
||||||
// This is relative to where the Riot instance is located.
|
|
||||||
this._appFrame.current.src = 'about:blank';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
return terminationPromise.finally(() => {
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
// 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
|
||||||
|
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||||
|
if (this._appFrame.current) {
|
||||||
|
// In practice we could just do `+= ''` to trick the browser
|
||||||
|
// into thinking the URL changed, however I can foresee this
|
||||||
|
// being optimized out by a browser. Instead, we'll just point
|
||||||
|
// the iframe at a page that is reasonably safe to use in the
|
||||||
|
// event the iframe doesn't wink away.
|
||||||
|
// This is relative to where the Riot instance is located.
|
||||||
|
this._appFrame.current.src = 'about:blank';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the widget from the persisted store for good measure.
|
||||||
|
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'),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue