Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/feat/widgets

 Conflicts:
	src/components/views/right_panel/WidgetCard.tsx
	src/stores/WidgetStore.ts
This commit is contained in:
Michael Telatynski 2020-10-07 10:04:45 +01:00
commit c784039cf6
73 changed files with 1832 additions and 1853 deletions

View file

@ -17,6 +17,7 @@ limitations under the License.
import EventEmitter from 'events';
import {MatrixClientPeg} from '../MatrixClientPeg';
import {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
/**
* Stores information about the widgets active in the app right now:
@ -29,15 +30,6 @@ class ActiveWidgetStore extends EventEmitter {
super();
this._persistentWidgetId = null;
// A list of negotiated capabilities for each widget, by ID
// {
// widgetId: [caps...],
// }
this._capsByWidgetId = {};
// A WidgetMessaging instance for each widget ID
this._widgetMessagingByWidgetId = {};
// What room ID each widget is associated with (if it's a room widget)
this._roomIdByWidgetId = {};
@ -54,8 +46,6 @@ class ActiveWidgetStore extends EventEmitter {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
this._capsByWidgetId = {};
this._widgetMessagingByWidgetId = {};
this._roomIdByWidgetId = {};
}
@ -76,9 +66,9 @@ class ActiveWidgetStore extends EventEmitter {
if (id !== this._persistentWidgetId) return;
const toDeleteId = this._persistentWidgetId;
WidgetMessagingStore.instance.stopMessagingById(id);
this.setWidgetPersistence(toDeleteId, false);
this.delWidgetMessaging(toDeleteId);
this.delWidgetCapabilities(toDeleteId);
this.delRoomId(toDeleteId);
}
@ -99,43 +89,6 @@ class ActiveWidgetStore extends EventEmitter {
return this._persistentWidgetId;
}
setWidgetCapabilities(widgetId, caps) {
this._capsByWidgetId[widgetId] = caps;
this.emit('update');
}
widgetHasCapability(widgetId, cap) {
return this._capsByWidgetId[widgetId] && this._capsByWidgetId[widgetId].includes(cap);
}
delWidgetCapabilities(widgetId) {
delete this._capsByWidgetId[widgetId];
this.emit('update');
}
setWidgetMessaging(widgetId, wm) {
// Stop any existing widget messaging first
this.delWidgetMessaging(widgetId);
this._widgetMessagingByWidgetId[widgetId] = wm;
this.emit('update');
}
getWidgetMessaging(widgetId) {
return this._widgetMessagingByWidgetId[widgetId];
}
delWidgetMessaging(widgetId) {
if (this._widgetMessagingByWidgetId[widgetId]) {
try {
this._widgetMessagingByWidgetId[widgetId].stop();
} catch (e) {
console.error('Failed to stop listening for widgetMessaging events', e.message);
}
delete this._widgetMessagingByWidgetId[widgetId];
this.emit('update');
}
}
getRoomId(widgetId) {
return this._roomIdByWidgetId[widgetId];
}

View file

@ -66,12 +66,14 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
/**
* Gets the user's avatar as an HTTP URL of the given size. If the user's
* avatar is not present, this returns null.
* @param size The size of the avatar
* @param size The size of the avatar. If zero, a full res copy of the avatar
* will be returned as an HTTP URL.
* @returns The HTTP URL of the user's avatar
*/
public getHttpAvatarUrl(size: number): string {
public getHttpAvatarUrl(size = 0): string {
if (!this.avatarMxc) return null;
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size);
const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
}
protected async onNotReady() {

View file

@ -22,6 +22,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import SettingsStore from "../settings/SettingsStore";
import WidgetEchoStore from "../stores/WidgetEchoStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import WidgetUtils from "../utils/WidgetUtils";
import {SettingLevel} from "../settings/SettingLevel";
import {WidgetType} from "../widgets/WidgetType";
@ -120,6 +121,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
}
private loadRoomWidgets(room: Room) {
if (!room) return;
const roomInfo = this.roomMap.get(room.roomId);
roomInfo.widgets = [];
this.generateApps(room).forEach(app => {
@ -160,17 +162,16 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
let pinned = roomInfo && roomInfo.pinned[widgetId];
// Jitsi widgets should be pinned by default
if (pinned === undefined && WidgetType.JITSI.matches(this.widgetMap.get(widgetId).type)) pinned = true;
const widget = this.widgetMap.get(widgetId);
if (pinned === undefined && WidgetType.JITSI.matches(widget?.type)) pinned = true;
return pinned;
}
public canPin(widgetId: string) {
// only allow pinning up to a max of two as we do not yet have grid splits
// the only case it will go to three is if you have two and then a Jitsi gets added
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId);
return roomInfo && Object.keys(roomInfo.pinned).filter(k => {
return roomInfo.widgets.some(app => app.id === k);
return roomInfo.pinned[k] && roomInfo.widgets.some(app => app.id === k);
}).length < MAX_PINNED;
}
@ -208,6 +209,24 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
}
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;

View file

@ -0,0 +1,21 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum ElementWidgetActions {
ClientReady = "im.vector.ready",
HangupCall = "im.vector.hangup",
OpenIntegrationManager = "integration_manager_open",
}

View file

@ -0,0 +1,266 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import {
ClientWidgetApi,
IStickerActionRequest,
IStickyActionRequest,
IWidget,
IWidgetApiRequest,
IWidgetApiRequestEmptyData,
IWidgetData,
MatrixCapabilities,
Widget,
WidgetApiFromWidgetAction,
} from "matrix-widget-api";
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
import { EventEmitter } from "events";
import { WidgetMessagingStore } from "./WidgetMessagingStore";
import RoomViewStore from "../RoomViewStore";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { OwnProfileStore } from "../OwnProfileStore";
import WidgetUtils from '../../utils/WidgetUtils';
import { IntegrationManagers } from "../../integrations/IntegrationManagers";
import SettingsStore from "../../settings/SettingsStore";
import { WidgetType } from "../../widgets/WidgetType";
import ActiveWidgetStore from "../ActiveWidgetStore";
import { objectShallowClone } from "../../utils/objects";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ElementWidgetActions } from "./ElementWidgetActions";
// TODO: Destroy all of this code
interface IAppTileProps {
// Note: these are only the props we care about
app: IWidget;
room: Room;
userId: string;
creatorUserId: string;
waitForIframeLoad: boolean;
whitelistCapabilities: string[];
userWidget: boolean;
}
// TODO: Don't use this because it's wrong
class ElementWidget extends Widget {
constructor(w) {
super(w);
}
public get templateUrl(): string {
if (WidgetType.JITSI.matches(this.type)) {
return WidgetUtils.getLocalJitsiWrapperUrl({
forLocalRender: true,
auth: this.rawData?.auth,
});
}
return super.templateUrl;
}
public get rawData(): IWidgetData {
let conferenceId = super.rawData['conferenceId'];
if (conferenceId === undefined) {
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
const parsedUrl = new URL(this.templateUrl);
conferenceId = parsedUrl.searchParams.get("confId");
}
return {
...super.rawData,
theme: SettingsStore.getValue("theme"),
conferenceId,
};
}
}
export class StopGapWidget extends EventEmitter {
private messaging: ClientWidgetApi;
private mockWidget: Widget;
private scalarToken: string;
constructor(private appTileProps: IAppTileProps) {
super();
let app = appTileProps.app;
// Backwards compatibility: not all old widgets have a creatorUserId
if (!app.creatorUserId) {
app = objectShallowClone(app); // clone to prevent accidental mutation
app.creatorUserId = MatrixClientPeg.get().getUserId();
}
this.mockWidget = new ElementWidget(app);
}
public get widgetApi(): ClientWidgetApi {
return this.messaging;
}
/**
* The URL to use in the iframe
*/
public get embedUrl(): string {
const templated = this.mockWidget.getCompleteUrl({
currentRoomId: RoomViewStore.getRoomId(),
currentUserId: MatrixClientPeg.get().getUserId(),
userDisplayName: OwnProfileStore.instance.displayName,
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
});
// Add in some legacy support sprinkles
// TODO: Replace these with proper widget params
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
const parsed = new URL(templated);
parsed.searchParams.set('widgetId', this.mockWidget.id);
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
// Give the widget a scalar token if we're supposed to (more legacy)
// TODO: Stop doing this
if (this.scalarToken) {
parsed.searchParams.set('scalar_token', this.scalarToken);
}
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
// in HTTP, but URL parsers encode them anyways.
return parsed.toString().replace(/%24/g, '$');
}
/**
* The URL to use in the popout
*/
public get popoutUrl(): string {
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
return WidgetUtils.getLocalJitsiWrapperUrl({
forLocalRender: false,
auth: this.mockWidget.rawData?.auth,
});
}
return this.embedUrl;
}
public get isManagedByManager(): boolean {
return !!this.scalarToken;
}
public get started(): boolean {
return !!this.messaging;
}
public start(iframe: HTMLIFrameElement) {
if (this.started) return;
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
this.messaging.addEventListener("ready", () => this.emit("ready"));
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
if (!this.appTileProps.userWidget && this.appTileProps.room) {
ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
}
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
this.messaging.addEventListener("action:set_always_on_screen",
(ev: CustomEvent<IStickyActionRequest>) => {
if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
}
},
);
} else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
this.messaging.addEventListener(`action:${ElementWidgetActions.OpenIntegrationManager}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
// Acknowledge first
ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
// First close the stickerpicker
defaultDispatcher.dispatch({action: "stickerpicker_close"});
// Now open the integration manager
// TODO: Spec this interaction.
const data = ev.detail.data;
const integType = data?.integType
const integId = <string>data?.integId;
// TODO: Open the right integration manager for the widget
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
`type_${integType}`,
integId,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
`type_${integType}`,
integId,
);
}
},
);
// TODO: Replace this event listener with appropriate driver functionality once the API
// establishes a sane way to send events back and forth.
this.messaging.addEventListener(`action:${WidgetApiFromWidgetAction.SendSticker}`,
(ev: CustomEvent<IStickerActionRequest>) => {
// Acknowledge first
ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
// Send the sticker
defaultDispatcher.dispatch({
action: 'm.sticker',
data: ev.detail.data,
widgetId: this.mockWidget.id,
});
},
);
}
}
public async prepare(): Promise<void> {
if (this.scalarToken) return;
const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget);
if (existingMessaging) this.messaging = existingMessaging;
try {
if (WidgetUtils.isScalarUrl(this.mockWidget.templateUrl)) {
const managers = IntegrationManagers.sharedInstance();
if (managers.hasManager()) {
// TODO: Pick the right manager for the widget
const defaultManager = managers.getPrimaryManager();
if (WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
const scalar = defaultManager.getScalarClient();
this.scalarToken = await scalar.getScalarToken();
}
}
}
} catch (e) {
// All errors are non-fatal
console.error("Error preparing widget communications: ", e);
}
}
public stop() {
if (ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
console.log("Skipping destroy - persistent widget");
return;
}
if (!this.started) return;
WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
ActiveWidgetStore.delRoomId(this.mockWidget.id);
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Capability, WidgetDriver } from "matrix-widget-api";
import { iterableUnion } from "../../utils/iterables";
// TODO: Purge this from the universe
export class StopGapWidgetDriver extends WidgetDriver {
constructor(private allowedCapabilities: Capability[]) {
super();
}
public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
return new Set(iterableUnion(requested, this.allowedCapabilities));
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ClientWidgetApi, Widget } from "matrix-widget-api";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { EnhancedMap } from "../../utils/maps";
/**
* Temporary holding store for widget messaging instances. This is eventually
* going to be merged with a more complete WidgetStore, but for now it's
* easiest to split this into a single place.
*/
export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
private static internalInstance = new WidgetMessagingStore();
// TODO: Fix uniqueness problem (widget IDs are not unique across the whole app)
private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
public constructor() {
super(defaultDispatcher);
}
public static get instance(): WidgetMessagingStore {
return WidgetMessagingStore.internalInstance;
}
protected async onAction(payload: ActionPayload): Promise<any> {
// nothing to do
}
protected async onReady(): Promise<any> {
// just in case
this.widgetMap.clear();
}
public storeMessaging(widget: Widget, widgetApi: ClientWidgetApi) {
this.stopMessaging(widget);
this.widgetMap.set(widget.id, widgetApi);
}
public stopMessaging(widget: Widget) {
this.widgetMap.remove(widget.id)?.stop();
}
public getMessaging(widget: Widget): ClientWidgetApi {
return this.widgetMap.get(widget.id);
}
/**
* Stops the widget messaging instance for a given widget ID.
* @param {string} widgetId The widget ID.
* @deprecated Widget IDs are not globally unique.
*/
public stopMessagingById(widgetId: string) {
this.widgetMap.remove(widgetId)?.stop();
}
/**
* Gets the widget messaging class for a given widget ID.
* @param {string} widgetId The widget ID.
* @returns {ClientWidgetApi} The widget API, or a falsey value if not found.
* @deprecated Widget IDs are not globally unique.
*/
public getMessagingForId(widgetId: string): ClientWidgetApi {
return this.widgetMap.get(widgetId);
}
}