187 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			187 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| 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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
 | |
| import { IWidget } from "matrix-widget-api";
 | |
| 
 | |
| import { ActionPayload } from "../dispatcher/payloads";
 | |
| import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
 | |
| import defaultDispatcher from "../dispatcher/dispatcher";
 | |
| import WidgetEchoStore from "../stores/WidgetEchoStore";
 | |
| import ActiveWidgetStore from "../stores/ActiveWidgetStore";
 | |
| import WidgetUtils from "../utils/WidgetUtils";
 | |
| import {WidgetType} from "../widgets/WidgetType";
 | |
| import {UPDATE_EVENT} from "./AsyncStore";
 | |
| import { MatrixClientPeg } from "../MatrixClientPeg";
 | |
| 
 | |
| interface IState {}
 | |
| 
 | |
| export interface IApp extends IWidget {
 | |
|     roomId: string;
 | |
|     eventId: string;
 | |
|     // eslint-disable-next-line camelcase
 | |
|     avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
 | |
| }
 | |
| 
 | |
| interface IRoomWidgets {
 | |
|     widgets: IApp[];
 | |
| }
 | |
| 
 | |
| function widgetUid(app: IApp): string {
 | |
|     return `${app.roomId ?? MatrixClientPeg.get().getUserId()}::${app.id}`;
 | |
| }
 | |
| 
 | |
| // TODO consolidate WidgetEchoStore into this
 | |
| // TODO consolidate ActiveWidgetStore into this
 | |
| export default class WidgetStore extends AsyncStoreWithClient<IState> {
 | |
|     private static internalInstance = new WidgetStore();
 | |
| 
 | |
|     private widgetMap = new Map<string, IApp>(); // Key is widget Unique ID (UID)
 | |
|     private roomMap = new Map<string, IRoomWidgets>(); // Key is room ID
 | |
| 
 | |
|     private constructor() {
 | |
|         super(defaultDispatcher, {});
 | |
| 
 | |
|         WidgetEchoStore.on("update", this.onWidgetEchoStoreUpdate);
 | |
|     }
 | |
| 
 | |
|     public static get instance(): WidgetStore {
 | |
|         return WidgetStore.internalInstance;
 | |
|     }
 | |
| 
 | |
|     private initRoom(roomId: string) {
 | |
|         if (!this.roomMap.has(roomId)) {
 | |
|             this.roomMap.set(roomId, {
 | |
|                 widgets: [],
 | |
|             });
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     protected async onReady(): Promise<any> {
 | |
|         this.matrixClient.on("Room", this.onRoom);
 | |
|         this.matrixClient.on("RoomState.events", this.onRoomStateEvents);
 | |
|         this.matrixClient.getRooms().forEach((room: Room) => {
 | |
|             this.loadRoomWidgets(room);
 | |
|         });
 | |
|         this.emit(UPDATE_EVENT, null); // emit for all rooms
 | |
|     }
 | |
| 
 | |
|     protected async onNotReady(): Promise<any> {
 | |
|         this.matrixClient.off("Room", this.onRoom);
 | |
|         this.matrixClient.off("RoomState.events", this.onRoomStateEvents);
 | |
|         this.widgetMap = new Map();
 | |
|         this.roomMap = new Map();
 | |
|         await this.reset({});
 | |
|     }
 | |
| 
 | |
|     // We don't need this, but our contract says we do.
 | |
|     protected async onAction(payload: ActionPayload) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     private onWidgetEchoStoreUpdate = (roomId: string, widgetId: string) => {
 | |
|         this.initRoom(roomId);
 | |
|         this.loadRoomWidgets(this.matrixClient.getRoom(roomId));
 | |
|         this.emit(UPDATE_EVENT, roomId);
 | |
|     };
 | |
| 
 | |
|     private generateApps(room: Room): IApp[] {
 | |
|         return WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room)).map((ev) => {
 | |
|             return WidgetUtils.makeAppConfig(
 | |
|                 ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(),
 | |
|             );
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     private loadRoomWidgets(room: Room) {
 | |
|         if (!room) return;
 | |
|         const roomInfo = this.roomMap.get(room.roomId) || <IRoomWidgets>{};
 | |
|         roomInfo.widgets = [];
 | |
| 
 | |
|         // first clean out old widgets from the map which originate from this room
 | |
|         // otherwise we are out of sync with the rest of the app with stale widget events during removal
 | |
|         Array.from(this.widgetMap.values()).forEach(app => {
 | |
|             if (app.roomId !== room.roomId) return; // skip - wrong room
 | |
|             this.widgetMap.delete(widgetUid(app));
 | |
|         });
 | |
| 
 | |
|         let edited = false;
 | |
|         this.generateApps(room).forEach(app => {
 | |
|             // Sanity check for https://github.com/vector-im/element-web/issues/15705
 | |
|             const existingApp = this.widgetMap.get(widgetUid(app));
 | |
|             if (existingApp) {
 | |
|                 console.warn(
 | |
|                     `Possible widget ID conflict for ${app.id} - wants to store in room ${app.roomId} ` +
 | |
|                     `but is currently stored as ${existingApp.roomId} - letting the want win`,
 | |
|                 );
 | |
|             }
 | |
| 
 | |
|             this.widgetMap.set(widgetUid(app), app);
 | |
|             roomInfo.widgets.push(app);
 | |
|             edited = true;
 | |
|         });
 | |
|         if (edited && !this.roomMap.has(room.roomId)) {
 | |
|             this.roomMap.set(room.roomId, roomInfo);
 | |
|         }
 | |
|         this.emit(room.roomId);
 | |
|     }
 | |
| 
 | |
|     private onRoom = (room: Room) => {
 | |
|         this.initRoom(room.roomId);
 | |
|         this.loadRoomWidgets(room);
 | |
|         this.emit(UPDATE_EVENT, room.roomId);
 | |
|     };
 | |
| 
 | |
|     private onRoomStateEvents = (ev: MatrixEvent) => {
 | |
|         if (ev.getType() !== "im.vector.modular.widgets") return; // TODO: Support m.widget too
 | |
|         const roomId = ev.getRoomId();
 | |
|         this.initRoom(roomId);
 | |
|         this.loadRoomWidgets(this.matrixClient.getRoom(roomId));
 | |
|         this.emit(UPDATE_EVENT, roomId);
 | |
|     };
 | |
| 
 | |
|     public getRoom = (roomId: string, initIfNeeded = false) => {
 | |
|         if (initIfNeeded) this.initRoom(roomId); // internally handles "if needed"
 | |
|         return this.roomMap.get(roomId);
 | |
|     };
 | |
| 
 | |
|     public getApps(roomId: string): IApp[] {
 | |
|         const roomInfo = this.getRoom(roomId);
 | |
|         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;
 |