Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/feat/room-list-widgets
Conflicts: src/components/views/elements/AppTile.js src/utils/WidgetUtils.ts
This commit is contained in:
commit
bec1d718e0
200 changed files with 7568 additions and 5549 deletions
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ import SettingsStore from "../settings/SettingsStore";
|
|||
import * as utils from "matrix-js-sdk/src/utils";
|
||||
import { UPDATE_EVENT } from "./AsyncStore";
|
||||
import FlairStore from "./FlairStore";
|
||||
import TagOrderStore from "./TagOrderStore";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import GroupFilterOrderStore from "./GroupFilterOrderStore";
|
||||
import GroupStore from "./GroupStore";
|
||||
import dis from "../dispatcher/dispatcher";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
|
||||
interface IState {
|
||||
// nothing of value - we use account data
|
||||
|
@ -50,7 +50,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
|||
|
||||
public getSelectedCommunityId(): string {
|
||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
return TagOrderStore.getSelectedTags()[0];
|
||||
return GroupFilterOrderStore.getSelectedTags()[0];
|
||||
}
|
||||
return null; // no selection as far as this function is concerned
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
|||
|
||||
public getGeneralChat(communityId: string): Room {
|
||||
const rooms = GroupStore.getGroupRooms(communityId)
|
||||
.map(r => MatrixClientPeg.get().getRoom(r.roomId))
|
||||
.map(r => this.matrixClient.getRoom(r.roomId))
|
||||
.filter(r => !!r);
|
||||
let chat = rooms.find(r => {
|
||||
const idState = r.currentState.getStateEvents("im.vector.general_chat", "");
|
||||
|
@ -88,6 +88,26 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
|||
return chat; // can be null
|
||||
}
|
||||
|
||||
public isAdminOf(communityId: string): boolean {
|
||||
const members = GroupStore.getGroupMembers(communityId);
|
||||
const myMember = members.find(m => m.userId === this.matrixClient.getUserId());
|
||||
return myMember?.isPrivileged;
|
||||
}
|
||||
|
||||
public canInviteTo(communityId: string): boolean {
|
||||
const generalChat = this.getGeneralChat(communityId);
|
||||
if (!generalChat) return this.isAdminOf(communityId);
|
||||
|
||||
const myMember = generalChat.getMember(this.matrixClient.getUserId());
|
||||
if (!myMember) return this.isAdminOf(communityId);
|
||||
|
||||
const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!pl) return this.isAdminOf(communityId);
|
||||
|
||||
const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite);
|
||||
return invitePl <= myMember.powerLevel;
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<any> {
|
||||
if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
return;
|
||||
|
|
|
@ -46,7 +46,7 @@ function commonPrefix(a, b) {
|
|||
return "";
|
||||
}
|
||||
/**
|
||||
* A class for storing application state for ordering tags in the TagPanel.
|
||||
* A class for storing application state for ordering tags in the GroupFilterPanel.
|
||||
*/
|
||||
class CustomRoomTagStore extends EventEmitter {
|
||||
constructor() {
|
||||
|
|
|
@ -33,9 +33,9 @@ const INITIAL_STATE = {
|
|||
};
|
||||
|
||||
/**
|
||||
* A class for storing application state for ordering tags in the TagPanel.
|
||||
* A class for storing application state for ordering tags in the GroupFilterPanel.
|
||||
*/
|
||||
class TagOrderStore extends Store {
|
||||
class GroupFilterOrderStore extends Store {
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
||||
|
@ -268,7 +268,7 @@ class TagOrderStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
if (global.singletonTagOrderStore === undefined) {
|
||||
global.singletonTagOrderStore = new TagOrderStore();
|
||||
if (global.singletonGroupFilterOrderStore === undefined) {
|
||||
global.singletonGroupFilterOrderStore = new GroupFilterOrderStore();
|
||||
}
|
||||
export default global.singletonTagOrderStore;
|
||||
export default global.singletonGroupFilterOrderStore;
|
|
@ -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() {
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 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 dis from '../dispatcher/dispatcher';
|
||||
import {Store} from 'flux/utils';
|
||||
|
||||
const INITIAL_STATE = {
|
||||
cachedPassword: localStorage.getItem('mx_pass'),
|
||||
};
|
||||
|
||||
/**
|
||||
* A class for storing application state to do with the session. This is a simple flux
|
||||
* store that listens for actions and updates its state accordingly, informing any
|
||||
* listeners (views) of state changes.
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* sessionStore.addListener(() => {
|
||||
* this.setState({ cachedPassword: sessionStore.getCachedPassword() })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
class SessionStore extends Store {
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
||||
// Initialise state
|
||||
this._state = INITIAL_STATE;
|
||||
}
|
||||
|
||||
_update() {
|
||||
// Persist state to localStorage
|
||||
if (this._state.cachedPassword) {
|
||||
localStorage.setItem('mx_pass', this._state.cachedPassword);
|
||||
} else {
|
||||
localStorage.removeItem('mx_pass', this._state.cachedPassword);
|
||||
}
|
||||
|
||||
this.__emitChange();
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
this._state = Object.assign(this._state, newState);
|
||||
this._update();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
switch (payload.action) {
|
||||
case 'cached_password':
|
||||
this._setState({
|
||||
cachedPassword: payload.cachedPassword,
|
||||
});
|
||||
break;
|
||||
case 'password_changed':
|
||||
this._setState({
|
||||
cachedPassword: null,
|
||||
});
|
||||
break;
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out':
|
||||
this._setState({
|
||||
cachedPassword: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getCachedPassword() {
|
||||
return this._state.cachedPassword;
|
||||
}
|
||||
}
|
||||
|
||||
let singletonSessionStore = null;
|
||||
if (!singletonSessionStore) {
|
||||
singletonSessionStore = new SessionStore();
|
||||
}
|
||||
export default singletonSessionStore;
|
|
@ -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";
|
||||
|
@ -123,6 +124,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 => {
|
||||
|
@ -163,7 +165,8 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -173,7 +176,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
|||
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 < 2;
|
||||
}
|
||||
|
||||
|
@ -211,6 +214,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;
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { RoomListStoreClass } from "./RoomListStore";
|
||||
import TagOrderStore from "../TagOrderStore";
|
||||
import GroupFilterOrderStore from "../GroupFilterOrderStore";
|
||||
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
|
||||
import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
|
||||
|
||||
|
@ -26,12 +26,12 @@ export class TagWatcher {
|
|||
private filters = new Map<string, CommunityFilterCondition>();
|
||||
|
||||
constructor(private store: RoomListStoreClass) {
|
||||
TagOrderStore.addListener(this.onTagsUpdated);
|
||||
GroupFilterOrderStore.addListener(this.onTagsUpdated);
|
||||
}
|
||||
|
||||
private onTagsUpdated = () => {
|
||||
const lastTags = Array.from(this.filters.keys());
|
||||
const newTags = TagOrderStore.getSelectedTags();
|
||||
const newTags = GroupFilterOrderStore.getSelectedTags();
|
||||
|
||||
if (arrayHasDiff(lastTags, newTags)) {
|
||||
// Selected tags changed, do some filtering
|
||||
|
|
|
@ -27,7 +27,13 @@ export class ReactionEventPreview implements IPreview {
|
|||
const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms");
|
||||
const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all");
|
||||
|
||||
if (!showAll && (!showDms || DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) return null;
|
||||
// If we're not showing all reactions, see if we're showing DMs instead
|
||||
if (!showAll) {
|
||||
// If we're not showing reactions on DMs, or we are and the room isn't a DM, skip
|
||||
if (!(showDms && DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) return null; // invalid reaction (probably redacted)
|
||||
|
|
21
src/stores/widgets/ElementWidgetActions.ts
Normal file
21
src/stores/widgets/ElementWidgetActions.ts
Normal 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",
|
||||
}
|
358
src/stores/widgets/StopGapWidget.ts
Normal file
358
src/stores/widgets/StopGapWidget.ts
Normal file
|
@ -0,0 +1,358 @@
|
|||
/*
|
||||
* 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,
|
||||
IGetOpenIDActionRequest,
|
||||
IGetOpenIDActionResponseData,
|
||||
IStickerActionRequest,
|
||||
IStickyActionRequest,
|
||||
ITemplateParams,
|
||||
IWidget,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiRequestEmptyData,
|
||||
IWidgetData,
|
||||
MatrixCapabilities,
|
||||
OpenIDRequestState,
|
||||
runTemplate,
|
||||
Widget,
|
||||
WidgetApiToWidgetAction,
|
||||
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";
|
||||
import Modal from "../../Modal";
|
||||
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||
|
||||
// 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: super.rawData?.auth, // this.rawData can call templateUrl, do this to prevent looping
|
||||
});
|
||||
}
|
||||
return super.templateUrl;
|
||||
}
|
||||
|
||||
public get popoutTemplateUrl(): string {
|
||||
if (WidgetType.JITSI.matches(this.type)) {
|
||||
return WidgetUtils.getLocalJitsiWrapperUrl({
|
||||
forLocalRender: false, // The only important difference between this and templateUrl()
|
||||
auth: super.rawData?.auth,
|
||||
});
|
||||
}
|
||||
return this.templateUrl; // use this instead of super to ensure we get appropriate templating
|
||||
}
|
||||
|
||||
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(super.templateUrl); // use super to get the raw widget URL
|
||||
conferenceId = parsedUrl.searchParams.get("confId");
|
||||
}
|
||||
let domain = super.rawData['domain'];
|
||||
if (domain === undefined) {
|
||||
// v1 widgets default to jitsi.riot.im regardless of user settings
|
||||
domain = "jitsi.riot.im";
|
||||
}
|
||||
return {
|
||||
...super.rawData,
|
||||
theme: SettingsStore.getValue("theme"),
|
||||
conferenceId,
|
||||
domain,
|
||||
};
|
||||
}
|
||||
|
||||
public getCompleteUrl(params: ITemplateParams, asPopout=false): string {
|
||||
return runTemplate(asPopout ? this.popoutTemplateUrl : this.templateUrl, {
|
||||
// we need to supply a whole widget to the template, but don't have
|
||||
// easy access to the definition the superclass is using, so be sad
|
||||
// and gutwrench it.
|
||||
// This isn't a problem when the widget architecture is fixed and this
|
||||
// subclass gets deleted.
|
||||
...super['definition'], // XXX: Private member access
|
||||
data: this.rawData,
|
||||
}, params);
|
||||
}
|
||||
}
|
||||
|
||||
export class StopGapWidget extends EventEmitter {
|
||||
private messaging: ClientWidgetApi;
|
||||
private mockWidget: ElementWidget;
|
||||
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 {
|
||||
return this.runUrlTemplate({asPopout: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to use in the popout
|
||||
*/
|
||||
public get popoutUrl(): string {
|
||||
return this.runUrlTemplate({asPopout: true});
|
||||
}
|
||||
|
||||
private runUrlTemplate(opts = {asPopout: false}): string {
|
||||
const templated = this.mockWidget.getCompleteUrl({
|
||||
currentRoomId: RoomViewStore.getRoomId(),
|
||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||
userDisplayName: OwnProfileStore.instance.displayName,
|
||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||
}, opts?.asPopout);
|
||||
|
||||
const parsed = new URL(templated);
|
||||
|
||||
// Add in some legacy support sprinkles (for non-popout widgets)
|
||||
// TODO: Replace these with proper widget params
|
||||
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
|
||||
if (!opts?.asPopout) {
|
||||
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, '$');
|
||||
}
|
||||
|
||||
public get isManagedByManager(): boolean {
|
||||
return !!this.scalarToken;
|
||||
}
|
||||
|
||||
public get started(): boolean {
|
||||
return !!this.messaging;
|
||||
}
|
||||
|
||||
private get widgetId() {
|
||||
return this.messaging.widget.id;
|
||||
}
|
||||
|
||||
private onOpenIdReq = async (ev: CustomEvent<IGetOpenIDActionRequest>) => {
|
||||
if (ev?.detail?.widgetId !== this.widgetId) return;
|
||||
|
||||
const rawUrl = this.appTileProps.app.url;
|
||||
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, rawUrl, this.appTileProps.userWidget);
|
||||
|
||||
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
||||
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
|
||||
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
|
||||
state: OpenIDRequestState.Blocked,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
|
||||
state: OpenIDRequestState.Allowed,
|
||||
...credentials,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm that we received the request
|
||||
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
|
||||
state: OpenIDRequestState.PendingUserConfirmation,
|
||||
});
|
||||
|
||||
// Actually ask for permission to send the user's data
|
||||
Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
|
||||
widgetUrl: rawUrl.substr(0, rawUrl.lastIndexOf("?")),
|
||||
widgetId: this.widgetId,
|
||||
isUserWidget: this.appTileProps.userWidget,
|
||||
|
||||
onFinished: async (confirm) => {
|
||||
const responseBody: IGetOpenIDActionResponseData = {
|
||||
state: confirm ? OpenIDRequestState.Allowed : OpenIDRequestState.Blocked,
|
||||
original_request_id: ev.detail.requestId, // eslint-disable-line camelcase
|
||||
};
|
||||
if (confirm) {
|
||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
||||
Object.assign(responseBody, credentials);
|
||||
}
|
||||
this.messaging.transport.send(WidgetApiToWidgetAction.OpenIDCredentials, responseBody).catch(error => {
|
||||
console.error("Failed to send OpenID credentials: ", error);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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.on("preparing", () => this.emit("preparing"));
|
||||
this.messaging.on("ready", () => this.emit("ready"));
|
||||
this.messaging.on(`action:${WidgetApiFromWidgetAction.GetOpenIDCredentials}`, this.onOpenIdReq);
|
||||
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.on("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.on(`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.on(`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(opts = {forceDestroy: false}) {
|
||||
if (!opts?.forceDestroy && 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);
|
||||
}
|
||||
}
|
30
src/stores/widgets/StopGapWidgetDriver.ts
Normal file
30
src/stores/widgets/StopGapWidgetDriver.ts
Normal 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));
|
||||
}
|
||||
}
|
82
src/stores/widgets/WidgetMessagingStore.ts
Normal file
82
src/stores/widgets/WidgetMessagingStore.ts
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue